13 Commits

34 changed files with 3236 additions and 553 deletions

View File

@@ -303,11 +303,8 @@ async function generateApiClient(): Promise<void> {
async function generateSignalRClient(): Promise<void> { async function generateSignalRClient(): Promise<void> {
console.log("Generating SignalR TypeScript client..."); console.log("Generating SignalR TypeScript client...");
try { try {
// TypedSignalR.Client.TypeScript.Analyzer 会在编译时自动生成客户端
// 我们只需要确保服务器项目构建一次即可生成 TypeScript 客户端
const { stdout, stderr } = await execAsync( const { stdout, stderr } = await execAsync(
"dotnet build --configuration Release", "dotnet tsrts --project ./server/server.csproj --output ./src/",
{ cwd: "./server" }
); );
if (stdout) console.log(stdout); if (stdout) console.log(stdout);
if (stderr) console.error(stderr); if (stderr) console.error(stderr);

View File

@@ -283,4 +283,28 @@ public class NumberTest
var reversed2 = Number.ReverseBits(new byte[0]); var reversed2 = Number.ReverseBits(new byte[0]);
Assert.Empty(reversed2); Assert.Empty(reversed2);
} }
/// <summary>
/// 测试 GetLength
/// </summary>
[Fact]
public void Test_GetLength()
{
Assert.Equal(5, Number.GetLength(12345));
Assert.Equal(4, Number.GetLength(-123));
Assert.Equal(1, Number.GetLength(0));
}
/// <summary>
/// 测试 IntPow
/// </summary>
[Fact]
public void Test_IntPow()
{
Assert.Equal(8, Number.IntPow(2, 3));
Assert.Equal(1, Number.IntPow(5, 0));
Assert.Equal(0, Number.IntPow(0, 5));
Assert.Equal(7, Number.IntPow(7, 1));
Assert.Equal(81, Number.IntPow(3, 4));
}
} }

View File

@@ -0,0 +1,99 @@
using Microsoft.AspNetCore.SignalR;
using Moq;
using server.Hubs;
using server.Services;
public class ProgressTrackerTest
{
[Fact]
public async Task Test_ProgressReporter_Basic()
{
int reportedValue = -1;
var reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
// Report
reporter.Report(50);
Assert.Equal(50, reporter.Progress);
Assert.Equal(ProgressStatus.InProgress, reporter.Status);
Assert.Equal(50, reportedValue);
// Increase by step
reporter.Increase();
Assert.Equal(60, reporter.Progress);
// Increase by value
reporter.Increase(20);
Assert.Equal(80, reporter.Progress);
// Finish
reporter.Finish();
Assert.Equal(ProgressStatus.Completed, reporter.Status);
Assert.Equal(100, reporter.Progress);
// Cancel
reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
reporter.Cancel();
Assert.Equal(ProgressStatus.Canceled, reporter.Status);
Assert.Equal("User Cancelled", reporter.ErrorMessage);
// Error
reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
reporter.Error("Test Error");
Assert.Equal(ProgressStatus.Failed, reporter.Status);
Assert.Equal("Test Error", reporter.ErrorMessage);
// CreateChild
var parent = new ProgressReporter(async v => { await Task.CompletedTask; }, 10, 100, 5);
var child = parent.CreateChild(50, 5);
Assert.Equal(ProgressStatus.Pending, child.Status);
Assert.NotNull(child);
// Child Increase
child.Increase();
Assert.Equal(ProgressStatus.InProgress, child.Status);
Assert.Equal(20, child.ProgressPercent);
Assert.Equal(20, parent.Progress);
// Child Complete
child.Finish();
Assert.Equal(ProgressStatus.Completed, child.Status);
Assert.Equal(100, child.ProgressPercent);
Assert.Equal(60, parent.Progress);
}
[Fact]
public void Test_ProgressTrackerService_Basic()
{
// Mock SignalR HubContext
var mockHubContext = new Mock<IHubContext<ProgressHub, IProgressReceiver>>();
var service = new ProgressTrackerService(mockHubContext.Object);
// CreateTask
var (taskId, reporter) = service.CreateTask();
Assert.NotNull(taskId);
Assert.NotNull(reporter);
// GetReporter
var optReporter = service.GetReporter(taskId);
Assert.True(optReporter.HasValue);
Assert.Equal(reporter, optReporter.Value);
// GetProgressStatus
var optStatus = service.GetProgressStatus(taskId);
Assert.True(optStatus.HasValue);
Assert.Equal(ProgressStatus.Pending, optStatus.Value);
// BindTask
var bindResult = service.BindTask(taskId, "conn1");
Assert.True(bindResult);
// CancelTask
var cancelResult = service.CancelTask(taskId);
Assert.True(cancelResult);
// After cancel, status should be Cancelled
var optStatus2 = service.GetProgressStatus(taskId);
Assert.True(optStatus2.HasValue);
Assert.Equal(ProgressStatus.Canceled, optStatus2.Value);
}
}

View File

@@ -11,6 +11,7 @@
<PackageReference Include="coverlet.collector" Version="6.0.2" /> <PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup> </ItemGroup>

View File

@@ -95,6 +95,12 @@ try
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader() .AllowAnyHeader()
); );
options.AddPolicy("SignalR", policy => policy
.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
}); });
// Use SignalR // Use SignalR
@@ -139,6 +145,12 @@ 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>());
// 添加进度跟踪服务
builder.Services.AddSingleton<ProgressTrackerService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<ProgressTrackerService>());
// Application Settings // Application Settings
var app = builder.Build(); var app = builder.Build();
@@ -201,12 +213,16 @@ try
}; };
}); });
app.UseSwaggerUi(); app.UseSwaggerUi();
// SignalR
app.UseWebSockets();
app.UseSignalRHubSpecification(); app.UseSignalRHubSpecification();
app.UseSignalRHubDevelopmentUI(); app.UseSignalRHubDevelopmentUI();
// Router // Router
app.MapControllers(); app.MapControllers();
app.MapHub<server.Hubs.JtagHub.JtagHub>("hubs/JtagHub").RequireCors("Users"); app.MapHub<server.Hubs.JtagHub>("hubs/JtagHub");
app.MapHub<server.Hubs.ProgressHub>("hubs/ProgressHub");
// Setup Program // Setup Program
MsgBus.Init(); MsgBus.Init();

View File

@@ -348,4 +348,37 @@ public class Number
} }
return dstBytes; return dstBytes;
} }
/// <summary>
/// 获取数字的长度
/// </summary>
/// <param name="number">数字</param>
/// <returns>数字的长度</returns>
public static int GetLength(int number)
{
// 将整数转换为字符串
string numberString = number.ToString();
// 返回字符串的长度
return numberString.Length;
}
/// <summary>
/// 计算整形的幂
/// </summary>
/// <param name="x">底数</param>
/// <param name="pow">幂</param>
/// <returns>计算结果</returns>
public static int IntPow(int x, int pow)
{
int ret = 1;
while (pow != 0)
{
if ((pow & 1) == 1)
ret *= x;
x *= x;
pow >>= 1;
}
return ret;
}
} }

View File

@@ -0,0 +1,97 @@
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);
}
// 禁用指定板子的 HDMI 传输
[HttpPost("DisableHdmiTransmission")]
[Authorize]
public async Task<IActionResult> DisableHdmiTransmission()
{
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.");
try
{
await _videoStreamService.DisableHdmiTransmissionAsync(boardId.ToString());
return Ok($"HDMI transmission for board {boardId} disabled.");
}
catch (Exception ex)
{
logger.Error(ex, $"Failed to disable HDMI transmission for board {boardId}");
return StatusCode(500, $"Error disabling HDMI transmission: {ex.Message}");
}
}
}

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Database; using Database;
using server.Services;
namespace server.Controllers; namespace server.Controllers;
@@ -15,6 +16,15 @@ public class JtagController : ControllerBase
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ProgressTrackerService _tracker;
private const string BITSTREAM_PATH = "bitstream/Jtag";
public JtagController(ProgressTrackerService tracker)
{
_tracker = tracker;
}
/// <summary> /// <summary>
/// 控制器首页信息 /// 控制器首页信息
/// </summary> /// </summary>
@@ -117,14 +127,14 @@ public class JtagController : ControllerBase
/// <param name="address">JTAG 设备地址</param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param> /// <param name="port">JTAG 设备端口</param>
/// <param name="bitstreamId">比特流ID</param> /// <param name="bitstreamId">比特流ID</param>
/// <returns>下载结果</returns> /// <returns>进度跟踪TaskID</returns>
[HttpPost("DownloadBitstream")] [HttpPost("DownloadBitstream")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> DownloadBitstream(string address, int port, int bitstreamId) public async ValueTask<IResult> DownloadBitstream(string address, int port, int bitstreamId, CancellationToken cancelToken)
{ {
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}"); logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
@@ -176,6 +186,12 @@ public class JtagController : ControllerBase
logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes"); logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
// 定义进度跟踪
var (taskId, progress) = _tracker.CreateTask(cancelToken);
progress.Report(10);
_ = Task.Run(async () =>
{
// 定义缓冲区大小: 32KB // 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024]; byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024]; byte[] revBuffer = new byte[32 * 1024];
@@ -193,7 +209,8 @@ public class JtagController : ControllerBase
if (!retBuffer.IsSuccessful) if (!retBuffer.IsSuccessful)
{ {
logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}"); logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error); progress.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
return;
} }
revBuffer = retBuffer.Value; revBuffer = retBuffer.Value;
@@ -210,6 +227,8 @@ public class JtagController : ControllerBase
var processedBytes = outputStream.ToArray(); var processedBytes = outputStream.ToArray();
logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}"); logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
progress.Report(20);
// 下载比特流 // 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(processedBytes); var ret = await jtagCtrl.DownloadBitstream(processedBytes);
@@ -217,14 +236,17 @@ public class JtagController : ControllerBase
if (ret.IsSuccessful) if (ret.IsSuccessful)
{ {
logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}"); logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}");
return TypedResults.Ok(ret.Value); progress.Finish();
} }
else else
{ {
logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}"); logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
return TypedResults.InternalServerError(ret.Error); progress.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
} }
} }
});
return TypedResults.Ok(taskId);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -58,6 +58,10 @@ public class LogicAnalyzerController : ControllerBase
/// </summary> /// </summary>
public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT; public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT;
/// <summary> /// <summary>
/// 时钟分频系数
/// </summary>
public AnalyzerClockDiv ClockDiv { get; set; } = AnalyzerClockDiv.DIV1;
/// <summary>
/// 信号触发配置列表 /// 信号触发配置列表
/// </summary> /// </summary>
public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty<SignalTriggerConfig>(); public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty<SignalTriggerConfig>();
@@ -248,6 +252,7 @@ public class LogicAnalyzerController : ControllerBase
/// <param name="capture_length">深度</param> /// <param name="capture_length">深度</param>
/// <param name="pre_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> /// <param name="channel_div">有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])</param>
/// <param name="clock_div">采样时钟分频系数</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[HttpPost("SetCaptureParams")] [HttpPost("SetCaptureParams")]
[EnableCors("Users")] [EnableCors("Users")]
@@ -255,11 +260,12 @@ public class LogicAnalyzerController : ControllerBase
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div) public async Task<IActionResult> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div, AnalyzerClockDiv clock_div)
{ {
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("预采样深度必须小于捕获深度");
@@ -268,18 +274,18 @@ public class LogicAnalyzerController : ControllerBase
if (analyzer == null) if (analyzer == null)
return BadRequest("用户未绑定有效的实验板"); return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.SetCaptureParams(capture_length, pre_capture_length, channel_div); var result = await analyzer.SetCaptureParams(capture_length, pre_capture_length, channel_div, clock_div);
if (!result.IsSuccessful) if (!result.IsSuccessful)
{ {
logger.Error($"设置深度、预采样深度、有效通道失败: {result.Error}"); logger.Error($"设置深度、预采样深度、有效通道、时钟分频失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道失败"); return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道、时钟分频失败");
} }
return Ok(result.Value); return Ok(result.Value);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex, "设置深度、预采样深度、有效通道失败时发生异常"); logger.Error(ex, "设置深度、预采样深度、有效通道、时钟分频失败时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试"); return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
} }
} }
@@ -331,7 +337,7 @@ public class LogicAnalyzerController : ControllerBase
} }
// 设置深度、预采样深度、有效通道 // 设置深度、预采样深度、有效通道
var paramsResult = await analyzer.SetCaptureParams( var paramsResult = await analyzer.SetCaptureParams(
config.CaptureLength, config.PreCaptureLength, config.ChannelDiv); config.CaptureLength, config.PreCaptureLength, config.ChannelDiv, config.ClockDiv);
if (!paramsResult.IsSuccessful) if (!paramsResult.IsSuccessful)
{ {
logger.Error($"设置深度、预采样深度、有效通道失败: {paramsResult.Error}"); logger.Error($"设置深度、预采样深度、有效通道失败: {paramsResult.Error}");

View File

@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using DotNext; using DotNext;
@@ -8,7 +7,7 @@ using System.Collections.Concurrent;
using TypedSignalR.Client; using TypedSignalR.Client;
using Tapper; using Tapper;
namespace server.Hubs.JtagHub; namespace server.Hubs;
[Hub] [Hub]
public interface IJtagHub public interface IJtagHub
@@ -25,12 +24,19 @@ public interface IJtagReceiver
} }
[Authorize] [Authorize]
[EnableCors("Users")] [EnableCors("SignalR")]
public class JtagHub : Hub<IJtagReceiver>, IJtagHub public class JtagHub : Hub<IJtagReceiver>, IJtagHub
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private ConcurrentDictionary<string, int> FreqTable = new(); private static ConcurrentDictionary<string, int> FreqTable = new();
private ConcurrentDictionary<string, CancellationTokenSource> CancellationTokenSourceTable = new(); private static ConcurrentDictionary<string, CancellationTokenSource> CancellationTokenSourceTable = new();
private readonly IHubContext<JtagHub, IJtagReceiver> _hubContext;
public JtagHub(IHubContext<JtagHub, IJtagReceiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Peripherals.JtagClient.Jtag> GetJtagClient(string userName) private Optional<Peripherals.JtagClient.Jtag> GetJtagClient(string userName)
{ {
@@ -92,33 +98,40 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
} }
await SetBoundaryScanFreq(freq); await SetBoundaryScanFreq(freq);
var cts = CancellationTokenSource.CreateLinkedTokenSource(); var cts = new CancellationTokenSource();
CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts); CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts);
_ = Task _ = Task.Run(
.Run( () => BoundaryScanLogicPorts(Context.ConnectionId, userName, cts.Token),
() => BoundaryScanLogicPorts(
Context.ConnectionId,
userName,
cts.Token),
cts.Token) cts.Token)
.ContinueWith((task) => .ContinueWith((task) =>
{ {
if (!task.IsFaulted) if (task.IsFaulted)
{ {
return; // 遍历所有异常
} foreach (var ex in task.Exception.InnerExceptions)
{
if (task.Exception.InnerException is OperationCanceledException) if (ex is OperationCanceledException)
{ {
logger.Info($"Boundary scan operation cancelled for user {userName}"); logger.Info($"Boundary scan operation cancelled for user {userName}");
} }
else else
{ {
logger.Error(task.Exception); logger.Error($"Boundary scan operation failed for user {userName}: {ex}");
}
}
}
else if (task.IsCanceled)
{
logger.Info($"Boundary scan operation cancelled for user {userName}");
}
else
{
logger.Info($"Boundary scan completed successfully for user {userName}");
} }
}); });
logger.Info($"Boundary scan started for user {userName}");
return true; return true;
} }
catch (Exception error) catch (Exception error)
@@ -144,10 +157,12 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
cts.Cancel(); cts.Cancel();
cts.Token.WaitHandle.WaitOne(); cts.Token.WaitHandle.WaitOne();
logger.Info($"Boundary scan stopped for user {userName}");
return true; return true;
} }
private async void BoundaryScanLogicPorts(string connectionID, string userName, CancellationToken cancellationToken) private async Task BoundaryScanLogicPorts(string connectionID, string userName, CancellationToken cancellationToken)
{ {
var jtagCtrl = GetJtagClient(userName).OrThrow(() => new InvalidOperationException("JTAG client not found")); var jtagCtrl = GetJtagClient(userName).OrThrow(() => new InvalidOperationException("JTAG client not found"));
var cntFail = 0; var cntFail = 0;
@@ -161,9 +176,10 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
{ {
logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address}: {ret.Error}"); logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address}: {ret.Error}");
cntFail++; cntFail++;
continue;
} }
await this.Clients.Client(connectionID).OnReceiveBoundaryScanData(ret.Value); await _hubContext.Clients.Client(connectionID).OnReceiveBoundaryScanData(ret.Value);
// logger.Info($"User {userName} successfully completed boundary scan for device {jtagCtrl.address}"); // logger.Info($"User {userName} successfully completed boundary scan for device {jtagCtrl.address}");
await Task.Delay(FreqTable.TryGetValue(userName, out var freq) ? 1000 / freq : 1000 / 100, cancellationToken); await Task.Delay(FreqTable.TryGetValue(userName, out var freq) ? 1000 / freq : 1000 / 100, cancellationToken);

