From aff9da2a601e3e262d70db4e0d082ca46359a756 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Mon, 4 Aug 2025 19:59:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/GenerateWebAPI.ts | 5 +- server.test/NumberTest.cs | 24 + server.test/ProgressTrackerTest.cs | 99 +++ server.test/server.test.csproj | 1 + server/Program.cs | 7 +- server/src/Common/Number.cs | 33 + server/src/Controllers/JtagController.cs | 106 ++-- server/src/Hubs/JtagHub.cs | 3 +- server/src/Hubs/ProgressHub.cs | 61 ++ server/src/Peripherals/JtagClient.cs | 50 +- server/src/Services/ProgressTrackerService.cs | 288 +++++++++ server/src/UdpClientPool.cs | 18 +- src/APIClient.ts | 10 +- src/TypedSignalR.Client/index.ts | 53 +- ...{server.Hubs.JtagHub.ts => server.Hubs.ts} | 17 + src/components/UploadCard.vue | 590 ++++++++++-------- src/components/equipments/MotherBoardCaps.vue | 6 - src/server.Hubs.ts | 25 + src/stores/equipments.ts | 21 +- src/utils/AuthManager.ts | 17 +- 20 files changed, 1073 insertions(+), 361 deletions(-) create mode 100644 server.test/ProgressTrackerTest.cs create mode 100644 server/src/Hubs/ProgressHub.cs create mode 100644 server/src/Services/ProgressTrackerService.cs rename src/TypedSignalR.Client/{server.Hubs.JtagHub.ts => server.Hubs.ts} (66%) create mode 100644 src/server.Hubs.ts diff --git a/scripts/GenerateWebAPI.ts b/scripts/GenerateWebAPI.ts index 2595ee6..6358155 100644 --- a/scripts/GenerateWebAPI.ts +++ b/scripts/GenerateWebAPI.ts @@ -303,11 +303,8 @@ async function generateApiClient(): Promise { async function generateSignalRClient(): Promise { console.log("Generating SignalR TypeScript client..."); try { - // TypedSignalR.Client.TypeScript.Analyzer 会在编译时自动生成客户端 - // 我们只需要确保服务器项目构建一次即可生成 TypeScript 客户端 const { stdout, stderr } = await execAsync( - "dotnet build --configuration Release", - { cwd: "./server" } + "dotnet tsrts --project ./server/server.csproj --output ./src/", ); if (stdout) console.log(stdout); if (stderr) console.error(stderr); diff --git a/server.test/NumberTest.cs b/server.test/NumberTest.cs index dee0b42..ae8af7f 100644 --- a/server.test/NumberTest.cs +++ b/server.test/NumberTest.cs @@ -283,4 +283,28 @@ public class NumberTest var reversed2 = Number.ReverseBits(new byte[0]); Assert.Empty(reversed2); } + + /// + /// 测试 GetLength + /// + [Fact] + public void Test_GetLength() + { + Assert.Equal(5, Number.GetLength(12345)); + Assert.Equal(4, Number.GetLength(-123)); + Assert.Equal(1, Number.GetLength(0)); + } + + /// + /// 测试 IntPow + /// + [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)); + } } diff --git a/server.test/ProgressTrackerTest.cs b/server.test/ProgressTrackerTest.cs new file mode 100644 index 0000000..95b4bed --- /dev/null +++ b/server.test/ProgressTrackerTest.cs @@ -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>(); + 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); + } +} diff --git a/server.test/server.test.csproj b/server.test/server.test.csproj index fd3a754..f608c55 100644 --- a/server.test/server.test.csproj +++ b/server.test/server.test.csproj @@ -11,6 +11,7 @@ + diff --git a/server/Program.cs b/server/Program.cs index ee23ac0..a0f9858 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -148,6 +148,10 @@ try builder.Services.AddSingleton(); builder.Services.AddHostedService(provider => provider.GetRequiredService()); + // 添加进度跟踪服务 + builder.Services.AddSingleton(); + builder.Services.AddHostedService(provider => provider.GetRequiredService()); + // Application Settings var app = builder.Build(); // Configure the HTTP request pipeline. @@ -217,7 +221,8 @@ try // Router app.MapControllers(); - app.MapHub("hubs/JtagHub"); + app.MapHub("hubs/JtagHub"); + app.MapHub("hubs/ProgressHub"); // Setup Program MsgBus.Init(); diff --git a/server/src/Common/Number.cs b/server/src/Common/Number.cs index 7987df0..56a2266 100644 --- a/server/src/Common/Number.cs +++ b/server/src/Common/Number.cs @@ -348,4 +348,37 @@ public class Number } return dstBytes; } + + /// + /// 获取数字的长度 + /// + /// 数字 + /// 数字的长度 + public static int GetLength(int number) + { + // 将整数转换为字符串 + string numberString = number.ToString(); + + // 返回字符串的长度 + return numberString.Length; + } + + /// + /// 计算整形的幂 + /// + /// 底数 + /// 幂 + /// 计算结果 + 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; + } } diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index 458e8ba..349442e 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Database; +using server.Services; namespace server.Controllers; @@ -15,6 +16,15 @@ public class JtagController : ControllerBase { 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; + } + /// /// 控制器首页信息 /// @@ -117,14 +127,14 @@ public class JtagController : ControllerBase /// JTAG 设备地址 /// JTAG 设备端口 /// 比特流ID - /// 下载结果 + /// 进度跟踪TaskID [HttpPost("DownloadBitstream")] [EnableCors("Users")] - [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async ValueTask DownloadBitstream(string address, int port, int bitstreamId) + public async ValueTask 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}"); @@ -176,55 +186,67 @@ public class JtagController : ControllerBase logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes"); - // 定义缓冲区大小: 32KB - byte[] buffer = new byte[32 * 1024]; - byte[] revBuffer = new byte[32 * 1024]; - long totalBytesProcessed = 0; + // 定义进度跟踪 + var (taskId, progress) = _tracker.CreateTask(cancelToken); + progress.Report(10); - // 使用内存流处理文件 - using (var inputStream = new MemoryStream(fileBytes)) - using (var outputStream = new MemoryStream()) + _ = Task.Run(async () => { - int bytesRead; - while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - // 反转 32bits - var retBuffer = Common.Number.ReverseBytes(buffer, 4); - if (!retBuffer.IsSuccessful) - { - logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}"); - return TypedResults.InternalServerError(retBuffer.Error); - } - revBuffer = retBuffer.Value; + // 定义缓冲区大小: 32KB + byte[] buffer = new byte[32 * 1024]; + byte[] revBuffer = new byte[32 * 1024]; + long totalBytesProcessed = 0; - for (int i = 0; i < revBuffer.Length; i++) + // 使用内存流处理文件 + using (var inputStream = new MemoryStream(fileBytes)) + using (var outputStream = new MemoryStream()) + { + int bytesRead; + while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { - revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]); + // 反转 32bits + var retBuffer = Common.Number.ReverseBytes(buffer, 4); + if (!retBuffer.IsSuccessful) + { + logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}"); + progress.Error($"User {username} failed to reverse bytes: {retBuffer.Error}"); + return; + } + revBuffer = retBuffer.Value; + + for (int i = 0; i < revBuffer.Length; i++) + { + revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]); + } + + await outputStream.WriteAsync(revBuffer, 0, bytesRead); + totalBytesProcessed += bytesRead; } - await outputStream.WriteAsync(revBuffer, 0, bytesRead); - totalBytesProcessed += bytesRead; - } + // 获取处理后的数据 + var processedBytes = outputStream.ToArray(); + logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}"); - // 获取处理后的数据 - var processedBytes = outputStream.ToArray(); - logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}"); + progress.Report(20); - // 下载比特流 - var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); - var ret = await jtagCtrl.DownloadBitstream(processedBytes); + // 下载比特流 + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); + var ret = await jtagCtrl.DownloadBitstream(processedBytes); - if (ret.IsSuccessful) - { - logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}"); - return TypedResults.Ok(ret.Value); + if (ret.IsSuccessful) + { + logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}"); + progress.Finish(); + } + else + { + logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}"); + progress.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}"); + } } - else - { - logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}"); - return TypedResults.InternalServerError(ret.Error); - } - } + }); + + return TypedResults.Ok(taskId); } catch (Exception ex) { diff --git a/server/src/Hubs/JtagHub.cs b/server/src/Hubs/JtagHub.cs index 8274a8d..f768f87 100644 --- a/server/src/Hubs/JtagHub.cs +++ b/server/src/Hubs/JtagHub.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; -using Microsoft.AspNetCore.Mvc; using System.Security.Claims; using Microsoft.AspNetCore.SignalR; using DotNext; @@ -8,7 +7,7 @@ using System.Collections.Concurrent; using TypedSignalR.Client; using Tapper; -namespace server.Hubs.JtagHub; +namespace server.Hubs; [Hub] public interface IJtagHub diff --git a/server/src/Hubs/ProgressHub.cs b/server/src/Hubs/ProgressHub.cs new file mode 100644 index 0000000..4bfeaf4 --- /dev/null +++ b/server/src/Hubs/ProgressHub.cs @@ -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 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, IProgressHub +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + private readonly IHubContext _hubContext; + private readonly ProgressTrackerService _tracker; + + public ProgressHub(IHubContext hubContext, ProgressTrackerService tracker) + { + _hubContext = hubContext; + _tracker = tracker; + } + + public async Task Join(string taskId) + { + return _tracker.BindTask(taskId, Context.ConnectionId); + } +} diff --git a/server/src/Peripherals/JtagClient.cs b/server/src/Peripherals/JtagClient.cs index 6260a50..04b5dd4 100644 --- a/server/src/Peripherals/JtagClient.cs +++ b/server/src/Peripherals/JtagClient.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Net; using DotNext; using Newtonsoft.Json; -using server; +using server.Services; using WebProtocol; namespace Peripherals.JtagClient; @@ -442,11 +442,12 @@ public class Jtag return Convert.ToUInt32(Common.Number.BytesToUInt32(retPackOpts.Data).Value); } - async ValueTask> WriteFIFO - (UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0) + async ValueTask> WriteFIFO( + 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.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); if (!ret.IsSuccessful) return new(ret.Error); + progress?.Finish(); return ret.Value; } } - async ValueTask> WriteFIFO - (UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0) + async ValueTask> WriteFIFO( + 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.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); if (!ret.IsSuccessful) return new(ret.Error); + progress?.Finish(); return ret.Value; } } @@ -559,7 +563,8 @@ public class Jtag return await ClearWriteDataReg(); } - async ValueTask> LoadDRCareInput(byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500) + async ValueTask> LoadDRCareInput( + byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, ProgressReporter? progress = null) { var bytesLen = ((uint)(bytesArray.Length * 8)); 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")); } + progress?.Report(10); + { var ret = await WriteFIFO( JtagAddr.WRITE_DATA, bytesArray, 0x01_00_00_00, - JtagState.CMD_EXEC_FINISH); + JtagState.CMD_EXEC_FINISH, + progress: progress?.CreateChild(90) + ); if (!ret.IsSuccessful) return new(ret.Error); return ret.Value; @@ -701,44 +710,55 @@ public class Jtag /// /// 比特流数据 /// 指示下载是否成功的异步结果 - public async ValueTask> DownloadBitstream(byte[] bitstream) + public async ValueTask> DownloadBitstream(byte[] bitstream, ProgressReporter? progress = null) { // Clear Data MsgBus.UDPServer.ClearUDPData(this.address, 0); logger.Trace($"Clear up udp server {this.address,0} receive data"); + if (progress != null) + { + progress.ExpectedSteps = 25; + progress.Increase(); + } Result ret; ret = await CloseTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); + progress?.Increase(); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); + progress?.Increase(); logger.Trace("Jtag initialize"); ret = await ExecRDCmd(JtagCmd.JTAG_DR_JRST); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed")); + progress?.Increase(); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); + progress?.Increase(); ret = await ExecRDCmd(JtagCmd.JTAG_DR_CFGI); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_CFGI Failed")); + progress?.Increase(); logger.Trace("Jtag ready to write bitstream"); ret = await IdleDelay(100000); if (!ret.IsSuccessful) return new(ret.Error); 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); else if (!ret.Value) return new(new Exception("Jtag Load Data Failed")); @@ -747,32 +767,40 @@ public class Jtag ret = await CloseTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); + progress?.Increase(); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); + progress?.Increase(); ret = await ExecRDCmd(JtagCmd.JTAG_DR_JWAKEUP); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JWAKEUP Failed")); + progress?.Increase(); logger.Trace("Jtag reset device"); ret = await IdleDelay(10000); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed")); + progress?.Increase(); var retCode = await ReadStatusReg(); if (!retCode.IsSuccessful) return new(retCode.Error); var jtagStatus = new JtagStatusReg(retCode.Value); if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete)) return new(new Exception("Jtag download bitstream failed")); + progress?.Increase(); ret = await CloseTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); logger.Trace("Jtag download bitstream successfully"); + progress?.Increase(); + // Finish + progress?.Finish(); return true; } diff --git a/server/src/Services/ProgressTrackerService.cs b/server/src/Services/ProgressTrackerService.cs new file mode 100644 index 0000000..bf555a7 --- /dev/null +++ b/server/src/Services/ProgressTrackerService.cs @@ -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 +{ + 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? 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? 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? 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 _taskMap = new(); + private readonly IHubContext _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 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 GetReporter(string taskId) + { + if (_taskMap.TryGetValue(taskId, out var info)) + { + return info.Reporter; + } + return Optional.None; + } + + public Optional GetProgressStatus(string taskId) + { + if (_taskMap.TryGetValue(taskId, out var info)) + { + return info.Reporter.Status; + } + return Optional.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; + } + } +} diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index 2e8e712..3cc45b1 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -3,6 +3,7 @@ using System.Net.Sockets; using System.Text; using DotNext; using WebProtocol; +using server.Services; /// /// UDP客户端发送池 @@ -465,7 +466,7 @@ public class UDPClientPool CommandID = Convert.ToByte(taskID), IsWrite = false, BurstLength = (byte)(currentSegmentSize - 1), - Address = (burstType == BurstType.ExtendBurst)?(devAddr + (uint)(i * max4BytesPerRead)):(devAddr), + Address = (burstType == BurstType.ExtendBurst) ? (devAddr + (uint)(i * max4BytesPerRead)) : (devAddr), // Address = devAddr + (uint)(i * max4BytesPerRead), }; pkgList.Add(new SendAddrPackage(opts)); @@ -586,7 +587,8 @@ public class UDPClientPool /// 超时时间(毫秒) /// 写入结果,true表示写入成功 public static async ValueTask> 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 opts = new SendAddrPackOptions() @@ -597,14 +599,17 @@ public class UDPClientPool Address = devAddr, IsWrite = true, }; + progress?.Report(20); // Write Register ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts)); if (!ret) return new(new Exception("Send 1st address package failed!")); + progress?.Report(40); // Send Data Package ret = await UDPClientPool.SendDataPackAsync(endPoint, new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value)); if (!ret) return new(new Exception("Send data package failed!")); + progress?.Report(60); // Check Msg Bus if (!MsgBus.IsRunning) @@ -614,6 +619,7 @@ public class UDPClientPool var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync( endPoint.Address.ToString(), taskID, endPoint.Port, timeout); if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error); + progress?.Finish(); return udpWriteAck.Value.IsSuccessful; } @@ -628,7 +634,8 @@ public class UDPClientPool /// 超时时间(毫秒) /// 写入结果,true表示写入成功 public static async ValueTask> 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 opts = new SendAddrPackOptions() @@ -650,6 +657,8 @@ public class UDPClientPool var writeTimes = hasRest ? dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 : dataArray.Length / (max4BytesPerRead * (32 / 8)); + if (progress != null) + progress.ExpectedSteps = writeTimes; for (var i = 0; i < writeTimes; i++) { // Sperate Data Array @@ -678,8 +687,11 @@ public class UDPClientPool if (!udpWriteAck.Value.IsSuccessful) return false; + + progress?.Increase(); } + progress?.Finish(); return true; } diff --git a/src/APIClient.ts b/src/APIClient.ts index 62a6e23..047d49c 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -3331,9 +3331,9 @@ export class JtagClient { * @param address (optional) JTAG 设备地址 * @param port (optional) JTAG 设备端口 * @param bitstreamId (optional) 比特流ID - * @return 下载结果 + * @return 进度跟踪TaskID */ - downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise { + downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; if (address === null) throw new Error("The parameter 'address' cannot be null."); @@ -3369,7 +3369,7 @@ export class JtagClient { }); } - protected processDownloadBitstream(response: AxiosResponse): Promise { + protected processDownloadBitstream(response: AxiosResponse): Promise { const status = response.status; let _headers: any = {}; if (response.headers && typeof response.headers === "object") { @@ -3385,7 +3385,7 @@ export class JtagClient { let resultData200 = _responseText; result200 = resultData200 !== undefined ? resultData200 : null; - return Promise.resolve(result200); + return Promise.resolve(result200); } else if (status === 400) { const _responseText = response.data; @@ -3413,7 +3413,7 @@ export class JtagClient { const _responseText = response.data; return throwException("An unexpected server error occurred.", status, _responseText, _headers); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } /** diff --git a/src/TypedSignalR.Client/index.ts b/src/TypedSignalR.Client/index.ts index 393fcab..0468439 100644 --- a/src/TypedSignalR.Client/index.ts +++ b/src/TypedSignalR.Client/index.ts @@ -3,7 +3,8 @@ /* tslint:disable */ // @ts-nocheck 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 @@ -43,22 +44,30 @@ class ReceiverMethodSubscription implements Disposable { export type HubProxyFactoryProvider = { (hubType: "IJtagHub"): HubProxyFactory; + (hubType: "IProgressHub"): HubProxyFactory; } export const getHubProxyFactory = ((hubType: string) => { if(hubType === "IJtagHub") { return IJtagHub_HubProxyFactory.Instance; } + if(hubType === "IProgressHub") { + return IProgressHub_HubProxyFactory.Instance; + } }) as HubProxyFactoryProvider; export type ReceiverRegisterProvider = { (receiverType: "IJtagReceiver"): ReceiverRegister; + (receiverType: "IProgressReceiver"): ReceiverRegister; } export const getReceiverRegister = ((receiverType: string) => { if(receiverType === "IJtagReceiver") { return IJtagReceiver_Binder.Instance; } + if(receiverType === "IProgressReceiver") { + return IProgressReceiver_Binder.Instance; + } }) as ReceiverRegisterProvider; // HubProxy @@ -92,6 +101,27 @@ class IJtagHub_HubProxy implements IJtagHub { } } +class IProgressHub_HubProxyFactory implements HubProxyFactory { + 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 => { + return await this.connection.invoke("Join", taskId); + } +} + // Receiver @@ -116,3 +146,24 @@ class IJtagReceiver_Binder implements ReceiverRegister { } } +class IProgressReceiver_Binder implements ReceiverRegister { + + 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); + } +} + diff --git a/src/TypedSignalR.Client/server.Hubs.JtagHub.ts b/src/TypedSignalR.Client/server.Hubs.ts similarity index 66% rename from src/TypedSignalR.Client/server.Hubs.JtagHub.ts rename to src/TypedSignalR.Client/server.Hubs.ts index bee5c63..1c4e068 100644 --- a/src/TypedSignalR.Client/server.Hubs.JtagHub.ts +++ b/src/TypedSignalR.Client/server.Hubs.ts @@ -3,6 +3,7 @@ /* tslint:disable */ // @ts-nocheck import type { IStreamResult, Subject } from '@microsoft/signalr'; +import type { ProgressInfo } from '../server.Hubs'; export type IJtagHub = { /** @@ -21,6 +22,14 @@ export type IJtagHub = { stopBoundaryScan(): Promise; } +export type IProgressHub = { + /** + * @param taskId Transpiled from string + * @returns Transpiled from System.Threading.Tasks.Task + */ + join(taskId: string): Promise; +} + export type IJtagReceiver = { /** * @param msg Transpiled from System.Collections.Generic.Dictionary @@ -29,3 +38,11 @@ export type IJtagReceiver = { onReceiveBoundaryScanData(msg: Partial>): Promise; } +export type IProgressReceiver = { + /** + * @param message Transpiled from server.Hubs.ProgressInfo + * @returns Transpiled from System.Threading.Tasks.Task + */ + onReceiveProgress(message: ProgressInfo): Promise; +} + diff --git a/src/components/UploadCard.vue b/src/components/UploadCard.vue index aa4d8b5..a2207b9 100644 --- a/src/components/UploadCard.vue +++ b/src/components/UploadCard.vue @@ -1,276 +1,314 @@ - - - - - + + + + + diff --git a/src/components/equipments/MotherBoardCaps.vue b/src/components/equipments/MotherBoardCaps.vue index 7bcd18a..6b95528 100644 --- a/src/components/equipments/MotherBoardCaps.vue +++ b/src/components/equipments/MotherBoardCaps.vue @@ -24,7 +24,6 @@ @@ -128,11 +127,6 @@ function handleBitstreamChange(file: File | undefined) { eqps.jtagBitstream = file; } -async function handleDownloadBitstream(bitstreamId: number): Promise { - console.log("开始下载比特流,ID:", bitstreamId); - return await eqps.jtagDownloadBitstream(bitstreamId); -} - function handleSelectJtagSpeed(event: Event) { const target = event.target as HTMLSelectElement; eqps.jtagSetSpeed(target.selectedIndex); diff --git a/src/server.Hubs.ts b/src/server.Hubs.ts new file mode 100644 index 0000000..b20811e --- /dev/null +++ b/src/server.Hubs.ts @@ -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; +} + diff --git a/src/stores/equipments.ts b/src/stores/equipments.ts index f990971..225d17c 100644 --- a/src/stores/equipments.ts +++ b/src/stores/equipments.ts @@ -12,7 +12,7 @@ import { AuthManager } from "@/utils/AuthManager"; import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr"; import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client"; import type { ResourceInfo } from "@/APIClient"; -import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs.JtagHub"; +import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs"; export const useEquipments = defineStore("equipments", () => { // Global Stores @@ -123,24 +123,27 @@ export const useEquipments = defineStore("equipments", () => { enableJtagBoundaryScan.value = enable; } - async function jtagUploadBitstream(bitstream: File, examId?: string): Promise { + async function jtagUploadBitstream( + bitstream: File, + examId?: string, + ): Promise { try { // 自动开启电源 await powerSetOnOff(true); const resourceClient = AuthManager.createAuthenticatedResourceClient(); const resp = await resourceClient.addResource( - 'bitstream', - 'user', + "bitstream", + "user", examId || null, toFileParameterOrUndefined(bitstream), ); - + // 如果上传成功,设置为当前选中的比特流 if (resp && resp.id !== undefined && resp.id !== null) { return resp.id; } - + return null; } catch (e) { dialog.error("上传错误"); @@ -149,10 +152,10 @@ export const useEquipments = defineStore("equipments", () => { } } - async function jtagDownloadBitstream(bitstreamId?: number): Promise { + async function jtagDownloadBitstream(bitstreamId?: number): Promise { if (bitstreamId === null || bitstreamId === undefined) { dialog.error("请先选择要下载的比特流"); - return false; + return ""; } const release = await jtagClientMutex.acquire(); @@ -170,7 +173,7 @@ export const useEquipments = defineStore("equipments", () => { } catch (e) { dialog.error("下载错误"); console.error(e); - return false; + throw e; } finally { release(); } diff --git a/src/utils/AuthManager.ts b/src/utils/AuthManager.ts index 5e2173e..50c47b8 100644 --- a/src/utils/AuthManager.ts +++ b/src/utils/AuthManager.ts @@ -210,7 +210,7 @@ export class AuthManager { public static createAuthenticatedHdmiVideoStreamClient(): HdmiVideoStreamClient { return AuthManager.createAuthenticatedClient(HdmiVideoStreamClient); } - + public static createAuthenticatedJtagHubConnection() { const token = this.getToken(); if (isNull(token)) { @@ -226,6 +226,21 @@ export class AuthManager { .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( username: string,