feat: 完成七段数码管后端

This commit is contained in:
SikongJueluo 2025-08-14 15:21:15 +08:00
parent 0e07a5996a
commit 7bfc362b1f
No known key found for this signature in database
13 changed files with 189 additions and 58 deletions

View File

@ -6,7 +6,7 @@ using server.Services;
public class ProgressTrackerTest
{
[Fact]
public async Task Test_ProgressReporter_Basic()
public void Test_ProgressReporter_Basic()
{
int reportedValue = -1;
var reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);

View File

@ -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>

View File

@ -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;

View 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;
}
}

View File

@ -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)

View File

@ -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));
}
}

View File

@ -21,7 +21,7 @@ public static class MsgBus
/// 通信总线初始化
/// </summary>
/// <returns>无</returns>
public async static void Init()
public static void Init()
{
if (!ArpClient.IsAdministrator())
{

View File

@ -1,6 +1,5 @@
using System.Net;
using DotNext;
using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.HdmiInClient;

View File

@ -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);

View 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;
}
}

View File

@ -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")
{

View File

@ -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)
{

View File

@ -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,