View File

@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using Tapper;
using server.Services;
namespace server.Hubs;
[Hub]
public interface IProgressHub
{
Task<bool> Join(string taskId);
}
[Receiver]
public interface IProgressReceiver
{
Task OnReceiveProgress(ProgressInfo message);
}
[TranspilationSource]
public enum ProgressStatus
{
Pending,
InProgress,
Completed,
Canceled,
Failed
}
[TranspilationSource]
public class ProgressInfo
{
public string TaskId { get; }
public ProgressStatus Status { get; }
public int ProgressPercent { get; }
public string ErrorMessage { get; }
};
[Authorize]
[EnableCors("SignalR")]
public class ProgressHub : Hub<IProgressReceiver>, IProgressHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
private readonly ProgressTrackerService _tracker;
public ProgressHub(IHubContext<ProgressHub, IProgressReceiver> hubContext, ProgressTrackerService tracker)
{
_hubContext = hubContext;
_tracker = tracker;
}
public async Task<bool> Join(string taskId)
{
return _tracker.BindTask(taskId, Context.ConnectionId);
}
}

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using DotNext; using DotNext;
using Peripherals.PowerClient; using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.CameraClient; namespace Peripherals.CameraClient;
@@ -19,7 +20,7 @@ class Camera
{ {
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000; readonly int timeout = 500;
readonly int taskID; readonly int taskID;
readonly int port; readonly int port;
readonly string address; readonly string address;
@@ -43,7 +44,7 @@ class Camera
/// <param name="address">摄像头设备IP地址</param> /// <param name="address">摄像头设备IP地址</param>
/// <param name="port">摄像头设备端口</param> /// <param name="port">摄像头设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
public Camera(string address, int port, int timeout = 2000) public Camera(string address, int port, 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));
@@ -225,6 +226,7 @@ class Camera
this.taskID, // taskID this.taskID, // taskID
FrameAddr, FrameAddr,
(int)_currentFrameLength, // 使用当前分辨率的动态大小 (int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.ExtendBurst,
this.timeout); this.timeout);
if (!result.IsSuccessful) if (!result.IsSuccessful)
@@ -462,6 +464,20 @@ class Camera
); );
} }
/// <summary>
/// 配置为960x540分辨率
/// </summary>
/// <returns>配置结果</returns>
public async ValueTask<Result<bool>> ConfigureResolution960x540()
{
return await ConfigureResolution(
hStart: 0, vStart: 0,
dvpHo: 960, dvpVo: 540,
hts: 1700, vts: 1500,
hOffset: 16, vOffset: 4
);
}
/// <summary> /// <summary>
/// 配置为320x240分辨率 /// 配置为320x240分辨率
/// </summary> /// </summary>
@@ -543,6 +559,9 @@ class Camera
case "640x480": case "640x480":
result = await ConfigureResolution640x480(); result = await ConfigureResolution640x480();
break; break;
case "960x540":
result = await ConfigureResolution960x540();
break;
case "1280x720": case "1280x720":
result = await ConfigureResolution1280x720(); result = await ConfigureResolution1280x720();
break; break;

View File

@@ -0,0 +1,120 @@
using System.Net;
using DotNext;
using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.HdmiInClient;
static class HdmiInAddr
{
public const UInt32 BASE = 0xA000_0000;
public const UInt32 HdmiIn_CTRL = BASE + 0x0; //[0]: rstn, 0 is reset.
public const UInt32 HdmiIn_READFIFO = BASE + 0x1;
}
class HdmiIn
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
// 动态分辨率参数
private UInt16 _currentWidth = 960;
private UInt16 _currentHeight = 540;
private UInt32 _currentFrameLength = 960 * 540 * 2 / 4; // RGB565格式2字节/像素按4字节对齐
/// <summary>
/// 初始化HDMI输入客户端
/// </summary>
/// <param name="address">HDMI输入设备IP地址</param>
/// <param name="port">HDMI输入设备端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="timeout">超时时间(毫秒)</param>
public HdmiIn(string address, int port, int taskID, int timeout = 500)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.taskID = taskID;
this.timeout = timeout;
}
public async ValueTask<Result<bool>> EnableTrans(bool isEnable)
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.HdmiIn_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
if (!ret.IsSuccessful)
{
logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"HdmiIn_CTRL write returned false for HdmiIn at {this.address}:{this.port}");
return false;
}
return true;
}
/// <summary>
/// 读取一帧图像数据
/// </summary>
/// <returns>包含图像数据的字节数组</returns>
public async ValueTask<Result<byte[]>> ReadFrame()
{
// 只在第一次或出错时清除UDP缓冲区避免每帧都清除造成延迟
// MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Reading frame from HdmiIn {this.address}");
// 使用UDPClientPool读取图像帧数据
var result = await UDPClientPool.ReadAddr4BytesAsync(
this.ep,
this.taskID, // taskID
HdmiInAddr.HdmiIn_READFIFO,
(int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.FixedBurst,
this.timeout);
if (!result.IsSuccessful)
{
logger.Error($"Failed to read frame from HdmiIn {this.address}:{this.port}, error: {result.Error}");
// 读取失败时清除缓冲区,为下次读取做准备
try
{
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
}
catch (Exception ex)
{
logger.Warn($"Failed to clear UDP data after read error: {ex.Message}");
}
return new(result.Error);
}
logger.Trace($"Successfully read frame from HdmiIn {this.address}:{this.port}, data length: {result.Value.Length} bytes");
return result.Value;
}
/// <summary>
/// 获取当前分辨率
/// </summary>
/// <returns>当前分辨率(宽度, 高度)</returns>
public (int Width, int Height) GetCurrentResolution()
{
return (_currentWidth, _currentHeight);
}
/// <summary>
/// 获取当前帧长度
/// </summary>
/// <returns>当前帧长度</returns>
public UInt32 GetCurrentFrameLength()
{
return _currentFrameLength;
}
}

View File

