feat: 添加下载进度条
This commit is contained in:
		@@ -303,11 +303,8 @@ async function generateApiClient(): Promise<void> {
 | 
			
		||||
async function generateSignalRClient(): Promise<void> {
 | 
			
		||||
  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);
 | 
			
		||||
 
 | 
			
		||||
@@ -283,4 +283,28 @@ public class NumberTest
 | 
			
		||||
        var reversed2 = Number.ReverseBits(new byte[0]);
 | 
			
		||||
        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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										99
									
								
								server.test/ProgressTrackerTest.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								server.test/ProgressTrackerTest.cs
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
    <PackageReference Include="coverlet.collector" Version="6.0.2" />
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
 | 
			
		||||
    <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.runner.visualstudio" Version="2.8.2" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -148,6 +148,10 @@ try
 | 
			
		||||
    builder.Services.AddSingleton<HttpHdmiVideoStreamService>();
 | 
			
		||||
    builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
 | 
			
		||||
 | 
			
		||||
    // 添加进度跟踪服务
 | 
			
		||||
    builder.Services.AddSingleton<ProgressTrackerService>();
 | 
			
		||||
    builder.Services.AddHostedService(provider => provider.GetRequiredService<ProgressTrackerService>());
 | 
			
		||||
 | 
			
		||||
    // Application Settings
 | 
			
		||||
    var app = builder.Build();
 | 
			
		||||
    // Configure the HTTP request pipeline.
 | 
			
		||||
@@ -217,7 +221,8 @@ try
 | 
			
		||||
 | 
			
		||||
    // Router
 | 
			
		||||
    app.MapControllers();
 | 
			
		||||
    app.MapHub<server.Hubs.JtagHub.JtagHub>("hubs/JtagHub");
 | 
			
		||||
    app.MapHub<server.Hubs.JtagHub>("hubs/JtagHub");
 | 
			
		||||
    app.MapHub<server.Hubs.ProgressHub>("hubs/ProgressHub");
 | 
			
		||||
 | 
			
		||||
    // Setup Program
 | 
			
		||||
    MsgBus.Init();
 | 
			
		||||
 
 | 
			
		||||
@@ -348,4 +348,37 @@ public class Number
 | 
			
		||||
        }
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 控制器首页信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -117,14 +127,14 @@ public class JtagController : ControllerBase
 | 
			
		||||
    /// <param name="address">JTAG 设备地址</param>
 | 
			
		||||
    /// <param name="port">JTAG 设备端口</param>
 | 
			
		||||
    /// <param name="bitstreamId">比特流ID</param>
 | 
			
		||||
    /// <returns>下载结果</returns>
 | 
			
		||||
    /// <returns>进度跟踪TaskID</returns>
 | 
			
		||||
    [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<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}");
 | 
			
		||||
 | 
			
		||||
@@ -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)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								server/src/Hubs/ProgressHub.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								server/src/Hubs/ProgressHub.cs
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<Result<bool>> WriteFIFO
 | 
			
		||||
        (UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
 | 
			
		||||
    async ValueTask<Result<bool>> 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<Result<bool>> WriteFIFO
 | 
			
		||||
        (UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
 | 
			
		||||
    async ValueTask<Result<bool>> 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<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));
 | 
			
		||||
        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
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="bitstream">比特流数据</param>
 | 
			
		||||
    /// <returns>指示下载是否成功的异步结果</returns>
 | 
			
		||||
    public async ValueTask<Result<bool>> DownloadBitstream(byte[] bitstream)
 | 
			
		||||
    public async ValueTask<Result<bool>> 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<bool> 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										288
									
								
								server/src/Services/ProgressTrackerService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								server/src/Services/ProgressTrackerService.cs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,6 +3,7 @@ using System.Net.Sockets;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using WebProtocol;
 | 
			
		||||
