feat: 完成七段数码管后端
This commit is contained in:
		@@ -31,7 +31,7 @@
 | 
			
		||||
    <PackageReference Include="NSwag.CodeGeneration.TypeScript" Version="14.4.0" />
 | 
			
		||||
    <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
 | 
			
		||||
    <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
 | 
			
		||||
    <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
 | 
			
		||||
    <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
 | 
			
		||||
    <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
 | 
			
		||||
    <PackageReference Include="Tapper.Analyzer" Version="1.13.1">
 | 
			
		||||
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,8 @@ public class NetConfigController : ControllerBase
 | 
			
		||||
    private const int BOARD_PORT = 1234;
 | 
			
		||||
 | 
			
		||||
    // 本机网络信息
 | 
			
		||||
    private readonly IPAddress _localIP;
 | 
			
		||||
    private readonly byte[] _localMAC;
 | 
			
		||||
    private readonly IPAddress _localIP = IPAddress.Any;
 | 
			
		||||
    private readonly byte[] _localMAC = new byte[6];
 | 
			
		||||
    private readonly string _localIPString;
 | 
			
		||||
    private readonly string _localMACString;
 | 
			
		||||
    private readonly string _localInterface;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								server/src/Hubs/DigitalTubesHub.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								server/src/Hubs/DigitalTubesHub.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
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 IDigitalTubesHub
 | 
			
		||||
{
 | 
			
		||||
    Task<bool> Join(string taskId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[Receiver]
 | 
			
		||||
public interface IDigitalTubesReceiver
 | 
			
		||||
{
 | 
			
		||||
    Task OnReceive();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[Authorize]
 | 
			
		||||
[EnableCors("SignalR")]
 | 
			
		||||
public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
			
		||||
{
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
 | 
			
		||||
 | 
			
		||||
    public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
 | 
			
		||||
    {
 | 
			
		||||
        _hubContext = hubContext;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Join(string taskId)
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -69,23 +69,26 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetBoundaryScanFreq(int freq)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        return await Task.Run(() =>
 | 
			
		||||
        {
 | 
			
		||||
            var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
 | 
			
		||||
            if (userName is null)
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("Can't get user info");
 | 
			
		||||
                var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
 | 
			
		||||
                if (userName is null)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error("Can't get user info");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                FreqTable.AddOrUpdate(userName, freq, (key, value) => freq);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception error)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error(error);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            FreqTable.AddOrUpdate(userName, freq, (key, value) => freq);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception error)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(error);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> StartBoundaryScan(int freq = 100)
 | 
			
		||||
@@ -99,7 +102,7 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            SetBoundaryScanFreq(freq);
 | 
			
		||||
            await SetBoundaryScanFreq(freq);
 | 
			
		||||
            var cts = new CancellationTokenSource();
 | 
			
		||||
            CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts);
 | 
			
		||||
 | 
			
		||||
@@ -145,23 +148,27 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> StopBoundaryScan()
 | 
			
		||||
    {
 | 
			
		||||
        var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
 | 
			
		||||
        if (userName is null)
 | 
			
		||||
        return await Task.Run(() =>
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("No Such User");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
            var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
 | 
			
		||||
            if (userName is null)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("No Such User");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        if (!CancellationTokenSourceTable.TryGetValue(userName, out var cts))
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
            if (!CancellationTokenSourceTable.TryGetValue(userName, out var cts))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        cts.Cancel();
 | 
			
		||||
        cts.Token.WaitHandle.WaitOne();
 | 
			
		||||
            cts.Cancel();
 | 
			
		||||
            cts.Token.WaitHandle.WaitOne();
 | 
			
		||||
 | 
			
		||||
            logger.Info($"Boundary scan stopped for user {userName}");
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        logger.Info($"Boundary scan stopped for user {userName}");
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task BoundaryScanLogicPorts(string connectionID, string userName, CancellationToken cancellationToken)
 | 
			
		||||
 
 | 
			
		||||
@@ -33,10 +33,10 @@ public enum ProgressStatus
 | 
			
		||||
[TranspilationSource]
 | 
			
		||||
public class ProgressInfo
 | 
			
		||||
{
 | 
			
		||||
    public string TaskId { get; }
 | 
			
		||||
    public ProgressStatus Status { get; }
 | 
			
		||||
    public int ProgressPercent { get; }
 | 
			
		||||
    public string ErrorMessage { get; }
 | 
			
		||||
    public virtual string TaskId { get; } = string.Empty;
 | 
			
		||||
    public virtual ProgressStatus Status { get; }
 | 
			
		||||
    public virtual int ProgressPercent { get; } = 0;
 | 
			
		||||
    public virtual string ErrorMessage { get; } = string.Empty;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
[Authorize]
 | 
			
		||||
@@ -56,6 +56,6 @@ public class ProgressHub : Hub<IProgressReceiver>, IProgressHub
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Join(string taskId)
 | 
			
		||||
    {
 | 
			
		||||
        return _tracker.BindTask(taskId, Context.ConnectionId);
 | 
			
		||||
        return await Task.Run(() => _tracker.BindTask(taskId, Context.ConnectionId));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ public static class MsgBus
 | 
			
		||||
    /// 通信总线初始化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>无</returns>
 | 
			
		||||
    public async static void Init()
 | 
			
		||||
    public static void Init()
 | 
			
		||||
    {
 | 
			
		||||
        if (!ArpClient.IsAdministrator())
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
using System.Net;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using Peripherals.PowerClient;
 | 
			
		||||
using WebProtocol;
 | 
			
		||||
 | 
			
		||||
namespace Peripherals.HdmiInClient;
 | 
			
		||||
 
 | 
			
		||||
@@ -709,8 +709,10 @@ public class Jtag
 | 
			
		||||
    /// 下载比特流到 JTAG 设备
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="bitstream">比特流数据</param>
 | 
			
		||||
    /// <param name="progress">进度报告器</param>
 | 
			
		||||
    /// <returns>指示下载是否成功的异步结果</returns>
 | 
			
		||||
    public async ValueTask<Result<bool>> DownloadBitstream(byte[] bitstream, ProgressReporter? progress = null)
 | 
			
		||||
    public async ValueTask<Result<bool>> DownloadBitstream(
 | 
			
		||||
        byte[] bitstream, ProgressReporter? progress = null)
 | 
			
		||||
    {
 | 
			
		||||
        // Clear Data
 | 
			
		||||
        MsgBus.UDPServer.ClearUDPData(this.address, 0);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										79
									
								
								server/src/Peripherals/SevenDigitalTubesClient.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								server/src/Peripherals/SevenDigitalTubesClient.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
using System.Net;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using Common;
 | 
			
		||||
 | 
			
		||||
namespace Peripherals.SevenDigitalTubesClient;
 | 
			
		||||
 | 
			
		||||
static class SevenDigitalTubesAddr
 | 
			
		||||
{
 | 
			
		||||
    public const UInt32 BASE = 0x0000_0000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class SevenDigitalTubesCtrl
 | 
			
		||||
{
 | 
			
		||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    readonly int timeout = 500;
 | 
			
		||||
    readonly int taskID;
 | 
			
		||||
    readonly int port;
 | 
			
		||||
    readonly string address;
 | 
			
		||||
    private IPEndPoint ep;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化七段数码管控制器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address">七段数码管控制器IP地址</param>
 | 
			
		||||
    /// <param name="port">七段数码管控制器端口</param>
 | 
			
		||||
    /// <param name="taskID">任务ID</param>
 | 
			
		||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
			
		||||
    public SevenDigitalTubesCtrl(string address, int port, int taskID, int timeout = 500)
 | 
			
		||||
    {
 | 
			
		||||
        if (timeout < 0)
 | 
			
		||||
            throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
 | 
			
		||||
        this.address = address;
 | 
			
		||||
        this.port = port;
 | 
			
		||||
        this.ep = new IPEndPoint(IPAddress.Parse(address), port);
 | 
			
		||||
        this.taskID = taskID;
 | 
			
		||||
        this.timeout = timeout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<Result<byte>> ReadTube(int num)
 | 
			
		||||
    {
 | 
			
		||||
        if (num < 0 || num > 31)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(num), "Tube number must be between 0 and 31");
 | 
			
		||||
 | 
			
		||||
        var ret = await UDPClientPool.ReadAddr(
 | 
			
		||||
            this.ep, this.taskID, SevenDigitalTubesAddr.BASE + (UInt32)num, this.timeout);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Read tubes failed: {ret.Error}");
 | 
			
		||||
            return new(ret.Error);
 | 
			
		||||
        }
 | 
			
		||||
        if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 4)
 | 
			
		||||
            return new(new Exception("Data length is too short"));
 | 
			
		||||
 | 
			
		||||
        var data = Number.BytesToUInt32(ret.Value.Options.Data, 0, 4, true).Value;
 | 
			
		||||
 | 
			
		||||
        if ((data >> 8) != num)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Read wrong tube number: {num} != {data >> 8}");
 | 
			
		||||
            return new(new Exception($"Read wrong tube number: {num} != {data >> 8}"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (byte)(data & 0xFF);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<Result<byte[]>> ScanTubes()
 | 
			
		||||
    {
 | 
			
		||||
        var tubes = new byte[32];
 | 
			
		||||
        for (int i = 0; i < 32; i++)
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await ReadTube(i);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            tubes[i] = ret.Value;
 | 
			
		||||
        }
 | 
			
		||||
        return tubes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -192,11 +192,6 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var hdmiInToken = _clientDict[boardId].CTS.Token;
 | 
			
		||||
        if (hdmiInToken == null)
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(context.Response, "HDMI input is not available");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (path == "/snapshot")
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -39,12 +39,12 @@ public class ProgressReporter : ProgressInfo, IProgress<int>
 | 
			
		||||
    public ProgressReporter? Child { get; set; }
 | 
			
		||||
 | 
			
		||||
    private ProgressStatus _status = ProgressStatus.Pending;
 | 
			
		||||
    private string _errorMessage;
 | 
			
		||||
    private string _errorMessage = string.Empty;
 | 
			
		||||
 | 
			
		||||
    public string TaskId { get; set; } = Guid.NewGuid().ToString();
 | 
			
		||||
    public int ProgressPercent => _progress * 100 / MaxProgress;
 | 
			
		||||
    public ProgressStatus Status => _status;
 | 
			
		||||
    public string ErrorMessage => _errorMessage;
 | 
			
		||||
    public override string TaskId { get; } = Guid.NewGuid().ToString();
 | 
			
		||||
    public override int ProgressPercent => _progress * 100 / MaxProgress;
 | 
			
		||||
    public override ProgressStatus Status => _status;
 | 
			
		||||
    public override string ErrorMessage => _errorMessage;
 | 
			
		||||
 | 
			
		||||
    public ProgressReporter(Func<int, Task>? reporter = null, int initProgress = 0, int maxProgress = 100, int step = 1)
 | 
			
		||||
    {
 | 
			
		||||
@@ -86,7 +86,7 @@ public class ProgressReporter : ProgressInfo, IProgress<int>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async void Report(int value)
 | 
			
		||||
    public void Report(int value)
 | 
			
		||||
    {
 | 
			
		||||
        if (this._status == ProgressStatus.Pending)
 | 
			
		||||
            this._status = ProgressStatus.InProgress;
 | 
			
		||||
@@ -153,7 +153,7 @@ public class ProgressTrackerService : BackgroundService
 | 
			
		||||
 | 
			
		||||
    private class TaskProgressInfo
 | 
			
		||||
    {
 | 
			
		||||
        public ProgressReporter Reporter { get; set; }
 | 
			
		||||
        public ProgressReporter? Reporter { get; set; }
 | 
			
		||||
        public string? ConnectionId { get; set; }
 | 
			
		||||
        public required CancellationToken CancellationToken { get; set; }
 | 
			
		||||
        public required CancellationTokenSource CancellationTokenSource { get; set; }
 | 
			
		||||
@@ -176,10 +176,15 @@ public class ProgressTrackerService : BackgroundService
 | 
			
		||||
                {
 | 
			
		||||
                    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))
 | 
			
		||||
                    if (
 | 
			
		||||
                        (now - info.UpdatedAt).TotalMinutes > 1 &&
 | 
			
		||||
                        info.Reporter != null &&
 | 
			
		||||
                        (
 | 
			
		||||
                            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}");
 | 
			
		||||
@@ -219,7 +224,7 @@ public class ProgressTrackerService : BackgroundService
 | 
			
		||||
            cancellationTokenSource.Token.ThrowIfCancellationRequested();
 | 
			
		||||
 | 
			
		||||
            // 通过 SignalR 推送进度
 | 
			
		||||
            if (progressInfo.ConnectionId != null)
 | 
			
		||||
            if (progressInfo.ConnectionId != null && progressInfo.Reporter != null)
 | 
			
		||||
                await _hubContext.Clients.Client(progressInfo.ConnectionId).OnReceiveProgress(progressInfo.Reporter);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -243,7 +248,8 @@ public class ProgressTrackerService : BackgroundService
 | 
			
		||||
    {
 | 
			
		||||
        if (_taskMap.TryGetValue(taskId, out var info))
 | 
			
		||||
        {
 | 
			
		||||
            return info.Reporter.Status;
 | 
			
		||||
            if (info.Reporter != null)
 | 
			
		||||
                return info.Reporter.Status;
 | 
			
		||||
        }
 | 
			
		||||
        return Optional<ProgressStatus>.None;
 | 
			
		||||
    }
 | 
			
		||||
@@ -265,7 +271,7 @@ public class ProgressTrackerService : BackgroundService
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (_taskMap.TryGetValue(taskId, out var info) && info != null)
 | 
			
		||||
            if (_taskMap.TryGetValue(taskId, out var info) && info != null && info.Reporter != null)
 | 
			
		||||
            {
 | 
			
		||||
                lock (info)
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -607,6 +607,7 @@ public class UDPClientPool
 | 
			
		||||
    /// <param name="devAddr">设备地址</param>
 | 
			
		||||
    /// <param name="data">要写入的32位数据</param>
 | 
			
		||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
			
		||||
    /// <param name="progress">进度报告器</param>
 | 
			
		||||
    /// <returns>写入结果,true表示写入成功</returns>
 | 
			
		||||
    public static async ValueTask<Result<bool>> WriteAddr(
 | 
			
		||||
            IPEndPoint endPoint, int taskID, UInt32 devAddr,
 | 
			
		||||
@@ -654,6 +655,7 @@ public class UDPClientPool
 | 
			
		||||
    /// <param name="devAddr">设备地址</param>
 | 
			
		||||
    /// <param name="dataArray">要写入的字节数组</param>
 | 
			
		||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
			
		||||
    /// <param name="progress">进度报告器</param>
 | 
			
		||||
    /// <returns>写入结果,true表示写入成功</returns>
 | 
			
		||||
    public static async ValueTask<Result<bool>> WriteAddr(
 | 
			
		||||
        IPEndPoint endPoint, int taskID, UInt32 devAddr,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user