@@ -2,7 +2,7 @@ using System.Collections;
using System.Net; using System.Net;
using DotNext; using DotNext;
using Newtonsoft.Json; using Newtonsoft.Json;
using server; using server.Services;
using WebProtocol; using WebProtocol;
namespace Peripherals.JtagClient; namespace Peripherals.JtagClient;
@@ -442,11 +442,12 @@ public class Jtag
return Convert.ToUInt32(Common.Number.BytesToUInt32(retPackOpts.Data).Value); return Convert.ToUInt32(Common.Number.BytesToUInt32(retPackOpts.Data).Value);
} }
async ValueTask<Result<bool>> WriteFIFO async ValueTask<Result<bool>> WriteFIFO(
(UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0) UInt32 devAddr, UInt32 data, UInt32 result,
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, ProgressReporter? progress = null)
{ {
{ {
var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progress?.CreateChild(80));
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed")); if (!ret.Value) return new(new Exception("Write FIFO failed"));
} }
@@ -457,15 +458,17 @@ public class Jtag
{ {
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout); var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
progress?.Finish();
return ret.Value; return ret.Value;
} }
} }
async ValueTask<Result<bool>> WriteFIFO async ValueTask<Result<bool>> WriteFIFO(
(UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0) UInt32 devAddr, byte[] data, UInt32 result,
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, ProgressReporter? progress = null)
{ {
{ {
var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progress?.CreateChild(80));
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed")); if (!ret.Value) return new(new Exception("Write FIFO failed"));
} }
@@ -476,6 +479,7 @@ public class Jtag
{ {
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout); var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
progress?.Finish();
return ret.Value; return ret.Value;
} }
} }
@@ -559,7 +563,8 @@ public class Jtag
return await ClearWriteDataReg(); return await ClearWriteDataReg();
} }
async ValueTask<Result<bool>> LoadDRCareInput(byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500) async ValueTask<Result<bool>> LoadDRCareInput(
byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, ProgressReporter? progress = null)
{ {
var bytesLen = ((uint)(bytesArray.Length * 8)); var bytesLen = ((uint)(bytesArray.Length * 8));
if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)")); if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)"));
@@ -574,11 +579,15 @@ public class Jtag
else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed")); else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed"));
} }
progress?.Report(10);
{ {
var ret = await WriteFIFO( var ret = await WriteFIFO(
JtagAddr.WRITE_DATA, JtagAddr.WRITE_DATA,
bytesArray, 0x01_00_00_00, bytesArray, 0x01_00_00_00,
JtagState.CMD_EXEC_FINISH); JtagState.CMD_EXEC_FINISH,
progress: progress?.CreateChild(90)
);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value; return ret.Value;
@@ -612,13 +621,10 @@ public class Jtag
if (ret.Value) if (ret.Value)
{ {
var array = new UInt32[UInt32Num]; var array = new UInt32[UInt32Num];
for (int i = 0; i < UInt32Num; i++) var retData = await UDPClientPool.ReadAddr4Bytes(ep, 0, JtagAddr.READ_DATA, (int)UInt32Num);
{
var retData = await ReadFIFO(JtagAddr.READ_DATA);
if (!retData.IsSuccessful) if (!retData.IsSuccessful)
return new(new Exception("Read FIFO failed when Load DR")); return new(new Exception("Read FIFO failed when Load DR"));
array[i] = retData.Value; Buffer.BlockCopy(retData.Value, 0, array, 0, (int)UInt32Num * 4);
}
return array; return array;
} }
else else
@@ -704,44 +710,55 @@ public class Jtag
/// </summary> /// </summary>
/// <param name="bitstream">比特流数据</param> /// <param name="bitstream">比特流数据</param>
/// <returns>指示下载是否成功的异步结果</returns> /// <returns>指示下载是否成功的异步结果</returns>
public async ValueTask<Result<bool>> DownloadBitstream(byte[] bitstream) public async ValueTask<Result<bool>> DownloadBitstream(byte[] bitstream, ProgressReporter? progress = null)
{ {
// Clear Data // Clear Data
MsgBus.UDPServer.ClearUDPData(this.address, 0); MsgBus.UDPServer.ClearUDPData(this.address, 0);
logger.Trace($"Clear up udp server {this.address,0} receive data"); logger.Trace($"Clear up udp server {this.address,0} receive data");
if (progress != null)
{
progress.ExpectedSteps = 25;
progress.Increase();
}
Result<bool> ret; Result<bool> ret;
ret = await CloseTest(); ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
progress?.Increase();
ret = await RunTest(); ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
progress?.Increase();
logger.Trace("Jtag initialize"); logger.Trace("Jtag initialize");
ret = await ExecRDCmd(JtagCmd.JTAG_DR_JRST); ret = await ExecRDCmd(JtagCmd.JTAG_DR_JRST);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed")); else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed"));
progress?.Increase();
ret = await RunTest(); ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
progress?.Increase();
ret = await ExecRDCmd(JtagCmd.JTAG_DR_CFGI); ret = await ExecRDCmd(JtagCmd.JTAG_DR_CFGI);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_CFGI Failed")); else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_CFGI Failed"));
progress?.Increase();
logger.Trace("Jtag ready to write bitstream"); logger.Trace("Jtag ready to write bitstream");
ret = await IdleDelay(100000); ret = await IdleDelay(100000);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed")); else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
progress?.Increase();
ret = await LoadDRCareInput(bitstream); ret = await LoadDRCareInput(bitstream, progress: progress?.CreateChild(50));
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Load Data Failed")); else if (!ret.Value) return new(new Exception("Jtag Load Data Failed"));
@@ -750,32 +767,40 @@ public class Jtag
ret = await CloseTest(); ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
progress?.Increase();
ret = await RunTest(); ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
progress?.Increase();
ret = await ExecRDCmd(JtagCmd.JTAG_DR_JWAKEUP); ret = await ExecRDCmd(JtagCmd.JTAG_DR_JWAKEUP);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JWAKEUP Failed")); else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JWAKEUP Failed"));
progress?.Increase();
logger.Trace("Jtag reset device"); logger.Trace("Jtag reset device");
ret = await IdleDelay(10000); ret = await IdleDelay(10000);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed")); else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
progress?.Increase();
var retCode = await ReadStatusReg(); var retCode = await ReadStatusReg();
if (!retCode.IsSuccessful) return new(retCode.Error); if (!retCode.IsSuccessful) return new(retCode.Error);
var jtagStatus = new JtagStatusReg(retCode.Value); var jtagStatus = new JtagStatusReg(retCode.Value);
if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete)) if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete))
return new(new Exception("Jtag download bitstream failed")); return new(new Exception("Jtag download bitstream failed"));
progress?.Increase();
ret = await CloseTest(); ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
logger.Trace("Jtag download bitstream successfully"); logger.Trace("Jtag download bitstream successfully");
progress?.Increase();
// Finish
progress?.Finish();
return true; return true;
} }
@@ -788,7 +813,7 @@ public class Jtag
{ {
var paser = new BsdlParser.Parser(); var paser = new BsdlParser.Parser();
var portNum = paser.GetBoundaryRegsNum().Value; var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}"); logger.Debug($"Get boundary scan registers number: {portNum}");
// Clear Data // Clear Data
MsgBus.UDPServer.ClearUDPData(this.address, 0); MsgBus.UDPServer.ClearUDPData(this.address, 0);

View File

@@ -2,6 +2,7 @@ using System.Collections;
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.LogicAnalyzerClient; namespace Peripherals.LogicAnalyzerClient;
@@ -66,10 +67,11 @@ static class AnalyzerAddr
public const UInt32 LOAD_NUM_ADDR = BASE + 0x0000_0002; public const UInt32 LOAD_NUM_ADDR = BASE + 0x0000_0002;
public const UInt32 PRE_LOAD_NUM_ADDR = BASE + 0x0000_0003; public const UInt32 PRE_LOAD_NUM_ADDR = BASE + 0x0000_0003;
public const UInt32 CAHNNEL_DIV_ADDR = BASE + 0x0000_0004; public const UInt32 CAHNNEL_DIV_ADDR = BASE + 0x0000_0004;
public const UInt32 CLOCK_DIV_ADDR = BASE + 0x0000_0005;
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/>
@@ -137,6 +139,52 @@ public enum GlobalCaptureMode
NOR = 0b11 NOR = 0b11
} }
/// <summary>
/// 逻辑分析仪采样时钟分频系数
/// </summary>
public enum AnalyzerClockDiv
{
/// <summary>
/// 1分频
/// </summary>
DIV1 = 0x0000_0000,
/// <summary>
/// 2分频
/// </summary>
DIV2 = 0x0000_0001,
/// <summary>
/// 4分频
/// </summary>
DIV4 = 0x0000_0002,
/// <summary>
/// 8分频
/// </summary>
DIV8 = 0x0000_0003,
/// <summary>
/// 16分频
/// </summary>
DIV16 = 0x0000_0004,
/// <summary>
/// 32分频
/// </summary>
DIV32 = 0x0000_0005,
/// <summary>
/// 64分频
/// </summary>
DIV64 = 0x0000_0006,
/// <summary>
/// 128分频
/// </summary>
DIV128 = 0x0000_0007
}
/// <summary> /// <summary>
/// 信号M的操作符枚举 /// 信号M的操作符枚举
/// </summary> /// </summary>
@@ -386,13 +434,14 @@ public class Analyzer
} }
/// <summary> /// <summary>
/// 设置逻辑分析仪的深度、预采样深度、有效通道 /// 设置逻辑分析仪的深度、预采样深度、有效通道、分频系数
/// </summary> /// </summary>
/// <param name="capture_length">深度</param> /// <param name="capture_length">深度</param>
/// <param name="pre_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> /// <param name="channel_div">有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])</param>
/// <param name="clock_div">采样时钟分频系数</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns> /// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div) public async ValueTask<Result<bool>> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div, AnalyzerClockDiv clock_div)
{ {
if (capture_length == 0) capture_length = 1; if (capture_length == 0) capture_length = 1;
if (pre_capture_length == 0) pre_capture_length = 1; if (pre_capture_length == 0) pre_capture_length = 1;
@@ -461,6 +510,19 @@ public class Analyzer
return new(new Exception("Failed to set CAHNNEL_DIV_ADDR")); return new(new Exception("Failed to set CAHNNEL_DIV_ADDR"));
} }
} }
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CLOCK_DIV_ADDR, (UInt32)clock_div, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set CLOCK_DIV_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to CLOCK_DIV_ADDR returned false");
return new(new Exception("Failed to set CLOCK_DIV_ADDR"));
}
}
return true; return true;
} }
@@ -475,6 +537,7 @@ public class Analyzer
this.taskID, this.taskID,
AnalyzerAddr.STORE_OFFSET_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR,
capture_length, capture_length,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.OscilloscopeClient; namespace Peripherals.OscilloscopeClient;
@@ -319,6 +320,7 @@ class Oscilloscope
this.taskID, this.taskID,
OscilloscopeAddr.RD_DATA_ADDR, OscilloscopeAddr.RD_DATA_ADDR,
(int)OscilloscopeAddr.RD_DATA_LENGTH / 32, (int)OscilloscopeAddr.RD_DATA_LENGTH / 32,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -0,0 +1,498 @@
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 readonly ConcurrentDictionary<string, CancellationTokenSource> _hdmiInCtsDict = new();
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
{
logger.Debug("Waiting for HTTP request...");
context = await _httpListener.GetContextAsync();
logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}");
}
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...");
// 禁用所有活跃的HDMI传输
var disableTasks = new List<Task>();
foreach (var hdmiKey in _hdmiInDict.Keys)
{
disableTasks.Add(DisableHdmiTransmissionAsync(hdmiKey));
}
// 等待所有禁用操作完成
await Task.WhenAll(disableTasks);
// 清空字典
_hdmiInDict.Clear();
_hdmiInCtsDict.Clear();
_httpListener?.Close(); // 立即关闭监听器,唤醒阻塞
await base.StopAsync(cancellationToken);
}
public async Task DisableHdmiTransmissionAsync(string key)
{
try
{
var cts = _hdmiInCtsDict[key];
cts.Cancel();
var hdmiIn = _hdmiInDict[key];
var disableResult = await hdmiIn.EnableTrans(false);
if (disableResult.IsSuccessful)
{
logger.Info("Successfully disabled HDMI transmission");
}
else
{
logger.Error($"Failed to disable HDMI transmission: {disableResult.Error}");
}
}
catch (Exception ex)
{
logger.Error(ex, "Exception occurred while disabling HDMI transmission");
}
}
// 获取/创建 HdmiIn 实例
private async Task<HdmiIn?> GetOrCreateHdmiInAsync(string boardId)
{
if (_hdmiInDict.TryGetValue(boardId, out var hdmiIn))
{
try
{
var enableResult = await hdmiIn.EnableTrans(true);
if (!enableResult.IsSuccessful)
{
logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}");
return null;
}
logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
}
catch (Exception ex)
{
logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}");
return null;
}
_hdmiInDict[boardId] = hdmiIn;
_hdmiInCtsDict[boardId] = new CancellationTokenSource();
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 可根据实际需求调整
// 启用HDMI传输
try
{
var enableResult = await hdmiIn.EnableTrans(true);
if (!enableResult.IsSuccessful)
{
logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}");
return null;
}
logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
}
catch (Exception ex)
{
logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}");
return null;
}
_hdmiInDict[boardId] = hdmiIn;
_hdmiInCtsDict[boardId] = new CancellationTokenSource();
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 = await GetOrCreateHdmiInAsync(boardId);
if (hdmiIn == null)
{
await SendErrorAsync(context.Response, "Invalid boardId or board not available");
return;
}
var hdmiInToken = _hdmiInCtsDict[boardId].Token;
if (hdmiInToken == null)
{
await SendErrorAsync(context.Response, "HDMI input is not available");
return;
}
if (path == "/snapshot")
{
await HandleSnapshotRequestAsync(context.Response, hdmiIn, hdmiInToken);
}
else if (path == "/mjpeg")
{
await HandleMjpegStreamAsync(context.Response, hdmiIn, hdmiInToken);
}
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)
{
try
{
logger.Debug("处理HDMI快照请求");
const int frameWidth = 960; // HDMI输入分辨率
const int frameHeight = 540;
// 从HDMI读取RGB565数据
var frameResult = await hdmiIn.ReadFrame();
if (!frameResult.IsSuccessful || frameResult.Value == null)
{
logger.Error("HDMI快照获取失败");
response.StatusCode = 500;
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI snapshot");
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
response.Close();
return;
}
var rgb565Data = frameResult.Value;
// 验证数据长度
var expectedLength = frameWidth * frameHeight * 2;
if (rgb565Data.Length != expectedLength)
{
logger.Warn("HDMI快照数据长度不匹配期望: {Expected}, 实际: {Actual}",
expectedLength, rgb565Data.Length);
}
// 将RGB565转换为RGB24
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, frameWidth, frameHeight, isLittleEndian: false);
if (!rgb24Result.IsSuccessful)
{
logger.Error("HDMI快照RGB565转RGB24失败: {Error}", rgb24Result.Error);
response.StatusCode = 500;
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to process HDMI snapshot");
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
response.Close();
return;
}
// 将RGB24转换为JPEG
var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Result.Value, frameWidth, frameHeight, 80);
if (!jpegResult.IsSuccessful)
{
logger.Error("HDMI快照RGB24转JPEG失败: {Error}", jpegResult.Error);
response.StatusCode = 500;
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to encode HDMI snapshot");
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
response.Close();
return;
}
var jpegData = jpegResult.Value;
// 设置响应头参考Camera版本
response.ContentType = "image/jpeg";
response.ContentLength64 = jpegData.Length;
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
await response.OutputStream.FlushAsync(cancellationToken);
logger.Debug("已发送HDMI快照图像大小{Size} 字节", jpegData.Length);
}
catch (Exception ex)
{
logger.Error(ex, "处理HDMI快照请求时出错");
response.StatusCode = 500;
}
finally
{
response.Close();
}
}
private async Task HandleMjpegStreamAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken)
{
try
{
// 设置MJPEG流的响应头参考Camera版本
response.ContentType = "multipart/x-mixed-replace; boundary=--boundary";
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
response.Headers.Add("Pragma", "no-cache");
response.Headers.Add("Expires", "0");
logger.Debug("开始HDMI MJPEG流传输");
int frameCounter = 0;
const int frameWidth = 960; // HDMI输入分辨率
const int frameHeight = 540;
while (!cancellationToken.IsCancellationRequested)
{
try
{
var frameStartTime = DateTime.UtcNow;
// 从HDMI读取RGB565数据
var readStartTime = DateTime.UtcNow;
var frameResult = await hdmiIn.ReadFrame();
var readEndTime = DateTime.UtcNow;
var readTime = (readEndTime - readStartTime).TotalMilliseconds;
if (!frameResult.IsSuccessful || frameResult.Value == null)
{
logger.Warn("HDMI帧读取失败或为空");
continue;
}
var rgb565Data = frameResult.Value;
// 验证数据长度是否正确 (RGB565为每像素2字节)
var expectedLength = frameWidth * frameHeight * 2;
if (rgb565Data.Length != expectedLength)
{
logger.Warn("HDMI数据长度不匹配期望: {Expected}, 实际: {Actual}",
expectedLength, rgb565Data.Length);
}
// 将RGB565转换为RGB24参考Camera版本的处理
var convertStartTime = DateTime.UtcNow;
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, frameWidth, frameHeight, isLittleEndian: false);
var convertEndTime = DateTime.UtcNow;
var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
if (!rgb24Result.IsSuccessful)
{
logger.Error("HDMI RGB565转RGB24失败: {Error}", rgb24Result.Error);
continue;
}
// 将RGB24转换为JPEG参考Camera版本的处理
var jpegStartTime = DateTime.UtcNow;
var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Result.Value, frameWidth, frameHeight, 80);
var jpegEndTime = DateTime.UtcNow;
var jpegTime = (jpegEndTime - jpegStartTime).TotalMilliseconds;
if (!jpegResult.IsSuccessful)
{
logger.Error("HDMI RGB24转JPEG失败: {Error}", jpegResult.Error);
continue;
}
var jpegData = jpegResult.Value;
// 发送MJPEG帧使用Camera版本的格式
var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
await response.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken);
await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
await response.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken);
await response.OutputStream.FlushAsync(cancellationToken);
frameCounter++;
var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds;
// 性能统计日志每30帧记录一次
if (frameCounter % 30 == 0)
{
logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 读取: {ReadTime:F1}ms, RGB转换: {ConvertTime:F1}ms, JPEG转换: {JpegTime:F1}ms, 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节",
frameCounter, readTime, convertTime, jpegTime, totalTime, jpegData.Length);
}
}
catch (Exception ex)
{
logger.Error(ex, "处理HDMI帧时发生错误");
}
}
}
catch (Exception ex)
{
logger.Error(ex, "HDMI MJPEG流处理异常");
}
finally
{
try
{
// 停止传输时禁用HDMI传输
await hdmiIn.EnableTrans(false);
logger.Info("已禁用HDMI传输");
}
catch (Exception ex)
{
logger.Error(ex, "禁用HDMI传输时出错");
}
try
{
response.Close();
}
catch
{
// 忽略关闭时的错误
}
logger.Debug("HDMI MJPEG流连接已关闭");
}
}
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();
}
/// <summary>
/// 获取所有可用的HDMI视频流终端点
/// </summary>
/// <returns>返回所有可用的HDMI视频流终端点列表</returns>
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;
}
/// <summary>
/// 获取指定板卡ID的HDMI视频流终端点
/// </summary>
/// <param name="boardId">板卡ID</param>
/// <returns>返回指定板卡的HDMI视频流终端点</returns>
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}"
};
}
}

View File