using server.Services;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 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
 | 
			
		||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
			
		||||
    /// <returns>写入结果,true表示写入成功</returns>
 | 
			
		||||
    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 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
 | 
			
		||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
			
		||||
    /// <returns>写入结果,true表示写入成功</returns>
 | 
			
		||||
    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 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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<boolean> {
 | 
			
		||||
    downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise<string> {
 | 
			
		||||
        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<boolean> {
 | 
			
		||||
    protected processDownloadBitstream(response: AxiosResponse): Promise<string> {
 | 
			
		||||
        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 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return Promise.resolve<boolean>(result200);
 | 
			
		||||
            return Promise.resolve<string>(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<boolean>(null as any);
 | 
			
		||||
        return Promise.resolve<string>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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<IJtagHub>;
 | 
			
		||||
    (hubType: "IProgressHub"): HubProxyFactory<IProgressHub>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<IJtagReceiver>;
 | 
			
		||||
    (receiverType: "IProgressReceiver"): ReceiverRegister<IProgressReceiver>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<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
 | 
			
		||||
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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<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 = {
 | 
			
		||||
    /**
 | 
			
		||||
    * @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>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IProgressReceiver = {
 | 
			
		||||
    /**
 | 
			
		||||
    * @param message Transpiled from server.Hubs.ProgressInfo
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task
 | 
			
		||||
    */
 | 
			
		||||
    onReceiveProgress(message: ProgressInfo): Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1,276 +1,314 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex flex-col bg-base-100 justify-center items-center gap-4">
 | 
			
		||||
    <!-- Title -->
 | 
			
		||||
    <h1 class="font-bold text-2xl">比特流文件</h1>
 | 
			
		||||
 | 
			
		||||
    <!-- 示例比特流下载区域 (仅在有examId时显示) -->
 | 
			
		||||
    <div v-if="examId && availableBitstreams.length > 0" class="w-full">
 | 
			
		||||
      <fieldset class="fieldset w-full">
 | 
			
		||||
        <legend class="fieldset-legend text-sm">示例比特流文件</legend>
 | 
			
		||||
        <div class="space-y-2">
 | 
			
		||||
          <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>
 | 
			
		||||
            <div class="flex gap-2">
 | 
			
		||||
              <button 
 | 
			
		||||
                @click="downloadExampleBitstream(bitstream)" 
 | 
			
		||||
                class="btn btn-sm btn-secondary"
 | 
			
		||||
                :disabled="isDownloading || isProgramming"
 | 
			
		||||
              >
 | 
			
		||||
                <div v-if="isDownloading">
 | 
			
		||||
                  <span class="loading loading-spinner loading-xs"></span>
 | 
			
		||||
                  下载中...
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-else>
 | 
			
		||||
                  下载示例
 | 
			
		||||
                </div>
 | 
			
		||||
              </button>
 | 
			
		||||
              <button 
 | 
			
		||||
                @click="programExampleBitstream(bitstream)" 
 | 
			
		||||
                class="btn btn-sm btn-primary"
 | 
			
		||||
                :disabled="isDownloading || isProgramming || !uploadEvent"
 | 
			
		||||
              >
 | 
			
		||||
                <div v-if="isProgramming">
 | 
			
		||||
                  <span class="loading loading-spinner loading-xs"></span>
 | 
			
		||||
                  烧录中...
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-else>
 | 
			
		||||
                  直接烧录
 | 
			
		||||
                </div>
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 分割线 -->
 | 
			
		||||
    <div v-if="examId && availableBitstreams.length > 0" class="divider">或</div>
 | 
			
		||||
 | 
			
		||||
    <!-- Input File -->
 | 
			
		||||
    <fieldset class="fieldset w-full">
 | 
			
		||||
      <legend class="fieldset-legend text-sm">上传自定义比特流文件</legend>
 | 
			
		||||
      <input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
 | 
			
		||||
      <label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
 | 
			
		||||
    </fieldset>
 | 
			
		||||
 | 
			
		||||
    <!-- Upload Button -->
 | 
			
		||||
    <div class="card-actions w-full">
 | 
			
		||||
      <button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading || isProgramming">
 | 
			
		||||
        <div v-if="isUploading">
 | 
			
		||||
          <span class="loading loading-spinner"></span>
 | 
			
		||||
          上传中...
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else>
 | 
			
		||||
          {{ buttonText }}
 | 
			
		||||
        </div>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref, useTemplateRef, onMounted } from "vue";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
 | 
			
		||||
import { useDialogStore } from "@/stores/dialog";
 | 
			
		||||
import { isNull, isUndefined } from "lodash";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  uploadEvent?: (file: File, examId: string) => Promise<number | null>;
 | 
			
		||||
  downloadEvent?: (bitstreamId: number) => Promise<boolean>;
 | 
			
		||||
  maxMemory?: number;
 | 
			
		||||
  examId?: string; // 新增examId属性
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  maxMemory: 4,
 | 
			
		||||
  examId: '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits<{
 | 
			
		||||
  finishedUpload: [file: File];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialog = useDialogStore();
 | 
			
		||||
 | 
			
		||||
const isUploading = ref(false);
 | 
			
		||||
const isDownloading = ref(false);
 | 
			
		||||
const isProgramming = ref(false);
 | 
			
		||||
const availableBitstreams = ref<{id: number, name: string}[]>([]);
 | 
			
		||||
 | 
			
		||||
const buttonText = computed(() => {
 | 
			
		||||
  return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const fileInput = useTemplateRef("fileInput");
 | 
			
		||||
const bitstream = defineModel("bitstreamFile", {
 | 
			
		||||
  type: File,
 | 
			
		||||
  default: undefined,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 初始化时加载示例比特流
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
 | 
			
		||||
    let fileList = new DataTransfer();
 | 
			
		||||
    fileList.items.add(bitstream.value);
 | 
			
		||||
    fileInput.value.files = fileList.files;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  await loadAvailableBitstreams();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 加载可用的比特流文件列表
 | 
			
		||||
async function loadAvailableBitstreams() {
 | 
			
		||||
  console.log('加载可用比特流文件,examId:', props.examId);
 | 
			
		||||
  if (!props.examId) {
 | 
			
		||||
    availableBitstreams.value = [];
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
			
		||||
    // 使用新的ResourceClient API获取比特流模板资源列表
 | 
			
		||||
    const resources = await resourceClient.getResourceList(props.examId, 'bitstream', 'template');
 | 
			
		||||
    availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || [];
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('加载比特流列表失败:', error);
 | 
			
		||||
    availableBitstreams.value = [];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 下载示例比特流
 | 
			
		||||
async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
 | 
			
		||||
  if (isDownloading.value) return;
 | 
			
		||||
  
 | 
			
		||||
  isDownloading.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
			
		||||
    
 | 
			
		||||
    // 使用新的ResourceClient API获取资源文件
 | 
			
		||||
    const response = await resourceClient.getResourceById(bitstream.id);
 | 
			
		||||
    
 | 
			
		||||
    if (response && response.data) {
 | 
			
		||||
      // 创建下载链接
 | 
			
		||||
      const url = URL.createObjectURL(response.data);
 | 
			
		||||
      const link = document.createElement('a');
 | 
			
		||||
      link.href = url;
 | 
			
		||||
      link.download = response.fileName || bitstream.name;
 | 
			
		||||
      document.body.appendChild(link);
 | 
			
		||||
      link.click();
 | 
			
		||||
      document.body.removeChild(link);
 | 
			
		||||
      URL.revokeObjectURL(url);
 | 
			
		||||
      
 | 
			
		||||
      dialog.info("示例比特流下载成功");
 | 
			
		||||
    } else {
 | 
			
		||||
      dialog.error("下载失败:响应数据为空");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('下载示例比特流失败:', error);
 | 
			
		||||
    dialog.error("下载示例比特流失败");
 | 
			
		||||
  } finally {
 | 
			
		||||
    isDownloading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 直接烧录示例比特流
 | 
			
		||||
async function programExampleBitstream(bitstream: {id: number, name: string}) {
 | 
			
		||||
  if (isProgramming.value) return;
 | 
			
		||||
  
 | 
			
		||||
  isProgramming.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
			
		||||
 | 
			
		||||
    if (props.downloadEvent) {
 | 
			
		||||
      const downloadSuccess = await props.downloadEvent(bitstream.id);
 | 
			
		||||
      if (downloadSuccess) {
 | 
			
		||||
        dialog.info("示例比特流烧录成功");
 | 
			
		||||
      } else {
 | 
			
		||||
        dialog.error("烧录失败");
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      dialog.info("示例比特流props.downloadEvent未定义 无法烧录");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('烧录示例比特流失败:', error);
 | 
			
		||||
    dialog.error("烧录示例比特流失败");
 | 
			
		||||
  } finally {
 | 
			
		||||
    isProgramming.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleFileChange(event: Event): void {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  const file = target.files?.[0]; // 获取选中的第一个文件
 | 
			
		||||
 | 
			
		||||
  if (!file) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bitstream.value = file;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkFile(file: File): boolean {
 | 
			
		||||
  const maxBytes = props.maxMemory! * 1024 * 1024; // 将最大容量从 MB 转换为字节
 | 
			
		||||
  if (file.size > maxBytes) {
 | 
			
		||||
    dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function handleClick(event: Event): Promise<void> {
 | 
			
		||||
  console.log("上传按钮被点击");
 | 
			
		||||
  if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
 | 
			
		||||
    dialog.error(`未选择文件`);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!checkFile(bitstream.value)) return;
 | 
			
		||||
  if (isUndefined(props.uploadEvent)) {
 | 
			
		||||
    dialog.error("无法上传");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isUploading.value = true;
 | 
			
		||||
  let uploadedBitstreamId: number | null = null;
 | 
			
		||||
  try {
 | 
			
		||||
    console.log("开始上传比特流文件:", bitstream.value.name);
 | 
			
		||||
    const bitstreamId = await props.uploadEvent(bitstream.value, props.examId || '');
 | 
			
		||||
    console.log("上传结果,ID:", bitstreamId);
 | 
			
		||||
    if (isUndefined(props.downloadEvent)) {
 | 
			
		||||
      console.log("上传成功,下载未定义");
 | 
			
		||||
      isUploading.value = false;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (bitstreamId === null || bitstreamId === undefined) {
 | 
			
		||||
      isUploading.value = false;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    uploadedBitstreamId = bitstreamId;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    dialog.error("上传失败");
 | 
			
		||||
    console.error(e);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Download
 | 
			
		||||
  try {
 | 
			
		||||
    console.log("开始下载比特流,ID:", uploadedBitstreamId);
 | 
			
		||||
    if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
 | 
			
		||||
      dialog.error("uploadedBitstreamId is null or undefined");
 | 
			
		||||
    } else {
 | 
			
		||||
      const ret = await props.downloadEvent(uploadedBitstreamId);
 | 
			
		||||
      if (ret) dialog.info("下载成功");
 | 
			
		||||
      else dialog.error("下载失败");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    dialog.error("下载失败");
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isUploading.value = false;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
@import "../assets/main.css";
 | 
			
		||||
</style>
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex flex-col bg-base-100 justify-center items-center gap-4">
 | 
			
		||||
    <!-- Title -->
 | 
			
		||||
    <h1 class="font-bold text-2xl">比特流文件</h1>
 | 
			
		||||
 | 
			
		||||
    <!-- 示例比特流下载区域 (仅在有examId时显示) -->
 | 
			
		||||
    <div v-if="examId && availableBitstreams.length > 0" class="w-full">
 | 
			
		||||
      <fieldset class="fieldset w-full">
 | 
			
		||||
        <legend class="fieldset-legend text-sm">示例比特流文件</legend>
 | 
			
		||||
        <div class="space-y-2">
 | 
			
		||||
          <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>
 | 
			
		||||
            <div class="flex gap-2">
 | 
			
		||||
              <button
 | 
			
		||||
                @click="downloadExampleBitstream(bitstream)"
 | 
			
		||||
                class="btn btn-sm btn-secondary"
 | 
			
		||||
                :disabled="isDownloading || isProgramming"
 | 
			
		||||
              >
 | 
			
		||||
                <div v-if="isDownloading">
 | 
			
		||||
                  <span class="loading loading-spinner loading-xs"></span>
 | 
			
		||||
                  {{ downloadProgress }}%
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-else>下载示例</div>
 | 
			
		||||
              </button>
 | 
			
		||||
              <button
 | 
			
		||||
                @click="programExampleBitstream(bitstream)"
 | 
			
		||||
                class="btn btn-sm btn-primary"
 | 
			
		||||
                :disabled="isDownloading || isProgramming"
 | 
			
		||||
              >
 | 
			
		||||
                <div v-if="isProgramming">
 | 
			
		||||
                  <span class="loading loading-spinner loading-xs"></span>
 | 
			
		||||
                  烧录中...
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-else>直接烧录</div>
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 分割线 -->
 | 
			
		||||
    <div v-if="examId && availableBitstreams.length > 0" class="divider">
 | 
			
		||||
      或
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Input File -->
 | 
			
		||||
    <fieldset class="fieldset w-full">
 | 
			
		||||
      <legend class="fieldset-legend text-sm">上传自定义比特流文件</legend>
 | 
			
		||||
      <input
 | 
			
		||||
        type="file"
 | 
			
		||||
        ref="fileInput"
 | 
			
		||||
        class="file-input w-full"
 | 
			
		||||
        @change="handleFileChange"
 | 
			
		||||
      />
 | 
			
		||||
      <label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
 | 
			
		||||
    </fieldset>
 | 
			
		||||
 | 
			
		||||
    <!-- Upload Button -->
 | 
			
		||||
    <div class="card-actions w-full">
 | 
			
		||||
      <button
 | 
			
		||||
        @click="handleClick"
 | 
			
		||||
        class="btn btn-primary grow"
 | 
			
		||||
        :disabled="isUploading || isProgramming"
 | 
			
		||||
      >
 | 
			
		||||
        <div v-if="isUploading">
 | 
			
		||||
          <span class="loading loading-spinner"></span>
 | 
			
		||||
          上传中...
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else>上传并下载</div>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref, useTemplateRef, onMounted } from "vue";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
 | 
			
		||||
import { useDialogStore } from "@/stores/dialog";
 | 
			
		||||
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 {
 | 
			
		||||
  maxMemory?: number;
 | 
			
		||||
  examId?: string; // 新增examId属性
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  maxMemory: 4,
 | 
			
		||||
  examId: "",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits<{
 | 
			
		||||
  finishedUpload: [file: File];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const alert = useRequiredInjection(useAlertStore);
 | 
			
		||||
const dialog = useDialogStore();
 | 
			
		||||
const eqps = useEquipments();
 | 
			
		||||
 | 
			
		||||
const isUploading = ref(false);
 | 
			
		||||
const isDownloading = ref(false);
 | 
			
		||||
const isProgramming = ref(false);
 | 
			
		||||
const availableBitstreams = ref<{ id: number; name: string }[]>([]);
 | 
			
		||||
 | 
			
		||||
// Progress
 | 
			
		||||
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 bitstream = defineModel("bitstreamFile", {
 | 
			
		||||
  type: File,
 | 
			
		||||
  default: undefined,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 初始化时加载示例比特流
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
 | 
			
		||||
    let fileList = new DataTransfer();
 | 
			
		||||
    fileList.items.add(bitstream.value);
 | 
			
		||||
    fileInput.value.files = fileList.files;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await loadAvailableBitstreams();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 加载可用的比特流文件列表
 | 
			
		||||
async function loadAvailableBitstreams() {
 | 
			
		||||
  console.log("加载可用比特流文件,examId:", props.examId);
 | 
			
		||||
  if (!props.examId) {
 | 
			
		||||
    availableBitstreams.value = [];
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
			
		||||
    // 使用新的ResourceClient API获取比特流模板资源列表
 | 
			
		||||
    const resources = await resourceClient.getResourceList(
 | 
			
		||||
      props.examId,
 | 
			
		||||
      "bitstream",
 | 
			
		||||
      "template",
 | 
			
		||||
    );
 | 
			
		||||
    availableBitstreams.value =
 | 
			
		||||
      resources.map((r) => ({ id: r.id, name: r.name })) || [];
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("加载比特流列表失败:", error);
 | 
			
		||||
    availableBitstreams.value = [];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 下载示例比特流
 | 
			
		||||
async function downloadExampleBitstream(bitstream: {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
}) {
 | 
			
		||||
  if (isDownloading.value) return;
 | 
			
		||||
 | 
			
		||||
  isDownloading.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
			
		||||
 | 
			
		||||
    // 使用新的ResourceClient API获取资源文件
 | 
			
		||||
    const response = await resourceClient.getResourceById(bitstream.id);
 | 
			
		||||
 | 
			
		||||
    if (response && response.data) {
 | 
			
		||||
      // 创建下载链接
 | 
			
		||||
      const url = URL.createObjectURL(response.data);
 | 
			
		||||
      const link = document.createElement("a");
 | 
			
		||||
      link.href = url;
 | 
			
		||||
      link.download = response.fileName || bitstream.name;
 | 
			
		||||
      document.body.appendChild(link);
 | 
			
		||||
      link.click();
 | 
			
		||||
      document.body.removeChild(link);
 | 
			
		||||
      URL.revokeObjectURL(url);
 | 
			
		||||
 | 
			
		||||
      dialog.info("示例比特流下载成功");
 | 
			
		||||
    } else {
 | 
			
		||||
      dialog.error("下载失败:响应数据为空");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("下载示例比特流失败:", error);
 | 
			
		||||
    dialog.error("下载示例比特流失败");
 | 
			
		||||
  } finally {
 | 
			
		||||
    isDownloading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 直接烧录示例比特流
 | 
			
		||||
async function programExampleBitstream(bitstream: {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
}) {
 | 
			
		||||
  if (isProgramming.value) return;
 | 
			
		||||
 | 
			
		||||
  isProgramming.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    const downloadTaskId = await eqps.jtagDownloadBitstream(bitstream.id);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("烧录示例比特流失败:", error);
 | 
			
		||||
    dialog.error("烧录示例比特流失败");
 | 
			
		||||
  } finally {
 | 
			
		||||
    isProgramming.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleFileChange(event: Event): void {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  const file = target.files?.[0]; // 获取选中的第一个文件
 | 
			
		||||
 | 
			
		||||
  if (!file) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bitstream.value = file;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkFile(file: File): boolean {
 | 
			
		||||
  const maxBytes = props.maxMemory! * 1024 * 1024; // 将最大容量从 MB 转换为字节
 | 
			
		||||
  if (file.size > maxBytes) {
 | 
			
		||||
    dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function handleClick(event: Event): Promise<void> {
 | 
			
		||||
  console.log("上传按钮被点击");
 | 
			
		||||
  if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
 | 
			
		||||
    dialog.error(`未选择文件`);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!checkFile(bitstream.value)) return;
 | 
			
		||||
 | 
			
		||||
  isUploading.value = true;
 | 
			
		||||
  let uploadedBitstreamId: number | null = null;
 | 
			
		||||
  try {
 | 
			
		||||
    console.log("开始上传比特流文件:", bitstream.value.name);
 | 
			
		||||
    const bitstreamId = await eqps.jtagUploadBitstream(
 | 
			
		||||
      bitstream.value,
 | 
			
		||||
      props.examId || "",
 | 
			
		||||
    );
 | 
			
		||||
    console.log("上传结果,ID:", bitstreamId);
 | 
			
		||||
    if (bitstreamId === null || bitstreamId === undefined) {
 | 
			
		||||
      isUploading.value = false;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    uploadedBitstreamId = bitstreamId;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    dialog.error("上传失败");
 | 
			
		||||
    console.error(e);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  isUploading.value = false;
 | 
			
		||||
 | 
			
		||||
  // Download
 | 
			
		||||
  try {
 | 
			
		||||
    console.log("开始下载比特流,ID:", uploadedBitstreamId);
 | 
			
		||||
    if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
 | 
			
		||||
      dialog.error("uploadedBitstreamId is null or undefined");
 | 
			
		||||
    } else {
 | 
			
		||||
      isDownloading.value = true;
 | 
			
		||||
      downloadTaskId.value =
 | 
			
		||||
        await eqps.jtagDownloadBitstream(uploadedBitstreamId);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    dialog.error("下载失败");
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
@import "../assets/main.css";
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,6 @@
 | 
			
		||||
    <UploadCard
 | 
			
		||||
      :exam-id="props.examId"
 | 
			
		||||
      :upload-event="eqps.jtagUploadBitstream"
 | 
			
		||||
      :download-event="handleDownloadBitstream"
 | 
			
		||||
      :bitstream-file="eqps.jtagBitstream"
 | 
			
		||||
      @update:bitstream-file="handleBitstreamChange"
 | 
			
		||||
    >
 | 
			
		||||
@@ -128,11 +127,6 @@ function handleBitstreamChange(file: File | undefined) {
 | 
			
		||||
  eqps.jtagBitstream = file;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function handleDownloadBitstream(bitstreamId: number): Promise<boolean> {
 | 
			
		||||
  console.log("开始下载比特流,ID:", bitstreamId);
 | 
			
		||||
  return await eqps.jtagDownloadBitstream(bitstreamId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleSelectJtagSpeed(event: Event) {
 | 
			
		||||
  const target = event.target as HTMLSelectElement;
 | 
			
		||||
  eqps.jtagSetSpeed(target.selectedIndex);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								src/server.Hubs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/server.Hubs.ts
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -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<number | null> {
 | 
			
		||||
  async function jtagUploadBitstream(
 | 
			
		||||
    bitstream: File,
 | 
			
		||||
    examId?: string,
 | 
			
		||||
  ): Promise<number | null> {
 | 
			
		||||
    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<boolean> {
 | 
			
		||||
  async function jtagDownloadBitstream(bitstreamId?: number): Promise<string> {
 | 
			
		||||
    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();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user