feat: 完成数码管websocket通信
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user