@@ -1109,6 +1109,7 @@ public class HttpVideoStreamService : BackgroundService
return new List<(int, int, string)> return new List<(int, int, string)>
{ {
(640, 480, "640x480 (VGA)"), (640, 480, "640x480 (VGA)"),
(960, 540, "960x540 (qHD)"),
(1280, 720, "1280x720 (HD)"), (1280, 720, "1280x720 (HD)"),
(1280, 960, "1280x960 (SXGA)"), (1280, 960, "1280x960 (SXGA)"),
(1920, 1080, "1920x1080 (Full HD)") (1920, 1080, "1920x1080 (Full HD)")

View File

@@ -0,0 +1,288 @@
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
using DotNext;
using Common;
using server.Hubs;
namespace server.Services;
public class ProgressReporter : ProgressInfo, IProgress<int>
{
private int _progress = 0;
private int _stepProgress = 1;
private int _expectedSteps = 100;
private int _parentProportion = 100;
public int Progress => _progress;
public int MaxProgress { get; set; } = 100;
public int StepProgress
{
get => _stepProgress;
set
{
_stepProgress = value;
ExpectedSteps = MaxProgress / value;
}
}
public int ExpectedSteps
{
get => _expectedSteps;
set
{
_expectedSteps = value;
MaxProgress = Number.IntPow(10, Number.GetLength(value));
StepProgress = MaxProgress / value;
}
}
public Func<int, Task>? ReporterFunc { get; set; } = null;
public ProgressReporter? Parent { get; set; }
public ProgressReporter? Child { get; set; }
private ProgressStatus _status = ProgressStatus.Pending;
private string _errorMessage;
public string TaskId { get; set; } = new Guid().ToString();
public int ProgressPercent => _progress * 100 / MaxProgress;
public ProgressStatus Status => _status;
public string ErrorMessage => _errorMessage;
public ProgressReporter(Func<int, Task>? reporter = null, int initProgress = 0, int maxProgress = 100, int step = 1)
{
_progress = initProgress;
MaxProgress = maxProgress;
StepProgress = step;
ReporterFunc = reporter;
}
public ProgressReporter(int parentProportion, int expectedSteps = 100, Func<int, Task>? reporter = null)
{
this._parentProportion = parentProportion;
MaxProgress = Number.IntPow(10, Number.GetLength(expectedSteps));
StepProgress = MaxProgress / expectedSteps;
ReporterFunc = reporter;
}
private async void ForceReport(int value)
{
try
{
if (ReporterFunc != null)
await ReporterFunc(value);
if (Parent != null)
Parent.Increase((value - _progress) / StepProgress * _parentProportion / (MaxProgress / StepProgress));
_progress = value;
}
catch (OperationCanceledException ex)
{
_errorMessage = ex.Message;
this._status = ProgressStatus.Canceled;
}
catch (Exception ex)
{
_errorMessage = ex.Message;
this._status = ProgressStatus.Failed;
}
}
public async void Report(int value)
{
if (this._status == ProgressStatus.Pending)
this._status = ProgressStatus.InProgress;
else if (this.Status != ProgressStatus.InProgress)
return;
if (value > MaxProgress) return;
ForceReport(value);
}
public void Increase(int? value = null)
{
if (this._status == ProgressStatus.Pending)
this._status = ProgressStatus.InProgress;
else if (this.Status != ProgressStatus.InProgress)
return;
if (value.HasValue)
{
if (_progress + value.Value >= MaxProgress) return;
this.Report(_progress + value.Value);
}
else
{
if (_progress + StepProgress >= MaxProgress) return;
this.Report(_progress + StepProgress);
}
}
public void Finish()
{
this._status = ProgressStatus.Completed;
this.ForceReport(MaxProgress);
}
public void Cancel()
{
this._status = ProgressStatus.Canceled;
this._errorMessage = "User Cancelled";
this.ForceReport(_progress);
}
public void Error(string message)
{
this._status = ProgressStatus.Failed;
this._errorMessage = message;
this.ForceReport(_progress);
}
public ProgressReporter CreateChild(int proportion, int expectedSteps = 100)
{
var child = new ProgressReporter(proportion, expectedSteps);
child.Parent = this;
this.Child = child;
return child;
}
}
public class ProgressTrackerService : BackgroundService
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ConcurrentDictionary<string, TaskProgressInfo> _taskMap = new();
private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
private class TaskProgressInfo
{
public ProgressReporter Reporter { get; set; }
public string? ConnectionId { get; set; }
public required CancellationToken CancellationToken { get; set; }
public required CancellationTokenSource CancellationTokenSource { get; set; }
public required DateTime UpdatedAt { get; set; }
}
public ProgressTrackerService(IHubContext<ProgressHub, IProgressReceiver> hubContext)
{
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var now = DateTime.UtcNow;
foreach (var kvp in _taskMap)
{
var info = kvp.Value;
// 超过 1 分钟且任务已完成/失败/取消
if ((now - info.UpdatedAt).TotalMinutes > 1 &&
(info.Reporter.Status == ProgressStatus.Completed ||
info.Reporter.Status == ProgressStatus.Failed ||
info.Reporter.Status == ProgressStatus.Canceled))
{
_taskMap.TryRemove(kvp.Key, out _);
logger.Info($"Cleaned up task {kvp.Key}");
}
}
}
catch (Exception ex)
{
logger.Error(ex, "Error during ProgressTracker cleanup");
}
await Task.Delay(TimeSpan.FromSeconds(30));
}
}
public (string, ProgressReporter) CreateTask(CancellationToken? cancellationToken = null)
{
CancellationTokenSource? cancellationTokenSource;
if (cancellationToken.HasValue)
{
cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value);
}
else
{
cancellationTokenSource = new CancellationTokenSource();
}
var progressInfo = new TaskProgressInfo
{
ConnectionId = null,
UpdatedAt = DateTime.UtcNow,
CancellationToken = cancellationTokenSource.Token,
CancellationTokenSource = cancellationTokenSource,
};
var progress = new ProgressReporter(async value =>
{
cancellationTokenSource.Token.ThrowIfCancellationRequested();
// 通过 SignalR 推送进度
if (progressInfo.ConnectionId != null)
await _hubContext.Clients.Client(progressInfo.ConnectionId).OnReceiveProgress(progressInfo.Reporter);
});
progressInfo.Reporter = progress;
_taskMap.TryAdd(progressInfo.Reporter.TaskId, progressInfo);
return (progressInfo.Reporter.TaskId, progress);
}
public Optional<ProgressReporter> GetReporter(string taskId)
{
if (_taskMap.TryGetValue(taskId, out var info))
{
return info.Reporter;
}
return Optional<ProgressReporter>.None;
}
public Optional<ProgressStatus> GetProgressStatus(string taskId)
{
if (_taskMap.TryGetValue(taskId, out var info))
{
return info.Reporter.Status;
}
return Optional<ProgressStatus>.None;
}
public bool BindTask(string taskId, string connectionId)
{
if (_taskMap.TryGetValue(taskId, out var info) && info != null)
{
lock (info)
{
info.ConnectionId = connectionId;
}
return true;
}
return false;
}
public bool CancelTask(string taskId)
{
try
{
if (_taskMap.TryGetValue(taskId, out var info) && info != null)
{
lock (info)
{
info.CancellationTokenSource.Cancel();
info.Reporter.Cancel();
info.UpdatedAt = DateTime.UtcNow;
}
return true;
}
return false;
}
catch (Exception ex)
{
logger.Error(ex, $"Failed to cancel task {taskId}");
return false;
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Net.Sockets;
using System.Text; using System.Text;
using DotNext; using DotNext;
using WebProtocol; using WebProtocol;
using server.Services;
/// <summary> /// <summary>
/// UDP客户端发送池 /// UDP客户端发送池
@@ -433,11 +434,12 @@ public class UDPClientPool
/// <param name="endPoint">IP端点IP地址与端口</param> /// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param> /// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param> /// <param name="devAddr">设备地址</param>
/// <param name="burstType">突发类型</param>
/// <param name="dataLength">要读取的数据长度4字节</param> /// <param name="dataLength">要读取的数据长度4字节</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的字节数组</returns> /// <returns>读取结果,包含接收到的字节数组</returns>
public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync( public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync(
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000) IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, BurstType burstType, int timeout = 1000)
{ {
var pkgList = new List<SendAddrPackage>(); var pkgList = new List<SendAddrPackage>();
var resultData = new List<byte>(); var resultData = new List<byte>();
@@ -460,11 +462,12 @@ public class UDPClientPool
var opts = new SendAddrPackOptions var opts = new SendAddrPackOptions
{ {
BurstType = BurstType.FixedBurst, BurstType = burstType,
CommandID = Convert.ToByte(taskID), CommandID = Convert.ToByte(taskID),
IsWrite = false, IsWrite = false,
BurstLength = (byte)(currentSegmentSize - 1), BurstLength = (byte)(currentSegmentSize - 1),
Address = devAddr + (uint)(i * max4BytesPerRead) Address = (burstType == BurstType.ExtendBurst) ? (devAddr + (uint)(i * max4BytesPerRead)) : (devAddr),
// Address = devAddr + (uint)(i * max4BytesPerRead),
}; };
pkgList.Add(new SendAddrPackage(opts)); pkgList.Add(new SendAddrPackage(opts));
} }
@@ -584,7 +587,8 @@ public class UDPClientPool
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
/// <returns>写入结果true表示写入成功</returns> /// <returns>写入结果true表示写入成功</returns>
public static async ValueTask<Result<bool>> WriteAddr( public static async ValueTask<Result<bool>> WriteAddr(
IPEndPoint endPoint, int taskID, UInt32 devAddr, UInt32 data, int timeout = 1000) IPEndPoint endPoint, int taskID, UInt32 devAddr,
UInt32 data, int timeout = 1000, ProgressReporter? progress = null)
{ {
var ret = false; var ret = false;
var opts = new SendAddrPackOptions() var opts = new SendAddrPackOptions()
@@ -595,14 +599,17 @@ public class UDPClientPool
Address = devAddr, Address = devAddr,
IsWrite = true, IsWrite = true,
}; };
progress?.Report(20);
// Write Register // Write Register
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts)); ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!")); if (!ret) return new(new Exception("Send 1st address package failed!"));
progress?.Report(40);
// Send Data Package // Send Data Package
ret = await UDPClientPool.SendDataPackAsync(endPoint, ret = await UDPClientPool.SendDataPackAsync(endPoint,
new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value)); new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value));
if (!ret) return new(new Exception("Send data package failed!")); if (!ret) return new(new Exception("Send data package failed!"));
progress?.Report(60);
// Check Msg Bus // Check Msg Bus
if (!MsgBus.IsRunning) if (!MsgBus.IsRunning)
@@ -612,6 +619,7 @@ public class UDPClientPool
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync( var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(
endPoint.Address.ToString(), taskID, endPoint.Port, timeout); endPoint.Address.ToString(), taskID, endPoint.Port, timeout);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error); if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
progress?.Finish();
return udpWriteAck.Value.IsSuccessful; return udpWriteAck.Value.IsSuccessful;
} }
@@ -626,7 +634,8 @@ public class UDPClientPool
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
/// <returns>写入结果true表示写入成功</returns> /// <returns>写入结果true表示写入成功</returns>
public static async ValueTask<Result<bool>> WriteAddr( public static async ValueTask<Result<bool>> WriteAddr(
IPEndPoint endPoint, int taskID, UInt32 devAddr, byte[] dataArray, int timeout = 1000) IPEndPoint endPoint, int taskID, UInt32 devAddr,
byte[] dataArray, int timeout = 1000, ProgressReporter? progress = null)
{ {
var ret = false; var ret = false;
var opts = new SendAddrPackOptions() var opts = new SendAddrPackOptions()
@@ -648,6 +657,8 @@ public class UDPClientPool
var writeTimes = hasRest ? var writeTimes = hasRest ?
dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 : dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 :
dataArray.Length / (max4BytesPerRead * (32 / 8)); dataArray.Length / (max4BytesPerRead * (32 / 8));
if (progress != null)
progress.ExpectedSteps = writeTimes;
for (var i = 0; i < writeTimes; i++) for (var i = 0; i < writeTimes; i++)
{ {
// Sperate Data Array // Sperate Data Array
@@ -676,8 +687,11 @@ public class UDPClientPool
if (!udpWriteAck.Value.IsSuccessful) if (!udpWriteAck.Value.IsSuccessful)
return false; return false;
progress?.Increase();
} }
progress?.Finish();
return true; return true;
} }

View File

