feat: 完成数码管websocket通信

This commit is contained in:
2025-08-14 20:25:32 +08:00
parent 7bfc362b1f
commit 56eeb5dce3
12 changed files with 444 additions and 243 deletions

View File

@@ -229,12 +229,12 @@ public class ExamController : ControllerBase
[Authorize]
[HttpPost("commit/{examId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Resource), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> SubmitHomework(string examId, IFormFile file)
public async Task<IActionResult> Commit(string examId, IFormFile file)
{
if (string.IsNullOrWhiteSpace(examId))
return BadRequest("实验ID不能为空");
@@ -287,7 +287,7 @@ public class ExamController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, $"提交作业失败: {commitResult.Error.Message}");
}
var commit = commitResult.Value;
var commit = new ResourceInfo(commitResult.Value);
logger.Info($"用户 {userName} 成功提交实验 {examId} 的作业Commit ID: {commit.ID}");
return CreatedAtAction(nameof(GetCommitsByExamId), new { examId = examId }, commit);
@@ -307,7 +307,7 @@ public class ExamController : ControllerBase
[Authorize]
[HttpGet("commits/{examId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Resource[]), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -352,8 +352,7 @@ public class ExamController : ControllerBase
logger.Error($"获取提交记录时出错: {commitsResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取提交记录失败: {commitsResult.Error.Message}");
}
var commits = commitsResult.Value;
var commits = commitsResult.Value.Select(x => new ResourceInfo(x)).ToArray();
logger.Info($"成功获取用户 {userName} 在实验 {examId} 中的提交记录,共 {commits.Length} 条");
return Ok(commits);

View File

@@ -82,20 +82,10 @@ public class ResourceController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {result.Error.Message}");
}
var resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID.ToString(),
Name = resource.ResourceName,
Type = resource.ResourceType,
Purpose = resource.Purpose,
UploadTime = resource.UploadTime,
ExamID = resource.ExamID,
MimeType = resource.MimeType
};
var resourceInfo = new ResourceInfo(result.Value);
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.ID }, resourceInfo);
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resourceInfo.ID }, resourceInfo);
}
catch (Exception ex)
{
@@ -168,16 +158,7 @@ public class ResourceController : ControllerBase
var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value)
.OrderByDescending(r => r.UploadTime);
var mergedResourceInfos = allResources.Select(r => new ResourceInfo
{
ID = r.ID.ToString(),
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.Purpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
var mergedResourceInfos = allResources.Select(r => new ResourceInfo(r)).ToArray();
logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
return Ok(mergedResourceInfos);
@@ -189,16 +170,7 @@ public class ResourceController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID.ToString(),
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.Purpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
var resources = result.Value.Select(r => new ResourceInfo(r)).ToArray();
logger.Info($"成功获取资源列表,共 {resources.Length} 个资源");
return Ok(resources);
@@ -317,67 +289,77 @@ public class ResourceController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {ex.Message}");
}
}
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public required string ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 资源类型
/// </summary>
public required string Type { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required ResourcePurpose Purpose { get; set; }
/// <summary>
/// 上传时间
/// </summary>
public DateTime UploadTime { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
/// <summary>
/// MIME类型
/// </summary>
public string? MimeType { get; set; }
}
/// <summary>
/// 添加资源请求类
/// </summary>
public class AddResourceRequest
{
/// <summary>
/// 资源类型
/// </summary>
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required ResourcePurpose ResourcePurpose { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
}
}
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public string ID { get; set; } = string.Empty;
/// <summary>
/// 资源名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 资源类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 资源用途template/user
/// </summary>
public ResourcePurpose Purpose { get; set; }
/// <summary>
/// 上传时间
/// </summary>
public DateTime UploadTime { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
/// <summary>
/// MIME类型
/// </summary>
public string? MimeType { get; set; }
public ResourceInfo(Resource resource)
{
ID = resource.ID.ToString();
Name = resource.ResourceName;
Type = resource.ResourceType;
Purpose = resource.Purpose;
UploadTime = resource.UploadTime;
ExamID = resource.ExamID;
MimeType = resource.MimeType;
}
}
/// <summary>
/// 添加资源请求类
/// </summary>
public class AddResourceRequest
{
/// <summary>
/// 资源类型
/// </summary>
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required ResourcePurpose ResourcePurpose { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
}

View File

@@ -216,7 +216,7 @@ public class ResourceManager
/// <param name="userId">用户ID可选</param>
/// </summary>
/// <returns>资源信息列表</returns>
public Result<(string ID, string Name)[]> GetResourceListByType(
public Result<Resource[]> GetResourceListByType(
string resourceType,
ResourcePurpose? resourcePurpose = null,
string? examId = null,
@@ -241,17 +241,14 @@ public class ResourceManager
query = query.Where(r => r.UserID == userId);
}
var resources = query
.Select(r => new { r.ID, r.ResourceName })
.ToArray();
var resources = query.ToArray();
var result = resources.Select(r => (r.ID.ToString(), r.ResourceName)).ToArray();
logger.Info($"获取资源列表: {resourceType}" +
(examId != null ? $"/{examId}" : "") +
($"/{resourcePurpose.ToString()}") +
(userId != null ? $"/{userId}" : "") +
$",共 {result.Length} 个资源");
return new(result);
$",共 {resources.Length} 个资源");
return new(resources);
}
catch (Exception ex)
{

View File

@@ -4,22 +4,40 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using Tapper;
using server.Services;
using DotNext;
using Peripherals.SevenDigitalTubesClient;
using System.Collections.Concurrent;
namespace server.Hubs;
[Hub]
public interface IDigitalTubesHub
{
Task<bool> Join(string taskId);
Task<bool> StartScan();
Task<bool> StopScan();
Task<bool> SetFrequency(int frequency);
}
[Receiver]
public interface IDigitalTubesReceiver
{
Task OnReceive();
Task OnReceive(byte[] data);
}
class DigitalTubeInfo
{
public string ClientID { get; set; }
public SevenDigitalTubesCtrl TubeClient { get; set; }
public CancellationTokenSource CTS { get; set; } = new CancellationTokenSource();
public int Frequency { get; set; } = 100;
public bool IsRunning { get; set; } = false;
public DigitalTubeInfo(string clientID, SevenDigitalTubesCtrl client)
{
ClientID = clientID;
TubeClient = client;
}
}
[Authorize]
[EnableCors("SignalR")]
@@ -28,14 +46,152 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
private readonly Database.UserManager _userManager;
public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
private ConcurrentDictionary<string, DigitalTubeInfo> _infoDict = new();
public DigitalTubesHub(
IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext,
Database.UserManager userManager)
{
_hubContext = hubContext;
_userManager = userManager;
}
public async Task<bool> Join(string taskId)
private Optional<Database.Board> TryGetBoard()
{
return true;
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrEmpty(userName))
{
logger.Error("User name is null or empty");
return null;
}
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
{
logger.Error($"User '{userName}' not found");
return null;
}
var user = userRet.Value.Value;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Board not found");
return null;
}
return boardRet.Value.Value;
}
private Task ScanAllTubes(DigitalTubeInfo info)
{
return Task.Run(async () =>
{
var cntError = 0;
while (info.IsRunning && !info.CTS.IsCancellationRequested)
{
var beginTime = DateTime.Now;
var waitTime = TimeSpan.FromMilliseconds(1000 / info.Frequency);
var dataRet = await info.TubeClient.ScanAllTubes();
if (!dataRet.IsSuccessful)
{
logger.Error($"Failed to scan tubes: {dataRet.Error}");
cntError++;
if (cntError > 3)
{
logger.Error($"Too many errors, stopping scan");
info.IsRunning = false;
}
}
await _hubContext.Clients.Client(info.ClientID).OnReceive(dataRet.Value);
var processTime = DateTime.Now - beginTime;
if (processTime < waitTime)
{
await Task.Delay(waitTime - processTime);
}
}
}, info.CTS.Token);
}
public Task<bool> StartScan()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
if (_infoDict.GetOrAdd(
board.ID.ToString(),
(_) => new DigitalTubeInfo(
Context.ConnectionId,
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2))
) is DigitalTubeInfo info)
{
if (!info.IsRunning)
{
info.IsRunning = true;
_ = ScanAllTubes(info);
}
}
return Task.FromResult(true);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to start scan");
return Task.FromResult(false);
}
}
public Task<bool> StopScan()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
if (_infoDict.GetOrAdd(
board.ID.ToString(),
(_) => new DigitalTubeInfo(
Context.ConnectionId,
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2))
) is DigitalTubeInfo info)
{
if (info.IsRunning) info.IsRunning = false;
}
return Task.FromResult(true);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to stop scan");
return Task.FromResult(false);
}
}
public Task<bool> SetFrequency(int frequency)
{
try
{
if (frequency < 1 || frequency > 1000)
throw new ArgumentException("Frequency must be between 1 and 1000");
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
if (_infoDict.GetOrAdd(
board.ID.ToString(),
(_) => new DigitalTubeInfo(
Context.ConnectionId,
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2))
) is DigitalTubeInfo info)
{
info.Frequency = frequency;
}
return Task.FromResult(true);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to set frequency");
return Task.FromResult(false);
}
}
}

View File

@@ -63,7 +63,7 @@ public class SevenDigitalTubesCtrl
return (byte)(data & 0xFF);
}
public async ValueTask<Result<byte[]>> ScanTubes()
public async ValueTask<Result<byte[]>> ScanAllTubes()
{
var tubes = new byte[32];
for (int i = 0; i < 32; i++)

View File

@@ -80,7 +80,6 @@ public class HttpHdmiVideoStreamService : BackgroundService
public override async Task StopAsync(CancellationToken cancellationToken)
{
logger.Info("Stopping HDMI Video Stream Service...");
_httpListener?.Close();
// 禁用所有活跃的HDMI传输
var disableTasks = new List<Task>();
@@ -95,7 +94,6 @@ public class HttpHdmiVideoStreamService : BackgroundService
// 清空字典
_clientDict.Clear();
_httpListener?.Close(); // 立即关闭监听器,唤醒阻塞
await base.StopAsync(cancellationToken);
}