@@ -2944,6 +2944,176 @@ 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);
}
disableHdmiTransmission( cancelToken?: CancelToken): Promise<FileResponse | null> {
let url_ = this.baseUrl + "/api/HdmiVideoStream/DisableHdmiTransmission";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
responseType: "blob",
method: "POST",
url: url_,
headers: {
"Accept": "application/octet-stream"
},
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.processDisableHdmiTransmission(_response);
});
}
protected processDisableHdmiTransmission(response: AxiosResponse): Promise<FileResponse | null> {
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 || status === 206) {
const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined;
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
if (fileName) {
fileName = decodeURIComponent(fileName);
} else {
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
}
return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers });
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<FileResponse | null>(null as any);
}
}
export class JtagClient { export class JtagClient {
protected instance: AxiosInstance; protected instance: AxiosInstance;
protected baseUrl: string; protected baseUrl: string;
@@ -3161,9 +3331,9 @@ export class JtagClient {
* @param address (optional) JTAG 设备地址 * @param address (optional) JTAG 设备地址
* @param port (optional) JTAG 设备端口 * @param port (optional) JTAG 设备端口
* @param bitstreamId (optional) 比特流ID * @param bitstreamId (optional) 比特流ID
* @return 下载结果 * @return 进度跟踪TaskID
*/ */
downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise<boolean> { downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise<string> {
let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?";
if (address === null) if (address === null)
throw new Error("The parameter 'address' cannot be null."); throw new Error("The parameter 'address' cannot be null.");
@@ -3199,7 +3369,7 @@ export class JtagClient {
}); });
} }
protected processDownloadBitstream(response: AxiosResponse): Promise<boolean> { protected processDownloadBitstream(response: AxiosResponse): Promise<string> {
const status = response.status; const status = response.status;
let _headers: any = {}; let _headers: any = {};
if (response.headers && typeof response.headers === "object") { if (response.headers && typeof response.headers === "object") {
@@ -3215,7 +3385,7 @@ export class JtagClient {
let resultData200 = _responseText; let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null; result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200); return Promise.resolve<string>(result200);
} else if (status === 400) { } else if (status === 400) {
const _responseText = response.data; const _responseText = response.data;
@@ -3243,7 +3413,7 @@ export class JtagClient {
const _responseText = response.data; const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers); return throwException("An unexpected server error occurred.", status, _responseText, _headers);
} }
return Promise.resolve<boolean>(null as any); return Promise.resolve<string>(null as any);
} }
/** /**
@@ -3831,9 +4001,10 @@ export class LogicAnalyzerClient {
* @param capture_length (optional) 深度 * @param capture_length (optional) 深度
* @param pre_capture_length (optional) 预采样深度 * @param pre_capture_length (optional) 预采样深度
* @param channel_div (optional) 有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32]) * @param channel_div (optional) 有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])
* @param clock_div (optional) 采样时钟分频系数
* @return 操作结果 * @return 操作结果
*/ */
setCaptureParams(capture_length: number | undefined, pre_capture_length: number | undefined, channel_div: AnalyzerChannelDiv | undefined, cancelToken?: CancelToken): Promise<boolean> { setCaptureParams(capture_length: number | undefined, pre_capture_length: number | undefined, channel_div: AnalyzerChannelDiv | undefined, clock_div: AnalyzerClockDiv | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/LogicAnalyzer/SetCaptureParams?"; let url_ = this.baseUrl + "/api/LogicAnalyzer/SetCaptureParams?";
if (capture_length === null) if (capture_length === null)
throw new Error("The parameter 'capture_length' cannot be null."); throw new Error("The parameter 'capture_length' cannot be null.");
@@ -3847,6 +4018,10 @@ export class LogicAnalyzerClient {
throw new Error("The parameter 'channel_div' cannot be null."); throw new Error("The parameter 'channel_div' cannot be null.");
else if (channel_div !== undefined) else if (channel_div !== undefined)
url_ += "channel_div=" + encodeURIComponent("" + channel_div) + "&"; url_ += "channel_div=" + encodeURIComponent("" + channel_div) + "&";
if (clock_div === null)
throw new Error("The parameter 'clock_div' cannot be null.");
else if (clock_div !== undefined)
url_ += "clock_div=" + encodeURIComponent("" + clock_div) + "&";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = { let options_: AxiosRequestConfig = {
@@ -8019,6 +8194,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,
@@ -8068,6 +8291,18 @@ export enum AnalyzerChannelDiv {
XXXII = 5, XXXII = 5,
} }
/** 逻辑分析仪采样时钟分频系数 */
export enum AnalyzerClockDiv {
DIV1 = 0,
DIV2 = 1,
DIV4 = 2,
DIV8 = 3,
DIV16 = 4,
DIV32 = 5,
DIV64 = 6,
DIV128 = 7,
}
/** 捕获配置 */ /** 捕获配置 */
export class CaptureConfig implements ICaptureConfig { export class CaptureConfig implements ICaptureConfig {
/** 全局触发模式 */ /** 全局触发模式 */
@@ -8078,6 +8313,8 @@ export class CaptureConfig implements ICaptureConfig {
preCaptureLength!: number; preCaptureLength!: number;
/** 有效通道 */ /** 有效通道 */
channelDiv!: AnalyzerChannelDiv; channelDiv!: AnalyzerChannelDiv;
/** 时钟分频系数 */
clockDiv!: AnalyzerClockDiv;
/** 信号触发配置列表 */ /** 信号触发配置列表 */
signalConfigs!: SignalTriggerConfig[]; signalConfigs!: SignalTriggerConfig[];
@@ -8099,6 +8336,7 @@ export class CaptureConfig implements ICaptureConfig {
this.captureLength = _data["captureLength"]; this.captureLength = _data["captureLength"];
this.preCaptureLength = _data["preCaptureLength"]; this.preCaptureLength = _data["preCaptureLength"];
this.channelDiv = _data["channelDiv"]; this.channelDiv = _data["channelDiv"];
this.clockDiv = _data["clockDiv"];
if (Array.isArray(_data["signalConfigs"])) { if (Array.isArray(_data["signalConfigs"])) {
this.signalConfigs = [] as any; this.signalConfigs = [] as any;
for (let item of _data["signalConfigs"]) for (let item of _data["signalConfigs"])
@@ -8120,6 +8358,7 @@ export class CaptureConfig implements ICaptureConfig {
data["captureLength"] = this.captureLength; data["captureLength"] = this.captureLength;
data["preCaptureLength"] = this.preCaptureLength; data["preCaptureLength"] = this.preCaptureLength;
data["channelDiv"] = this.channelDiv; data["channelDiv"] = this.channelDiv;
data["clockDiv"] = this.clockDiv;
if (Array.isArray(this.signalConfigs)) { if (Array.isArray(this.signalConfigs)) {
data["signalConfigs"] = []; data["signalConfigs"] = [];
for (let item of this.signalConfigs) for (let item of this.signalConfigs)
@@ -8139,6 +8378,8 @@ export interface ICaptureConfig {
preCaptureLength: number; preCaptureLength: number;
/** 有效通道 */ /** 有效通道 */
channelDiv: AnalyzerChannelDiv; channelDiv: AnalyzerChannelDiv;
/** 时钟分频系数 */
clockDiv: AnalyzerClockDiv;
/** 信号触发配置列表 */ /** 信号触发配置列表 */
signalConfigs: SignalTriggerConfig[]; signalConfigs: SignalTriggerConfig[];
} }

View File

@@ -3,7 +3,8 @@
/* tslint:disable */ /* tslint:disable */
// @ts-nocheck // @ts-nocheck
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr'; import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
import type { IJtagHub, IJtagReceiver } from './server.Hubs.JtagHub'; import type { IJtagHub, IProgressHub, IJtagReceiver, IProgressReceiver } from './server.Hubs';
import type { ProgressInfo } from '../server.Hubs';
// components // components
@@ -43,22 +44,30 @@ class ReceiverMethodSubscription implements Disposable {
export type HubProxyFactoryProvider = { export type HubProxyFactoryProvider = {
(hubType: "IJtagHub"): HubProxyFactory<IJtagHub>; (hubType: "IJtagHub"): HubProxyFactory<IJtagHub>;
(hubType: "IProgressHub"): HubProxyFactory<IProgressHub>;
} }
export const getHubProxyFactory = ((hubType: string) => { export const getHubProxyFactory = ((hubType: string) => {
if(hubType === "IJtagHub") { if(hubType === "IJtagHub") {
return IJtagHub_HubProxyFactory.Instance; return IJtagHub_HubProxyFactory.Instance;
} }
if(hubType === "IProgressHub") {
return IProgressHub_HubProxyFactory.Instance;
}
}) as HubProxyFactoryProvider; }) as HubProxyFactoryProvider;
export type ReceiverRegisterProvider = { export type ReceiverRegisterProvider = {
(receiverType: "IJtagReceiver"): ReceiverRegister<IJtagReceiver>; (receiverType: "IJtagReceiver"): ReceiverRegister<IJtagReceiver>;
(receiverType: "IProgressReceiver"): ReceiverRegister<IProgressReceiver>;
} }
export const getReceiverRegister = ((receiverType: string) => { export const getReceiverRegister = ((receiverType: string) => {
if(receiverType === "IJtagReceiver") { if(receiverType === "IJtagReceiver") {
return IJtagReceiver_Binder.Instance; return IJtagReceiver_Binder.Instance;
} }
if(receiverType === "IProgressReceiver") {
return IProgressReceiver_Binder.Instance;
}
}) as ReceiverRegisterProvider; }) as ReceiverRegisterProvider;
// HubProxy // HubProxy
@@ -92,6 +101,27 @@ class IJtagHub_HubProxy implements IJtagHub {
} }
} }
class IProgressHub_HubProxyFactory implements HubProxyFactory<IProgressHub> {
public static Instance = new IProgressHub_HubProxyFactory();
private constructor() {
}
public readonly createHubProxy = (connection: HubConnection): IProgressHub => {
return new IProgressHub_HubProxy(connection);
}
}
class IProgressHub_HubProxy implements IProgressHub {
public constructor(private connection: HubConnection) {
}
public readonly join = async (taskId: string): Promise<boolean> => {
return await this.connection.invoke("Join", taskId);
}
}
// Receiver // Receiver
@@ -116,3 +146,24 @@ class IJtagReceiver_Binder implements ReceiverRegister<IJtagReceiver> {
} }
} }
class IProgressReceiver_Binder implements ReceiverRegister<IProgressReceiver> {
public static Instance = new IProgressReceiver_Binder();
private constructor() {
}
public readonly register = (connection: HubConnection, receiver: IProgressReceiver): Disposable => {
const __onReceiveProgress = (...args: [ProgressInfo]) => receiver.onReceiveProgress(...args);
connection.on("OnReceiveProgress", __onReceiveProgress);
const methodList: ReceiverMethod[] = [
{ methodName: "OnReceiveProgress", method: __onReceiveProgress }
]
return new ReceiverMethodSubscription(connection, methodList);
}
}

View File

@@ -3,6 +3,7 @@
/* tslint:disable */ /* tslint:disable */
// @ts-nocheck // @ts-nocheck
import type { IStreamResult, Subject } from '@microsoft/signalr'; import type { IStreamResult, Subject } from '@microsoft/signalr';
import type { ProgressInfo } from '../server.Hubs';
export type IJtagHub = { export type IJtagHub = {
/** /**
@@ -21,6 +22,14 @@ export type IJtagHub = {
stopBoundaryScan(): Promise<boolean>; stopBoundaryScan(): Promise<boolean>;
} }
export type IProgressHub = {
/**
* @param taskId Transpiled from string
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
join(taskId: string): Promise<boolean>;
}
export type IJtagReceiver = { export type IJtagReceiver = {
/** /**
* @param msg Transpiled from System.Collections.Generic.Dictionary<string, bool> * @param msg Transpiled from System.Collections.Generic.Dictionary<string, bool>
@@ -29,3 +38,11 @@ export type IJtagReceiver = {
onReceiveBoundaryScanData(msg: Partial<Record<string, boolean>>): Promise<void>; onReceiveBoundaryScanData(msg: Partial<Record<string, boolean>>): Promise<void>;
} }
export type IProgressReceiver = {
/**
* @param message Transpiled from server.Hubs.ProgressInfo
* @returns Transpiled from System.Threading.Tasks.Task
*/
onReceiveProgress(message: ProgressInfo): Promise<void>;
}

View File

@@ -10,6 +10,7 @@ import {
SignalTriggerConfig, SignalTriggerConfig,
SignalValue, SignalValue,
AnalyzerChannelDiv, AnalyzerChannelDiv,
AnalyzerClockDiv,
} from "@/APIClient"; } from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { useAlertStore } from "@/components/Alert"; import { useAlertStore } from "@/components/Alert";
@@ -30,16 +31,8 @@ export type Channel = {
// 全局模式选项 // 全局模式选项
const globalModes = [ const globalModes = [
{ {value: GlobalCaptureMode.AND,label: "AND",description: "所有条件都满足时触发",},
value: GlobalCaptureMode.AND, {value: GlobalCaptureMode.OR,label: "OR",description: "任一条件满足时触发",},
label: "AND",
description: "所有条件都满足时触发",
},
{
value: GlobalCaptureMode.OR,
label: "OR",
description: "任一条件满足时触发",
},
{ value: GlobalCaptureMode.NAND, label: "NAND", description: "AND的非" }, { value: GlobalCaptureMode.NAND, label: "NAND", description: "AND的非" },
{ value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" }, { value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" },
]; ];
@@ -76,28 +69,23 @@ const channelDivOptions = [
{ value: 32, label: "32通道", description: "启用32个通道 (CH0-CH31)" }, { value: 32, label: "32通道", description: "启用32个通道 (CH0-CH31)" },
]; ];
// 捕获深度选项 const ClockDivOptions = [
const captureLengthOptions = [ { value: AnalyzerClockDiv.DIV1, label: "120MHz", description: "采样频率120MHz" },
{ value: 256, label: "256" }, { value: AnalyzerClockDiv.DIV2, label: "60MHz", description: "采样频率60MHz" },
{ value: 512, label: "512" }, { value: AnalyzerClockDiv.DIV4, label: "30MHz", description: "采样频率30MHz" },
{ value: 1024, label: "1K" }, { value: AnalyzerClockDiv.DIV8, label: "15MHz", description: "采样频率15MHz" },
{ value: 2048, label: "2K" }, { value: AnalyzerClockDiv.DIV16, label: "7.5MHz", description: "采样频率7.5MHz" },
{ value: 4096, label: "4K" }, { value: AnalyzerClockDiv.DIV32, label: "3.75MHz", description: "采样频率3.75MHz" },
{ value: 8192, label: "8K" }, { value: AnalyzerClockDiv.DIV64, label: "1.875MHz", description: "采样频率1.875MHz" },
{ value: 16384, label: "16K" }, { value: AnalyzerClockDiv.DIV128, label: "937.5KHz", description: "采样频率937.5KHz" },
{ value: 32768, label: "32K" },
]; ];
// 捕获深度选项 // 捕获深度限制常量
const preCaptureLengthOptions = [ const CAPTURE_LENGTH_MIN = 1024; // 最小捕获深度 1024
{ value: 0, label: "0" }, const CAPTURE_LENGTH_MAX = 0x10000000 - 0x01000000; // 最大捕获深度
{ value: 16, label: "16" },
{ value: 32, label: "32" }, // 预捕获深度限制常量
{ value: 64, label: "64" }, const PRE_CAPTURE_LENGTH_MIN = 2; // 最小预捕获深度 2
{ value: 128, label: "128" },
{ value: 256, label: "256" },
{ value: 512, label: "512" },
];
// 默认颜色数组 // 默认颜色数组
const defaultColors = [ const defaultColors = [
@@ -111,9 +99,8 @@ const defaultColors = [
"#8C33FF", "#8C33FF",
]; ];
// 添加逻辑分析仪频率常量 // 添加逻辑分析仪基础频率常量
const LOGIC_ANALYZER_FREQUENCY = 125_000_000; // 125MHz const BASE_LOGIC_ANALYZER_FREQUENCY = 120_000_000; // 120MHz基础频率
const SAMPLE_PERIOD_NS = 1_000_000_000 / LOGIC_ANALYZER_FREQUENCY; // 采样周期,单位:纳秒
const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
() => { () => {
@@ -126,8 +113,9 @@ 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 currentclockDiv = ref<AnalyzerClockDiv>(AnalyzerClockDiv.DIV1); // 默认时钟分频为1
const isApplying = ref(false); const isApplying = ref(false);
const isCapturing = ref(false); // 添加捕获状态标识 const isCapturing = ref(false); // 添加捕获状态标识
@@ -168,6 +156,17 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
channels.filter((channel) => channel.enabled), channels.filter((channel) => channel.enabled),
); );
// 计算属性:根据当前时钟分频获取实际采样频率
const currentSampleFrequency = computed(() => {
const divValue = Math.pow(2, currentclockDiv.value);
return BASE_LOGIC_ANALYZER_FREQUENCY / divValue;
});
// 计算属性:获取当前采样周期(纳秒)
const currentSamplePeriodNs = computed(() => {
return 1_000_000_000 / currentSampleFrequency.value;
});
// 转换通道数字到枚举值 // 转换通道数字到枚举值
const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => { const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
switch (channelCount) { switch (channelCount) {
@@ -181,6 +180,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) => {
// 验证通道数量是否有效 // 验证通道数量是否有效
@@ -210,9 +267,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
alert?.info(`全局触发模式已设置为 ${modeOption?.label}`, 2000); alert?.info(`全局触发模式已设置为 ${modeOption?.label}`, 2000);
}; };
const setClockDiv = (mode: AnalyzerClockDiv) => {
currentclockDiv.value = mode;
const modeOption = ClockDivOptions.find((m) => m.value === mode);
alert?.info(`时钟分频已设置为 ${modeOption?.label}`, 2000);
};
const resetConfiguration = () => { const resetConfiguration = () => {
currentGlobalMode.value = GlobalCaptureMode.AND; currentGlobalMode.value = GlobalCaptureMode.AND;
currentChannelDiv.value = 8; // 重置为默认的8通道 currentChannelDiv.value = 8; // 重置为默认的8通道
currentclockDiv.value = AnalyzerClockDiv.DIV1; // 重置为默认采样频率
setChannelDiv(8); // 重置为默认的8通道 setChannelDiv(8); // 重置为默认的8通道
signalConfigs.forEach((signal) => { signalConfigs.forEach((signal) => {
@@ -243,7 +307,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 根据当前通道数量解析数据 // 根据当前通道数量解析数据
const channelCount = currentChannelDiv.value; const channelCount = currentChannelDiv.value;
const timeStepNs = SAMPLE_PERIOD_NS; const timeStepNs = currentSamplePeriodNs.value;
let sampleCount: number; let sampleCount: number;
let x: number[]; let x: number[];
@@ -486,6 +550,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
channelDiv: getChannelDivEnum(currentChannelDiv.value), channelDiv: getChannelDivEnum(currentChannelDiv.value),
captureLength: captureLength.value, captureLength: captureLength.value,
preCaptureLength: preCaptureLength.value, preCaptureLength: preCaptureLength.value,
clockDiv: currentclockDiv.value,
signalConfigs: allSignals, signalConfigs: allSignals,
}); });
@@ -624,13 +689,13 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 添加生成测试数据的方法 // 添加生成测试数据的方法
const generateTestData = () => { const generateTestData = () => {
const sampleRate = LOGIC_ANALYZER_FREQUENCY; // 使用实际的逻辑分析仪频率 const sampleRate = currentSampleFrequency.value; // 使用当前设置的采样频率
const duration = 0.001; // 1ms的数据 const duration = 0.001; // 1ms的数据
const points = Math.floor(sampleRate * duration); const points = Math.floor(sampleRate * duration);
const x = Array.from( const x = Array.from(
{ length: points }, { length: points },
(_, i) => (i * SAMPLE_PERIOD_NS) / 1000, // 时间轴,单位:微秒 (_, i) => (i * currentSamplePeriodNs.value) / 1000, // 时间轴,单位:微秒
); );
// Generate 8 channels with different digital patterns // Generate 8 channels with different digital patterns
@@ -703,6 +768,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
currentChannelDiv, // 导出当前通道组状态 currentChannelDiv, // 导出当前通道组状态
captureLength, // 导出捕获深度 captureLength, // 导出捕获深度
preCaptureLength, // 导出预捕获深度 preCaptureLength, // 导出预捕获深度
currentclockDiv, // 导出当前采样频率状态
isApplying, isApplying,
isCapturing, // 导出捕获状态 isCapturing, // 导出捕获状态
isOperationInProgress, // 导出操作进行状态 isOperationInProgress, // 导出操作进行状态
@@ -711,18 +777,29 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
enabledChannelCount, enabledChannelCount,
channelNames, channelNames,
enabledChannels, enabledChannels,
currentSampleFrequency, // 导出当前采样频率
currentSamplePeriodNs, // 导出当前采样周期
// 选项数据 // 选项数据
globalModes, globalModes,
operators, operators,
signalValues, signalValues,
channelDivOptions, // 导出通道组选项 channelDivOptions, // 导出通道组选项
captureLengthOptions, // 导出捕获深度选项 ClockDivOptions, // 导出采样频率选项
preCaptureLengthOptions, // 导出预捕获深度选项
// 捕获深度常量和验证
CAPTURE_LENGTH_MIN,
CAPTURE_LENGTH_MAX,
PRE_CAPTURE_LENGTH_MIN,
validateCaptureLength,
validatePreCaptureLength,
setCaptureLength,
setPreCaptureLength,
// 触发设置方法 // 触发设置方法
setChannelDiv, // 导出设置通道组方法 setChannelDiv, // 导出设置通道组方法
setGlobalMode, setGlobalMode,
setClockDiv, // 导出设置采样频率方法
resetConfiguration, resetConfiguration,
setLogicData, setLogicData,
startCapture, startCapture,

View File

@@ -3,89 +3,220 @@
<!-- 通道配置 --> <!-- 通道配置 -->
<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"
@click="toggleGlobalModeDropdown"
:aria-expanded="showGlobalModeDropdown"
aria-haspopup="listbox"
role="combobox"
> >
<option <span>{{ currentGlobalModeLabel }}</span>
<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]">
<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" v-for="mode in globalModes"
:key="mode.value" :key="mode.value"
:value="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 }} - {{ mode.description }} {{ mode.label }}
</option>
</select>
</div> </div>
</div>
<div class="flex flex-row gap-2 items-center"> </div>
<label class="label"> <p class="flex items-center text-xs text-slate-400">
<span class="label-text text-sm">通道组</span> {{ currentGlobalModeDescription }}
</p>
</div>
<div class="flex flex-col gap-2">
<label class="block text-sm font-semibold antialiased">
通道组
</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"
@click="toggleChannelDivDropdown"
:aria-expanded="showChannelDivDropdown"
aria-haspopup="listbox"
role="combobox"
> >
<option <span>{{ currentChannelDivLabel }}</span>
<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]">
<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" v-for="option in channelDivOptions"
:key="option.value" :key="option.value"
:value="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 }} {{ option.label }}
</option>
</select>
</div> </div>
</div>
<div class="flex flex-row gap-2 items-center"> </div>
<label class="label"> <p class="flex items-center text-xs text-slate-400">
<span class="label-text text-sm">捕获深度</span> {{ currentChannelDivDescription }}
</p>
</div>
<div class="flex flex-col gap-2">
<label class="block text-sm font-semibold antialiased text-slate-800">
采样频率
</label> </label>
<select <div class="relative w-[200px]">
v-model="captureLength" <button
class="select select-sm select-bordered" tabindex="0"
type="button"
class="flex items-center gap-4 justify-between h-max outline-none focus:outline-none text-slate-600 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"
@click="toggleClockDivDropdown"
:aria-expanded="showClockDivDropdown"
aria-haspopup="listbox"
role="combobox"
> >
<option <span>{{ currentClockDivLabel }}</span>
v-for="option in captureLengthOptions" <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]">
:key="option.value" <path d="M17 8L12 3L7 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
:value="option.value" <path d="M17 16L12 21L7 16" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
> </svg>
{{ 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-2">
<button @click="resetConfiguration" class="btn btn-outline btn-sm">
重置配置
</button> </button>
<input readonly style="display:none" :value="currentclockDiv" />
<!-- 下拉菜单 -->
<div v-if="showClockDivDropdown" 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 ClockDivOptions"
:key="option.value"
@click="selectClockDiv(option.value)"
class="px-3 py-2 text-sm text-slate-700 hover:bg-slate-100 cursor-pointer"
:class="{ 'bg-slate-100': option.value === currentclockDiv }"
>
{{ option.label }}
</div>
</div>
</div>
<p class="flex items-center text-xs text-slate-400">
{{ currentClockDivDescription }}
</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="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"
type="button"
:disabled="captureLength <= CAPTURE_LENGTH_MIN"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
<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" />
</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 class="flex flex-col gap-2">
<label class="block text-sm font-semibold antialiased">
预捕获深度
</label>
<div class="relative w-[200px]">
<button
@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"
type="button"
:disabled="preCaptureLength <= PRE_CAPTURE_LENGTH_MIN"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
<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" />
</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> </div>
<!-- 通道列表 --> <!-- 通道列表 -->
@@ -177,12 +308,14 @@
</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";
const { const {
currentGlobalMode, currentGlobalMode,
currentChannelDiv, currentChannelDiv,
currentclockDiv,
captureLength, captureLength,
preCaptureLength, preCaptureLength,
isApplying, isApplying,
@@ -193,10 +326,153 @@ const {
operators, operators,
signalValues, signalValues,
channelDivOptions, channelDivOptions,
captureLengthOptions, ClockDivOptions,
preCaptureLengthOptions, CAPTURE_LENGTH_MIN,
CAPTURE_LENGTH_MAX,
PRE_CAPTURE_LENGTH_MIN,
validateCaptureLength,
validatePreCaptureLength,
setCaptureLength,
setPreCaptureLength,
setChannelDiv, setChannelDiv,
setGlobalMode, setGlobalMode,
setClockDiv,
resetConfiguration, resetConfiguration,
} = useRequiredInjection(useLogicAnalyzerState); } = useRequiredInjection(useLogicAnalyzerState);
// 下拉菜单状态
const showGlobalModeDropdown = ref(false);
const showChannelDivDropdown = ref(false);
const showClockDivDropdown = 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 currentClockDivLabel = computed(() => {
const option = ClockDivOptions.find(opt => opt.value === currentclockDiv.value);
return option ? option.label : '';
});
// 计算属性:获取当前采样频率的描述
const currentClockDivDescription = computed(() => {
const option = ClockDivOptions.find(opt => opt.value === currentclockDiv.value);
return option ? option.description : '';
});
// 全局模式下拉菜单相关函数
const toggleGlobalModeDropdown = () => {
showGlobalModeDropdown.value = !showGlobalModeDropdown.value;
if (showGlobalModeDropdown.value) {
showChannelDivDropdown.value = false;
showClockDivDropdown.value = false;
}
};
const selectGlobalMode = (mode: any) => {
setGlobalMode(mode);
showGlobalModeDropdown.value = false;
};
// 通道组下拉菜单相关函数
const toggleChannelDivDropdown = () => {
showChannelDivDropdown.value = !showChannelDivDropdown.value;
if (showChannelDivDropdown.value) {
showGlobalModeDropdown.value = false;
showClockDivDropdown.value = false;
}
};
const selectChannelDiv = (value: number) => {
setChannelDiv(value);
showChannelDivDropdown.value = false;
};
// 采样频率下拉菜单相关函数
const toggleClockDivDropdown = () => {
showClockDivDropdown.value = !showClockDivDropdown.value;
if (showClockDivDropdown.value) {
showGlobalModeDropdown.value = false;
showChannelDivDropdown.value = false;
}
};
const selectClockDiv = (value: any) => {
setClockDiv(value);
showClockDivDropdown.value = false;
};
// 点击其他地方关闭下拉菜单
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.relative')) {
showGlobalModeDropdown.value = false;
showChannelDivDropdown.value = false;
showClockDivDropdown.value = false;
}
};
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
</script> </script>

View File

@@ -8,7 +8,11 @@
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">示例比特流文件</legend> <legend class="fieldset-legend text-sm">示例比特流文件</legend>
<div class="space-y-2"> <div class="space-y-2">
<div v-for="bitstream in availableBitstreams" :key="bitstream.id" class="flex items-center justify-between p-2 bg-base-200 rounded"> <div
v-for="bitstream in availableBitstreams"
:key="bitstream.id"
class="flex items-center justify-between p-2 border-2 border-base-300 rounded-lg"
>
<span class="text-sm">{{ bitstream.name }}</span> <span class="text-sm">{{ bitstream.name }}</span>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -18,24 +22,20 @@
> >
<div v-if="isDownloading"> <div v-if="isDownloading">
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
下载中... {{ downloadProgress }}%
</div>
<div v-else>
下载示例
</div> </div>
<div v-else>下载示例</div>
</button> </button>
<button <button
@click="programExampleBitstream(bitstream)" @click="programExampleBitstream(bitstream)"
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"
:disabled="isDownloading || isProgramming || !uploadEvent" :disabled="isDownloading || isProgramming"
> >
<div v-if="isProgramming"> <div v-if="isProgramming">
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
烧录中... 烧录中...
</div> </div>
<div v-else> <div v-else>直接烧录</div>
直接烧录
</div>
</button> </button>
</div> </div>
</div> </div>
@@ -44,25 +44,34 @@
</div> </div>
<!-- 分割线 --> <!-- 分割线 -->
<div v-if="examId && availableBitstreams.length > 0" class="divider"></div> <div v-if="examId && availableBitstreams.length > 0" class="divider">
</div>
<!-- Input File --> <!-- Input File -->
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">上传自定义比特流文件</legend> <legend class="fieldset-legend text-sm">上传自定义比特流文件</legend>
<input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" /> <input
type="file"
ref="fileInput"
class="file-input w-full"
@change="handleFileChange"
/>
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label> <label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
</fieldset> </fieldset>
<!-- Upload Button --> <!-- Upload Button -->
<div class="card-actions w-full"> <div class="card-actions w-full">
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading || isProgramming"> <button
@click="handleClick"
class="btn btn-primary grow"
:disabled="isUploading || isProgramming"
>
<div v-if="isUploading"> <div v-if="isUploading">
<span class="loading loading-spinner"></span> <span class="loading loading-spinner"></span>
上传中... 上传中...
</div> </div>
<div v-else> <div v-else>上传并下载</div>
{{ buttonText }}
</div>
</button> </button>
</div> </div>
</div> </div>
@@ -73,32 +82,68 @@ import { computed, ref, useTemplateRef, onMounted } from "vue";
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
import { useDialogStore } from "@/stores/dialog"; import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash"; import { isNull, isUndefined } from "lodash";
import { useEquipments } from "@/stores/equipments";
import type { HubConnection } from "@microsoft/signalr";
import type {
IProgressHub,
IProgressReceiver,
} from "@/TypedSignalR.Client/server.Hubs";
import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client";
import { ProgressStatus } from "@/server.Hubs";
import { useRequiredInjection } from "@/utils/Common";
import { useAlertStore } from "./Alert";
interface Props { interface Props {
uploadEvent?: (file: File, examId: string) => Promise<number | null>;
downloadEvent?: (bitstreamId: number) => Promise<boolean>;
maxMemory?: number; maxMemory?: number;
examId?: string; // 新增examId属性 examId?: string; // 新增examId属性
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
maxMemory: 4, maxMemory: 4,
examId: '', examId: "",
}); });
const emits = defineEmits<{ const emits = defineEmits<{
finishedUpload: [file: File]; finishedUpload: [file: File];
}>(); }>();
const alert = useRequiredInjection(useAlertStore);
const dialog = useDialogStore(); const dialog = useDialogStore();
const eqps = useEquipments();
const isUploading = ref(false); const isUploading = ref(false);
const isDownloading = ref(false); const isDownloading = ref(false);
const isProgramming = ref(false); const isProgramming = ref(false);
const availableBitstreams = ref<{id: number, name: string}[]>([]); const availableBitstreams = ref<{ id: number; name: string }[]>([]);
const buttonText = computed(() => { // Progress
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载"; const downloadTaskId = ref("");
const downloadProgress = ref(0);
const progressHubConnection = ref<HubConnection>();
const progressHubProxy = ref<IProgressHub>();
const progressHubReceiver: IProgressReceiver = {
onReceiveProgress: async (msg) => {
if (msg.taskId == downloadTaskId.value) {
if (msg.status == ProgressStatus.InProgress) {
downloadProgress.value = msg.progressPercent;
} else if (msg.status == ProgressStatus.Failed) {
dialog.error(msg.errorMessage);
} else if (msg.status == ProgressStatus.Completed) {
alert.info("比特流下载成功");
}
}
},
};
onMounted(async () => {
progressHubConnection.value =
AuthManager.createAuthenticatedProgressHubConnection();
progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy(
progressHubConnection.value,
);
getReceiverRegister("IProgressReceiver").register(
progressHubConnection.value,
progressHubReceiver,
);
}); });
const fileInput = useTemplateRef("fileInput"); const fileInput = useTemplateRef("fileInput");
@@ -120,7 +165,7 @@ onMounted(async () => {
// 加载可用的比特流文件列表 // 加载可用的比特流文件列表
async function loadAvailableBitstreams() { async function loadAvailableBitstreams() {
console.log('加载可用比特流文件examId:', props.examId); console.log("加载可用比特流文件examId:", props.examId);
if (!props.examId) { if (!props.examId) {
availableBitstreams.value = []; availableBitstreams.value = [];
return; return;
@@ -129,16 +174,24 @@ async function loadAvailableBitstreams() {
try { try {
const resourceClient = AuthManager.createAuthenticatedResourceClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取比特流模板资源列表 // 使用新的ResourceClient API获取比特流模板资源列表
const resources = await resourceClient.getResourceList(props.examId, 'bitstream', 'template'); const resources = await resourceClient.getResourceList(
availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || []; props.examId,
"bitstream",
"template",
);
availableBitstreams.value =
resources.map((r) => ({ id: r.id, name: r.name })) || [];
} catch (error) { } catch (error) {
console.error('加载比特流列表失败:', error); console.error("加载比特流列表失败:", error);
availableBitstreams.value = []; availableBitstreams.value = [];
} }
} }
// 下载示例比特流 // 下载示例比特流
async function downloadExampleBitstream(bitstream: {id: number, name: string}) { async function downloadExampleBitstream(bitstream: {
id: number;
name: string;
}) {
if (isDownloading.value) return; if (isDownloading.value) return;
isDownloading.value = true; isDownloading.value = true;
@@ -151,7 +204,7 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
if (response && response.data) { if (response && response.data) {
// 创建下载链接 // 创建下载链接
const url = URL.createObjectURL(response.data); const url = URL.createObjectURL(response.data);
const link = document.createElement('a'); const link = document.createElement("a");
link.href = url; link.href = url;
link.download = response.fileName || bitstream.name; link.download = response.fileName || bitstream.name;
document.body.appendChild(link); document.body.appendChild(link);
@@ -164,7 +217,7 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
dialog.error("下载失败:响应数据为空"); dialog.error("下载失败:响应数据为空");
} }
} catch (error) { } catch (error) {
console.error('下载示例比特流失败:', error); console.error("下载示例比特流失败:", error);
dialog.error("下载示例比特流失败"); dialog.error("下载示例比特流失败");
} finally { } finally {
isDownloading.value = false; isDownloading.value = false;
@@ -172,25 +225,17 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
} }
// 直接烧录示例比特流 // 直接烧录示例比特流
async function programExampleBitstream(bitstream: {id: number, name: string}) { async function programExampleBitstream(bitstream: {
id: number;
name: string;
}) {
if (isProgramming.value) return; if (isProgramming.value) return;
isProgramming.value = true; isProgramming.value = true;
try { try {
const resourceClient = AuthManager.createAuthenticatedResourceClient(); const downloadTaskId = await eqps.jtagDownloadBitstream(bitstream.id);
if (props.downloadEvent) {
const downloadSuccess = await props.downloadEvent(bitstream.id);
if (downloadSuccess) {
dialog.info("示例比特流烧录成功");
} else {
dialog.error("烧录失败");
}
} else {
dialog.info("示例比特流props.downloadEvent未定义 无法烧录");
}
} catch (error) { } catch (error) {
console.error('烧录示例比特流失败:', error); console.error("烧录示例比特流失败:", error);
dialog.error("烧录示例比特流失败"); dialog.error("烧录示例比特流失败");
} finally { } finally {
isProgramming.value = false; isProgramming.value = false;
@@ -225,22 +270,16 @@ async function handleClick(event: Event): Promise<void> {
} }
if (!checkFile(bitstream.value)) return; if (!checkFile(bitstream.value)) return;
if (isUndefined(props.uploadEvent)) {
dialog.error("无法上传");
return;
}
isUploading.value = true; isUploading.value = true;
let uploadedBitstreamId: number | null = null; let uploadedBitstreamId: number | null = null;
try { try {
console.log("开始上传比特流文件:", bitstream.value.name); console.log("开始上传比特流文件:", bitstream.value.name);
const bitstreamId = await props.uploadEvent(bitstream.value, props.examId || ''); const bitstreamId = await eqps.jtagUploadBitstream(
bitstream.value,
props.examId || "",
);
console.log("上传结果ID:", bitstreamId); console.log("上传结果ID:", bitstreamId);
if (isUndefined(props.downloadEvent)) {
console.log("上传成功,下载未定义");
isUploading.value = false;
return;
}
if (bitstreamId === null || bitstreamId === undefined) { if (bitstreamId === null || bitstreamId === undefined) {
isUploading.value = false; isUploading.value = false;
return; return;
@@ -251,6 +290,7 @@ async function handleClick(event: Event): Promise<void> {
console.error(e); console.error(e);
return; return;
} }
isUploading.value = false;
// Download // Download
try { try {
@@ -258,16 +298,14 @@ async function handleClick(event: Event): Promise<void> {
if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) { if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
dialog.error("uploadedBitstreamId is null or undefined"); dialog.error("uploadedBitstreamId is null or undefined");
} else { } else {
const ret = await props.downloadEvent(uploadedBitstreamId); isDownloading.value = true;
if (ret) dialog.info("下载成功"); downloadTaskId.value =
else dialog.error("下载失败"); await eqps.jtagDownloadBitstream(uploadedBitstreamId);
} }
} catch (e) { } catch (e) {
dialog.error("下载失败"); dialog.error("下载失败");
console.error(e); console.error(e);
} }
isUploading.value = false;
} }
</script> </script>

View File

@@ -24,7 +24,6 @@
<UploadCard <UploadCard
:exam-id="props.examId" :exam-id="props.examId"
:upload-event="eqps.jtagUploadBitstream" :upload-event="eqps.jtagUploadBitstream"
:download-event="handleDownloadBitstream"
:bitstream-file="eqps.jtagBitstream" :bitstream-file="eqps.jtagBitstream"
@update:bitstream-file="handleBitstreamChange" @update:bitstream-file="handleBitstreamChange"
> >
@@ -128,11 +127,6 @@ function handleBitstreamChange(file: File | undefined) {
eqps.jtagBitstream = file; eqps.jtagBitstream = file;
} }
async function handleDownloadBitstream(bitstreamId: number): Promise<boolean> {
console.log("开始下载比特流ID:", bitstreamId);
return await eqps.jtagDownloadBitstream(bitstreamId);
}
function handleSelectJtagSpeed(event: Event) { function handleSelectJtagSpeed(event: Event) {
const target = event.target as HTMLSelectElement; const target = event.target as HTMLSelectElement;
eqps.jtagSetSpeed(target.selectedIndex); eqps.jtagSetSpeed(target.selectedIndex);

25
src/server.Hubs.ts Normal file
View File

@@ -0,0 +1,25 @@
/* THIS (.ts) FILE IS GENERATED BY Tapper */
/* eslint-disable */
/* tslint:disable */
/** Transpiled from server.Hubs.ProgressStatus */
export enum ProgressStatus {
Pending = 0,
InProgress = 1,
Completed = 2,
Canceled = 3,
Failed = 4,
}
/** Transpiled from server.Hubs.ProgressInfo */
export type ProgressInfo = {
/** Transpiled from string */
taskId: string;
/** Transpiled from server.Hubs.ProgressStatus */
status: ProgressStatus;
/** Transpiled from int */
progressPercent: number;
/** Transpiled from string */
errorMessage: string;
}

View File

@@ -1,7 +1,7 @@
import { ref, reactive, watchPostEffect, onMounted, onUnmounted } from "vue"; import { ref, reactive, watchPostEffect, onMounted, onUnmounted } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import { isString, toNumber, type Dictionary } from "lodash"; import { isString, toNumber, isUndefined, type Dictionary } from "lodash";
import z from "zod"; import z from "zod";
import { isNumber } from "mathjs"; import { isNumber } from "mathjs";
import { Mutex, withTimeout } from "async-mutex"; import { Mutex, withTimeout } from "async-mutex";
@@ -9,9 +9,10 @@ import { useConstraintsStore } from "@/stores/constraints";
import { useDialogStore } from "./dialog"; import { useDialogStore } from "./dialog";
import { toFileParameterOrUndefined } from "@/utils/Common"; import { toFileParameterOrUndefined } from "@/utils/Common";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { HubConnectionBuilder } from "@microsoft/signalr"; import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client"; import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client";
import type { ResourceInfo } from "@/APIClient"; import type { ResourceInfo } from "@/APIClient";
import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs";
export const useEquipments = defineStore("equipments", () => { export const useEquipments = defineStore("equipments", () => {
// Global Stores // Global Stores
@@ -30,22 +31,32 @@ export const useEquipments = defineStore("equipments", () => {
1000, 1000,
new Error("JtagClient Mutex Timeout!"), new Error("JtagClient Mutex Timeout!"),
); );
const jtagHubConnection = new HubConnectionBuilder() const jtagHubConnection = ref<HubConnection>();
.withUrl("http://localhost:5000/hubs/JtagHub") const jtagHubProxy = ref<IJtagHub>();
.withAutomaticReconnect()
.build(); onMounted(async () => {
const jtagHubProxy = // 每次挂载都重新创建连接
getHubProxyFactory("IJtagHub").createHubProxy(jtagHubConnection); jtagHubConnection.value =
const jtagHubSubscription = getReceiverRegister("IJtagReceiver").register( AuthManager.createAuthenticatedJtagHubConnection();
jtagHubConnection, jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
{ jtagHubConnection.value,
);
getReceiverRegister("IJtagReceiver").register(jtagHubConnection.value, {
onReceiveBoundaryScanData: async (msg) => { onReceiveBoundaryScanData: async (msg) => {
constrainsts.batchSetConstraintStates(msg); constrainsts.batchSetConstraintStates(msg);
}, },
}, });
); await jtagHubConnection.value.start();
onMounted(() => { });
onUnmounted(() => {
// 断开连接,清理资源
if (jtagHubConnection.value) {
jtagHubConnection.value.stop();
jtagHubConnection.value = undefined;
jtagHubProxy.value = undefined;
}
}); });
// Matrix Key // Matrix Key
@@ -89,24 +100,41 @@ export const useEquipments = defineStore("equipments", () => {
} }
async function jtagBoundaryScanSetOnOff(enable: boolean) { async function jtagBoundaryScanSetOnOff(enable: boolean) {
jtagHubConnection.start(); if (isUndefined(jtagHubProxy.value)) {
enableJtagBoundaryScan.value = enable; console.error("JtagHub Not Initialize...");
if (enable) { return;
jtagHubProxy.startBoundaryScan(jtagBoundaryScanFreq.value);
} else {
jtagHubProxy.stopBoundaryScan();
}
} }
async function jtagUploadBitstream(bitstream: File, examId?: string): Promise<number | null> { if (enable) {
const ret = await jtagHubProxy.value.startBoundaryScan(
jtagBoundaryScanFreq.value,
);
if (!ret) {
console.error("Failed to start boundary scan");
return;
}
} else {
const ret = await jtagHubProxy.value.stopBoundaryScan();
if (!ret) {
console.error("Failed to stop boundary scan");
return;
}
}
enableJtagBoundaryScan.value = enable;
}
async function jtagUploadBitstream(
bitstream: File,
examId?: string,
): Promise<number | null> {
try { try {
// 自动开启电源 // 自动开启电源
await powerSetOnOff(true); await powerSetOnOff(true);
const resourceClient = AuthManager.createAuthenticatedResourceClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resp = await resourceClient.addResource( const resp = await resourceClient.addResource(
'bitstream', "bitstream",
'user', "user",
examId || null, examId || null,
toFileParameterOrUndefined(bitstream), toFileParameterOrUndefined(bitstream),
); );
@@ -124,10 +152,10 @@ export const useEquipments = defineStore("equipments", () => {
} }
} }
async function jtagDownloadBitstream(bitstreamId?: number): Promise<boolean> { async function jtagDownloadBitstream(bitstreamId?: number): Promise<string> {
if (bitstreamId === null || bitstreamId === undefined) { if (bitstreamId === null || bitstreamId === undefined) {
dialog.error("请先选择要下载的比特流"); dialog.error("请先选择要下载的比特流");
return false; return "";
} }
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
@@ -145,7 +173,7 @@ export const useEquipments = defineStore("equipments", () => {
} catch (e) { } catch (e) {
dialog.error("下载错误"); dialog.error("下载错误");
console.error(e); console.error(e);
return false; throw e;
} finally { } finally {
release(); release();
} }

View File

@@ -15,8 +15,12 @@ import {
DebuggerClient, DebuggerClient,
ExamClient, ExamClient,
ResourceClient, ResourceClient,
HdmiVideoStreamClient,
} from "@/APIClient"; } from "@/APIClient";
import router from "@/router";
import { HubConnectionBuilder } from "@microsoft/signalr";
import axios, { type AxiosInstance } from "axios"; import axios, { type AxiosInstance } from "axios";
import { isNull } from "lodash";
// 支持的客户端类型联合类型 // 支持的客户端类型联合类型
type SupportedClient = type SupportedClient =
@@ -35,7 +39,8 @@ type SupportedClient =
| OscilloscopeApiClient | OscilloscopeApiClient
| DebuggerClient | DebuggerClient
| ExamClient | ExamClient
| ResourceClient; | ResourceClient
| HdmiVideoStreamClient;
export class AuthManager { export class AuthManager {
// 存储token到localStorage // 存储token到localStorage
@@ -119,7 +124,7 @@ export class AuthManager {
if (!token) return null; if (!token) return null;
const instance = axios.create(); const instance = axios.create();
instance.interceptors.request.use(config => { instance.interceptors.request.use((config) => {
config.headers = config.headers || {}; config.headers = config.headers || {};
(config.headers as any)["Authorization"] = `Bearer ${token}`; (config.headers as any)["Authorization"] = `Bearer ${token}`;
return config; return config;
@@ -202,6 +207,40 @@ export class AuthManager {
return AuthManager.createAuthenticatedClient(ResourceClient); return AuthManager.createAuthenticatedClient(ResourceClient);
} }
public static createAuthenticatedHdmiVideoStreamClient(): HdmiVideoStreamClient {
return AuthManager.createAuthenticatedClient(HdmiVideoStreamClient);
}
public static createAuthenticatedJtagHubConnection() {
const token = this.getToken();
if (isNull(token)) {
router.push("/login");
throw Error("Token Null!");
}
return new HubConnectionBuilder()
.withUrl("http://127.0.0.1:5000/hubs/JtagHub", {
accessTokenFactory: () => token,
})
.withAutomaticReconnect()
.build();
}
public static createAuthenticatedProgressHubConnection() {
const token = this.getToken();
if (isNull(token)) {
router.push("/login");
throw Error("Token Null!");
}
return new HubConnectionBuilder()
.withUrl("http://127.0.0.1:5000/hubs/ProgressHub", {
accessTokenFactory: () => token,
})
.withAutomaticReconnect()
.build();
}
// 登录函数 // 登录函数
public static async login( public static async login(
username: string, username: string,

View File

@@ -31,8 +31,8 @@
:checked="checkID === 3" :checked="checkID === 3"
@change="handleTabChange" @change="handleTabChange"
/> />
<SquareActivityIcon class="icon" /> <Monitor class="icon" />
示波器 HDMI视频流
</label> </label>
<label class="tab"> <label class="tab">
<input <input
@@ -42,8 +42,8 @@
:checked="checkID === 4" :checked="checkID === 4"
@change="handleTabChange" @change="handleTabChange"
/> />
<Binary class="icon" /> <SquareActivityIcon class="icon" />
逻辑分析仪 示波器
</label> </label>
<label class="tab"> <label class="tab">
<input <input
@@ -53,6 +53,17 @@
:checked="checkID === 5" :checked="checkID === 5"
@change="handleTabChange" @change="handleTabChange"
/> />
<Binary class="icon" />
逻辑分析仪
</label>
<label class="tab">
<input
type="radio"
name="function-bar"
id="6"
:checked="checkID === 6"
@change="handleTabChange"
/>
<Hand class="icon" /> <Hand class="icon" />
嵌入式逻辑分析仪 嵌入式逻辑分析仪
</label> </label>
@@ -73,12 +84,15 @@
<VideoStreamView /> <VideoStreamView />
</div> </div>
<div v-else-if="checkID === 3" class="h-full overflow-y-auto"> <div v-else-if="checkID === 3" class="h-full overflow-y-auto">
<OscilloscopeView /> <HdmiVideoStreamView />
</div> </div>
<div v-else-if="checkID === 4" class="h-full overflow-y-auto"> <div v-else-if="checkID === 4" class="h-full overflow-y-auto">
<LogicAnalyzerView /> <OscilloscopeView />
</div> </div>
<div v-else-if="checkID === 5" class="h-full overflow-y-auto"> <div v-else-if="checkID === 5" class="h-full overflow-y-auto">
<LogicAnalyzerView />
</div>
<div v-else-if="checkID === 6" class="h-full overflow-y-auto">
<Debugger /> <Debugger />
</div> </div>
</div> </div>
@@ -94,9 +108,11 @@ import {
MinimizeIcon, MinimizeIcon,
Binary, Binary,
Hand, Hand,
Monitor,
} from "lucide-vue-next"; } from "lucide-vue-next";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import VideoStreamView from "@/views/Project/VideoStream.vue"; import VideoStreamView from "@/views/Project/VideoStream.vue";
import HdmiVideoStreamView from "@/views/Project/HdmiVideoStream.vue";
import OscilloscopeView from "@/views/Project/Oscilloscope.vue"; import OscilloscopeView from "@/views/Project/Oscilloscope.vue";
import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue"; import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue";
import { isNull, toNumber } from "lodash"; import { isNull, toNumber } from "lodash";

View File

@@ -0,0 +1,490 @@
<template>
<div class="bg-base-100 flex flex-col gap-7">
<!-- 控制面板 -->
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title text-primary">
<Settings class="w-6 h-6" />
HDMI视频流控制面板
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 板卡信息 -->
<div class="stats shadow">
<div class="stat bg-base-100">
<div class="stat-figure text-primary">
<div class="badge" :class="endpoint ? 'badge-success' : 'badge-warning'">
{{ endpoint ? "已连接" : "未配置" }}
</div>
</div>
<div class="stat-title">板卡状态</div>
<div class="stat-value text-primary">HDMI</div>
<div class="stat-desc">{{ endpoint ? `板卡: ${endpoint.boardId.substring(0, 8)}...` : "请先连接板卡" }}</div>
</div>
</div>
<!-- 连接状态 -->
<div class="stats shadow">
<div class="stat bg-base-100">
<div class="stat-figure text-secondary">
<Video class="w-8 h-8" />
</div>
<div class="stat-title">视频状态</div>
<div class="stat-value text-secondary">
{{ isPlaying ? "播放中" : "未播放" }}
</div>
<div class="stat-desc">{{ videoStatus }}</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="card-actions justify-end mt-4">
<button class="btn btn-outline btn-primary" @click="refreshEndpoint" :disabled="loading">
<RefreshCw v-if="loading" class="animate-spin h-4 w-4 mr-2" />
<RefreshCw v-else class="h-4 w-4 mr-2" />
{{ loading ? "刷新中..." : "刷新连接" }}
</button>
<button class="btn btn-primary" @click="testConnection" :disabled="testing || !endpoint">
<RefreshCw v-if="testing" class="animate-spin h-4 w-4 mr-2" />
<TestTube v-else class="h-4 w-4 mr-2" />
{{ testing ? "测试中..." : "测试连接" }}
</button>
</div>
</div>
</div>
<!-- 视频预览区域 -->
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title text-primary">
<Video class="w-6 h-6" />
HDMI视频预览
</h2>
<div class="relative bg-black rounded-lg overflow-hidden cursor-pointer" :class="[
{ 'cursor-not-allowed': !isPlaying || hasVideoError || !endpoint }
]" style="aspect-ratio: 16/9" @click="handleVideoClick">
<!-- 视频播放器 - 使用img标签直接显示MJPEG流 -->
<div v-show="isPlaying && endpoint" class="w-full h-full flex items-center justify-center">
<img :src="currentVideoSource" alt="HDMI视频流" class="max-w-full max-h-full object-contain"
@error="handleVideoError" @load="handleVideoLoad" />
</div>
<!-- 错误信息显示 -->
<div v-if="hasVideoError" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70">
<div class="card bg-error text-white shadow-lg w-full max-w-lg">
<div class="card-body">
<h3 class="card-title flex items-center gap-2">
<AlertTriangle class="h-6 w-6" />
HDMI视频流加载失败
</h3>
<p>无法连接到HDMI视频服务器请检查以下内容</p>
<ul class="list-disc list-inside">
<li>HDMI输入设备是否已连接</li>
<li>板卡是否正常工作</li>
<li>网络连接是否正常</li>
<li>HDMI视频流服务是否已启动</li>
</ul>
<div class="card-actions justify-end mt-2">
<button class="btn btn-sm btn-outline btn-primary" @click="tryReconnect">
重试连接
</button>
</div>
</div>
</div>
</div>
<!-- 占位符 -->
<div v-show="(!isPlaying && !hasVideoError) || !endpoint"
class="absolute inset-0 flex items-center justify-center text-white">
<div class="text-center">
<Video class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p class="text-lg opacity-75">{{ videoStatus }}</p>
<p class="text-sm opacity-60 mt-2">
{{ endpoint ? '点击"播放HDMI视频流"按钮开始查看实时视频' : '请先刷新连接以获取板卡信息' }}
</p>
</div>
</div>
</div>
<!-- 视频控制 -->
<div class="flex justify-between items-center mt-4" v-if="endpoint">
<div class="text-sm text-base-content/70">
MJPEG地址:
<code class="bg-base-300 px-2 py-1 rounded text-xs">{{
endpoint.mjpegUrl
}}</code>
</div>
<div class="space-x-2">
<div class="dropdown dropdown-hover dropdown-top dropdown-end">
<div tabindex="0" role="button" class="btn btn-sm btn-outline btn-accent">
<MoreHorizontal class="w-4 h-4 mr-1" />
更多功能
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">
<li>
<a @click="openInNewTab(endpoint.videoUrl)">
<ExternalLink class="w-4 h-4" />
在新标签打开视频页面
</a>
</li>
<li>
<a @click="takeSnapshot">
<Camera class="w-4 h-4" />
获取并下载快照
</a>
</li>
<li>
<a @click="copyToClipboard(endpoint.mjpegUrl)">
<Copy class="w-4 h-4" />
复制MJPEG地址
</a>
</li>
</ul>
</div>
<button class="btn btn-success btn-sm" @click="startStream" :disabled="isPlaying || !endpoint">
<Play class="w-4 h-4 mr-1" />
播放HDMI视频流
</button>
<button class="btn btn-error btn-sm" @click="stopStream" :disabled="!isPlaying">
<Square class="w-4 h-4 mr-1" />
停止视频流
</button>
</div>
</div>
</div>
</div>
<!-- 日志区域 -->
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title text-primary">
<FileText class="w-6 h-6" />
操作日志
</h2>
<div class="bg-base-300 rounded-lg p-4 h-48 overflow-y-auto">
<div v-for="(log, index) in logs" :key="index" class="text-sm font-mono mb-1">
<span class="text-base-content/50">[{{ formatTime(log.time) }}]</span>
<span :class="getLogClass(log.level)">{{ log.message }}</span>
</div>
<div v-if="logs.length === 0" class="text-base-content/50 text-center py-8">
暂无日志记录
</div>
</div>
<div class="card-actions justify-end mt-2">
<button class="btn btn-outline btn-sm" @click="clearLogs">
清空日志
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import {
Settings,
Video,
RefreshCw,
TestTube,
Play,
Square,
ExternalLink,
Camera,
Copy,
FileText,
AlertTriangle,
MoreHorizontal,
} from "lucide-vue-next";
import { HdmiVideoStreamClient, type HdmiVideoStreamEndpoint } from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager";
import { useAlertStore } from "@/components/Alert";
// Alert系统
const alert = useAlertStore();
// 状态管理
const loading = ref(false);
const testing = ref(false);
const isPlaying = ref(false);
const hasVideoError = ref(false);
const videoStatus = ref('未连接');
// HDMI视频流数据
const endpoint = ref<HdmiVideoStreamEndpoint | null>(null);
const currentVideoSource = ref('');
// 日志系统
interface LogEntry {
time: Date;
level: 'info' | 'success' | 'warning' | 'error';
message: string;
}
const logs = ref<LogEntry[]>([]);
// 添加日志
function addLog(level: LogEntry['level'], message: string) {
logs.value.unshift({
time: new Date(),
level,
message
});
// 保持最近100条日志
if (logs.value.length > 100) {
logs.value = logs.value.slice(0, 100);
}
}
// 格式化时间
function formatTime(date: Date): string {
return date.toLocaleTimeString();
}
// 获取日志样式类
function getLogClass(level: LogEntry['level']): string {
switch (level) {
case 'success':
return 'text-success';
case 'warning':
return 'text-warning';
case 'error':
return 'text-error';
default:
return 'text-base-content';
}
}
// 清空日志
function clearLogs() {
logs.value = [];
addLog('info', '日志已清空');
}
// 刷新HDMI视频流端点
async function refreshEndpoint() {
loading.value = true;
try {
addLog('info', '正在获取HDMI视频流端点...');
const client = AuthManager.createAuthenticatedHdmiVideoStreamClient();
const result = await client.getMyEndpoint();
if (result) {
endpoint.value = result;
videoStatus.value = '已连接板卡,可以播放视频流';
addLog('success', `成功获取HDMI视频流端点板卡ID: ${result.boardId.substring(0, 8)}...`);
alert?.success('HDMI视频流连接成功');
} else {
endpoint.value = null;
videoStatus.value = '无法获取板卡信息';
addLog('error', '未找到绑定的板卡或板卡未配置HDMI输入');
alert?.error('未找到绑定的板卡');
}
} catch (error) {
console.error('获取HDMI视频流端点失败:', error);
endpoint.value = null;
videoStatus.value = '连接失败';
addLog('error', `获取HDMI视频流端点失败: ${error}`);
alert?.error('获取HDMI视频流信息失败');
} finally {
loading.value = false;
}
}
// 测试连接
async function testConnection() {
if (!endpoint.value) {
alert?.warn('请先刷新连接获取板卡信息');
return;
}
testing.value = true;
try {
addLog('info', '正在测试HDMI视频流连接...');
// 尝试获取快照来测试连接
const response = await fetch(endpoint.value.snapshotUrl, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache'
}
});
if (response.ok) {
addLog('success', 'HDMI视频流连接测试成功');
alert?.success('HDMI连接测试成功');
videoStatus.value = '连接正常,可以播放视频流';
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
console.error('HDMI视频流连接测试失败:', error);
addLog('error', `连接测试失败: ${error}`);
alert?.error('HDMI连接测试失败');
videoStatus.value = '连接测试失败';
} finally {
testing.value = false;
}
}
// 开始播放视频流
function startStream() {
if (!endpoint.value) {
alert?.warn('请先刷新连接获取板卡信息');
return;
}
try {
// 添加时间戳防止缓存
const timestamp = new Date().getTime();
currentVideoSource.value = `${endpoint.value.mjpegUrl}&t=${timestamp}`;
isPlaying.value = true;
hasVideoError.value = false;
videoStatus.value = '正在加载视频流...';
addLog('info', '开始播放HDMI视频流');
alert?.success('开始播放HDMI视频流');
} catch (error) {
console.error('启动HDMI视频流失败:', error);
addLog('error', `启动视频流失败: ${error}`);
alert?.error('启动HDMI视频流失败');
}
}
// 停止播放视频流
function stopStream() {
isPlaying.value = false;
currentVideoSource.value = '';
videoStatus.value = '已停止播放';
const client = AuthManager.createAuthenticatedHdmiVideoStreamClient();
client.disableHdmiTransmission();
addLog('info', '停止播放HDMI视频流');
alert?.info('已停止播放HDMI视频流');
}
// 处理视频加载错误
function handleVideoError() {
hasVideoError.value = true;
videoStatus.value = '视频流加载失败';
addLog('error', 'HDMI视频流加载失败');
}
// 处理视频加载成功
function handleVideoLoad() {
hasVideoError.value = false;
videoStatus.value = '视频流播放中';
addLog('success', 'HDMI视频流加载成功');
}
// 处理视频点击
function handleVideoClick() {
if (!isPlaying.value || hasVideoError.value || !endpoint.value) {
return;
}
// 可以在这里添加点击视频的交互逻辑
addLog('info', '视频画面被点击');
}
// 重试连接
function tryReconnect() {
hasVideoError.value = false;
if (endpoint.value) {
startStream();
}
}
// 在新标签页打开视频
function openInNewTab(url: string) {
window.open(url, '_blank');
addLog('info', '在新标签页打开HDMI视频页面');
}
// 获取快照
async function takeSnapshot() {
if (!endpoint.value) {
alert?.warn('请先刷新连接获取板卡信息');
return;
}
try {
addLog('info', '正在获取HDMI视频快照...');
const response = await fetch(endpoint.value.snapshotUrl, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache'
}
});
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `hdmi_snapshot_${new Date().toISOString().replace(/:/g, '-')}.jpg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
addLog('success', '快照下载成功');
alert?.success('HDMI快照下载成功');
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
console.error('获取HDMI快照失败:', error);
addLog('error', `获取快照失败: ${error}`);
alert?.error('获取HDMI快照失败');
}
}
// 复制到剪贴板
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
addLog('success', '地址已复制到剪贴板');
alert?.success('地址已复制到剪贴板');
} catch (error) {
console.error('复制到剪贴板失败:', error);
addLog('error', '复制到剪贴板失败');
alert?.error('复制到剪贴板失败');
}
}
// 组件挂载时初始化
onMounted(() => {
addLog('info', 'HDMI视频流界面已初始化');
refreshEndpoint();
});
// 组件卸载时清理
onUnmounted(() => {
stopStream();
});
</script>
<style scoped>
/* 对焦动画效果 */
@keyframes focus-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
}
50% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
}
.focus-animation {
animation: focus-pulse 1s ease-out;
}
</style>

View File

@@ -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">
<!-- 状态指示 --> <!-- 状态指示 -->

View File

@@ -53,10 +53,6 @@ export default defineConfig({
target: "http://localhost:5000", target: "http://localhost:5000",
changeOrigin: true, changeOrigin: true,
}, },
"/hubs": {
target: "http://localhost:5000",
changeOrigin: true,
},
}, },
port: 5173, port: 5173,
}, },