diff --git a/server/Program.cs b/server/Program.cs index 35d028a..9e2c8bf 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -259,6 +259,7 @@ try app.MapControllers(); app.MapHub("hubs/JtagHub"); app.MapHub("hubs/ProgressHub"); + app.MapHub("hubs/DigitalTubesHub"); // Setup Program MsgBus.Init(); diff --git a/server/src/Controllers/ExamController.cs b/server/src/Controllers/ExamController.cs index 071f7e2..85faee6 100644 --- a/server/src/Controllers/ExamController.cs +++ b/server/src/Controllers/ExamController.cs @@ -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 SubmitHomework(string examId, IFormFile file) + public async Task 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); diff --git a/server/src/Controllers/ResourceController.cs b/server/src/Controllers/ResourceController.cs index 7cca444..e852a9a 100644 --- a/server/src/Controllers/ResourceController.cs +++ b/server/src/Controllers/ResourceController.cs @@ -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}"); } } - - /// - /// 资源信息类 - /// - public class ResourceInfo - { - /// - /// 资源ID - /// - public required string ID { get; set; } - - /// - /// 资源名称 - /// - public required string Name { get; set; } - - /// - /// 资源类型 - /// - public required string Type { get; set; } - - /// - /// 资源用途(template/user) - /// - public required ResourcePurpose Purpose { get; set; } - - /// - /// 上传时间 - /// - public DateTime UploadTime { get; set; } - - /// - /// 所属实验ID(可选) - /// - public string? ExamID { get; set; } - - /// - /// MIME类型 - /// - public string? MimeType { get; set; } - } - - /// - /// 添加资源请求类 - /// - public class AddResourceRequest - { - /// - /// 资源类型 - /// - public required string ResourceType { get; set; } - - /// - /// 资源用途(template/user) - /// - public required ResourcePurpose ResourcePurpose { get; set; } - - /// - /// 所属实验ID(可选) - /// - public string? ExamID { get; set; } - } - +} + +/// +/// 资源信息类 +/// +public class ResourceInfo +{ + /// + /// 资源ID + /// + public string ID { get; set; } = string.Empty; + + /// + /// 资源名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 资源类型 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 资源用途(template/user) + /// + public ResourcePurpose Purpose { get; set; } + + /// + /// 上传时间 + /// + public DateTime UploadTime { get; set; } + + /// + /// 所属实验ID(可选) + /// + public string? ExamID { get; set; } + + /// + /// MIME类型 + /// + 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; + } +} + +/// +/// 添加资源请求类 +/// +public class AddResourceRequest +{ + /// + /// 资源类型 + /// + public required string ResourceType { get; set; } + + /// + /// 资源用途(template/user) + /// + public required ResourcePurpose ResourcePurpose { get; set; } + + /// + /// 所属实验ID(可选) + /// + public string? ExamID { get; set; } } diff --git a/server/src/Database/ResourceManager.cs b/server/src/Database/ResourceManager.cs index 5df3691..e34458c 100644 --- a/server/src/Database/ResourceManager.cs +++ b/server/src/Database/ResourceManager.cs @@ -216,7 +216,7 @@ public class ResourceManager /// 用户ID(可选) /// /// 资源信息列表 - public Result<(string ID, string Name)[]> GetResourceListByType( + public Result 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) { diff --git a/server/src/Hubs/DigitalTubesHub.cs b/server/src/Hubs/DigitalTubesHub.cs index da0c961..5a7f419 100644 --- a/server/src/Hubs/DigitalTubesHub.cs +++ b/server/src/Hubs/DigitalTubesHub.cs @@ -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 Join(string taskId); + Task StartScan(); + Task StopScan(); + Task 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, IDigitalTubesHub private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private readonly IHubContext _hubContext; + private readonly Database.UserManager _userManager; - public DigitalTubesHub(IHubContext hubContext) + private ConcurrentDictionary _infoDict = new(); + + public DigitalTubesHub( + IHubContext hubContext, + Database.UserManager userManager) { _hubContext = hubContext; + _userManager = userManager; } - public async Task Join(string taskId) + private Optional 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 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 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 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); + } } } diff --git a/server/src/Peripherals/SevenDigitalTubesClient.cs b/server/src/Peripherals/SevenDigitalTubesClient.cs index bc182ea..1c95936 100644 --- a/server/src/Peripherals/SevenDigitalTubesClient.cs +++ b/server/src/Peripherals/SevenDigitalTubesClient.cs @@ -63,7 +63,7 @@ public class SevenDigitalTubesCtrl return (byte)(data & 0xFF); } - public async ValueTask> ScanTubes() + public async ValueTask> ScanAllTubes() { var tubes = new byte[32]; for (int i = 0; i < 32; i++) diff --git a/server/src/Services/HttpHdmiVideoStreamService.cs b/server/src/Services/HttpHdmiVideoStreamService.cs index e5c3442..c438d08 100644 --- a/server/src/Services/HttpHdmiVideoStreamService.cs +++ b/server/src/Services/HttpHdmiVideoStreamService.cs @@ -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(); @@ -95,7 +94,6 @@ public class HttpHdmiVideoStreamService : BackgroundService // 清空字典 _clientDict.Clear(); - _httpListener?.Close(); // 立即关闭监听器,唤醒阻塞 await base.StopAsync(cancellationToken); } diff --git a/src/APIClient.ts b/src/APIClient.ts index 38edd7d..8f3a32e 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -1675,6 +1675,54 @@ export class DataClient { } return Promise.resolve(null as any); } + + addEmptyBoard( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Data/AddEmptyBoard"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + method: "POST", + url: url_, + headers: { + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processAddEmptyBoard(_response); + }); + } + + protected processAddEmptyBoard(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200) { + const _responseText = response.data; + return Promise.resolve(null as any); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } } export class DDSClient { @@ -2840,7 +2888,7 @@ export class ExamClient { * @param file (optional) 提交的文件 * @return 提交结果 */ - submitCommit(examId: string, file: FileParameter | undefined, cancelToken?: CancelToken): Promise { + commit(examId: string, file: FileParameter | undefined, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Exam/commit/{examId}"; if (examId === undefined || examId === null) throw new Error("The parameter 'examId' must be defined."); @@ -2870,11 +2918,11 @@ export class ExamClient { throw _error; } }).then((_response: AxiosResponse) => { - return this.processSubmitCommit(_response); + return this.processCommit(_response); }); } - protected processSubmitCommit(response: AxiosResponse): Promise { + protected processCommit(response: AxiosResponse): Promise { const status = response.status; let _headers: any = {}; if (response.headers && typeof response.headers === "object") { @@ -2888,8 +2936,8 @@ export class ExamClient { const _responseText = response.data; let result201: any = null; let resultData201 = _responseText; - result201 = Commit.fromJS(resultData201); - return Promise.resolve(result201); + result201 = ResourceInfo.fromJS(resultData201); + return Promise.resolve(result201); } else if (status === 400) { const _responseText = response.data; @@ -2920,7 +2968,7 @@ export class ExamClient { const _responseText = response.data; return throwException("An unexpected server error occurred.", status, _responseText, _headers); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } /** @@ -2928,7 +2976,7 @@ export class ExamClient { * @param examId 实验ID * @return 提交记录列表 */ - getCommitsByExamId(examId: string, cancelToken?: CancelToken): Promise { + getCommitsByExamId(examId: string, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Exam/commits/{examId}"; if (examId === undefined || examId === null) throw new Error("The parameter 'examId' must be defined."); @@ -2955,7 +3003,7 @@ export class ExamClient { }); } - protected processGetCommitsByExamId(response: AxiosResponse): Promise { + protected processGetCommitsByExamId(response: AxiosResponse): Promise { const status = response.status; let _headers: any = {}; if (response.headers && typeof response.headers === "object") { @@ -2972,12 +3020,12 @@ export class ExamClient { if (Array.isArray(resultData200)) { result200 = [] as any; for (let item of resultData200) - result200!.push(Commit.fromJS(item)); + result200!.push(ResourceInfo.fromJS(item)); } else { result200 = null; } - return Promise.resolve(result200); + return Promise.resolve(result200); } else if (status === 400) { const _responseText = response.data; @@ -3008,7 +3056,7 @@ export class ExamClient { const _responseText = response.data; return throwException("An unexpected server error occurred.", status, _responseText, _headers); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } /** @@ -6562,7 +6610,7 @@ export class ResourceClient { * @param file (optional) 资源文件 * @return 添加结果 */ - addResource(resourceType: string | undefined, resourcePurpose: string | undefined, examID: string | null | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise { + addResource(resourceType: string | undefined, resourcePurpose: ResourcePurpose | undefined, examID: string | null | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Resource"; url_ = url_.replace(/[?&]$/, ""); @@ -6659,7 +6707,7 @@ export class ResourceClient { * @param resourcePurpose (optional) 资源用途(可选) * @return 资源列表 */ - getResourceList(examId: string | null | undefined, resourceType: string | null | undefined, resourcePurpose: string | null | undefined, cancelToken?: CancelToken): Promise { + getResourceList(examId: string | null | undefined, resourceType: string | null | undefined, resourcePurpose: ResourcePurpose | null | undefined, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Resource?"; if (examId !== undefined && examId !== null) url_ += "examId=" + encodeURIComponent("" + examId) + "&"; @@ -8330,18 +8378,24 @@ export interface IExamDto { isVisibleToUsers: boolean; } -export class Commit implements ICommit { - /** 资源的唯一标识符 */ +/** 资源信息类 */ +export class ResourceInfo implements IResourceInfo { + /** 资源ID */ id!: string; - /** 上传资源的用户ID */ - userID!: string; - /** 所属实验ID */ + /** 资源名称 */ + name!: string; + /** 资源类型 */ + type!: string; + /** 资源用途(template/user) */ + purpose!: ResourcePurpose; + /** 上传时间 */ + uploadTime!: Date; + /** 所属实验ID(可选) */ examID?: string | undefined; - type!: CommitType; - resourceID!: string; - createdAt!: Date; + /** MIME类型 */ + mimeType?: string | undefined; - constructor(data?: ICommit) { + constructor(data?: IResourceInfo) { if (data) { for (var property in data) { if (data.hasOwnProperty(property)) @@ -8353,17 +8407,18 @@ export class Commit implements ICommit { init(_data?: any) { if (_data) { this.id = _data["id"]; - this.userID = _data["userID"]; - this.examID = _data["examID"]; + this.name = _data["name"]; this.type = _data["type"]; - this.resourceID = _data["resourceID"]; - this.createdAt = _data["createdAt"] ? new Date(_data["createdAt"].toString()) : undefined; + this.purpose = _data["purpose"]; + this.uploadTime = _data["uploadTime"] ? new Date(_data["uploadTime"].toString()) : undefined; + this.examID = _data["examID"]; + this.mimeType = _data["mimeType"]; } } - static fromJS(data: any): Commit { + static fromJS(data: any): ResourceInfo { data = typeof data === 'object' ? data : {}; - let result = new Commit(); + let result = new ResourceInfo(); result.init(data); return result; } @@ -8371,31 +8426,38 @@ export class Commit implements ICommit { toJSON(data?: any) { data = typeof data === 'object' ? data : {}; data["id"] = this.id; - data["userID"] = this.userID; - data["examID"] = this.examID; + data["name"] = this.name; data["type"] = this.type; - data["resourceID"] = this.resourceID; - data["createdAt"] = this.createdAt ? this.createdAt.toISOString() : undefined; + data["purpose"] = this.purpose; + data["uploadTime"] = this.uploadTime ? this.uploadTime.toISOString() : undefined; + data["examID"] = this.examID; + data["mimeType"] = this.mimeType; return data; } } -export interface ICommit { - /** 资源的唯一标识符 */ +/** 资源信息类 */ +export interface IResourceInfo { + /** 资源ID */ id: string; - /** 上传资源的用户ID */ - userID: string; - /** 所属实验ID */ + /** 资源名称 */ + name: string; + /** 资源类型 */ + type: string; + /** 资源用途(template/user) */ + purpose: ResourcePurpose; + /** 上传时间 */ + uploadTime: Date; + /** 所属实验ID(可选) */ examID?: string | undefined; - type: CommitType; - resourceID: string; - createdAt: Date; + /** MIME类型 */ + mimeType?: string | undefined; } -export enum CommitType { - Homework = 0, - Project = 1, - Markdown = 2, +export enum ResourcePurpose { + Template = 0, + User = 1, + Homework = 2, } export class HdmiVideoStreamEndpoint implements IHdmiVideoStreamEndpoint { @@ -8913,82 +8975,6 @@ export interface IOscilloscopeDataResponse { waveformData: string; } -/** 资源信息类 */ -export class ResourceInfo implements IResourceInfo { - /** 资源ID */ - id!: string; - /** 资源名称 */ - name!: string; - /** 资源类型 */ - type!: string; - /** 资源用途(template/user) */ - purpose!: string; - /** 上传时间 */ - uploadTime!: Date; - /** 所属实验ID(可选) */ - examID?: string | undefined; - /** MIME类型 */ - mimeType?: string | undefined; - - constructor(data?: IResourceInfo) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) - (this)[property] = (data)[property]; - } - } - } - - init(_data?: any) { - if (_data) { - this.id = _data["id"]; - this.name = _data["name"]; - this.type = _data["type"]; - this.purpose = _data["purpose"]; - this.uploadTime = _data["uploadTime"] ? new Date(_data["uploadTime"].toString()) : undefined; - this.examID = _data["examID"]; - this.mimeType = _data["mimeType"]; - } - } - - static fromJS(data: any): ResourceInfo { - data = typeof data === 'object' ? data : {}; - let result = new ResourceInfo(); - result.init(data); - return result; - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {}; - data["id"] = this.id; - data["name"] = this.name; - data["type"] = this.type; - data["purpose"] = this.purpose; - data["uploadTime"] = this.uploadTime ? this.uploadTime.toISOString() : undefined; - data["examID"] = this.examID; - data["mimeType"] = this.mimeType; - return data; - } -} - -/** 资源信息类 */ -export interface IResourceInfo { - /** 资源ID */ - id: string; - /** 资源名称 */ - name: string; - /** 资源类型 */ - type: string; - /** 资源用途(template/user) */ - purpose: string; - /** 上传时间 */ - uploadTime: Date; - /** 所属实验ID(可选) */ - examID?: string | undefined; - /** MIME类型 */ - mimeType?: string | undefined; -} - /** Package options which to send address to read or write */ export class SendAddrPackOptions implements ISendAddrPackOptions { /** 突发类型 */ diff --git a/src/stores/equipments.ts b/src/stores/equipments.ts index b236fbc..fd959a4 100644 --- a/src/stores/equipments.ts +++ b/src/stores/equipments.ts @@ -14,7 +14,7 @@ import { getHubProxyFactory, getReceiverRegister, } from "@/utils/signalR/TypedSignalR.Client"; -import type { ResourceInfo } from "@/APIClient"; +import { ResourcePurpose, type ResourceInfo } from "@/APIClient"; import type { IJtagHub } from "@/utils/signalR/TypedSignalR.Client/server.Hubs"; export const useEquipments = defineStore("equipments", () => { @@ -137,7 +137,7 @@ export const useEquipments = defineStore("equipments", () => { const resourceClient = AuthManager.createAuthenticatedResourceClient(); const resp = await resourceClient.addResource( "bitstream", - "user", + ResourcePurpose.User, examId || null, toFileParameterOrUndefined(bitstream), ); diff --git a/src/utils/signalR/TypedSignalR.Client/index.ts b/src/utils/signalR/TypedSignalR.Client/index.ts index 0468439..7992780 100644 --- a/src/utils/signalR/TypedSignalR.Client/index.ts +++ b/src/utils/signalR/TypedSignalR.Client/index.ts @@ -3,7 +3,7 @@ /* tslint:disable */ // @ts-nocheck import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr'; -import type { IJtagHub, IProgressHub, IJtagReceiver, IProgressReceiver } from './server.Hubs'; +import type { IDigitalTubesHub, IJtagHub, IProgressHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver } from './server.Hubs'; import type { ProgressInfo } from '../server.Hubs'; @@ -43,11 +43,15 @@ class ReceiverMethodSubscription implements Disposable { // API export type HubProxyFactoryProvider = { + (hubType: "IDigitalTubesHub"): HubProxyFactory; (hubType: "IJtagHub"): HubProxyFactory; (hubType: "IProgressHub"): HubProxyFactory; } export const getHubProxyFactory = ((hubType: string) => { + if(hubType === "IDigitalTubesHub") { + return IDigitalTubesHub_HubProxyFactory.Instance; + } if(hubType === "IJtagHub") { return IJtagHub_HubProxyFactory.Instance; } @@ -57,11 +61,15 @@ export const getHubProxyFactory = ((hubType: string) => { }) as HubProxyFactoryProvider; export type ReceiverRegisterProvider = { + (receiverType: "IDigitalTubesReceiver"): ReceiverRegister; (receiverType: "IJtagReceiver"): ReceiverRegister; (receiverType: "IProgressReceiver"): ReceiverRegister; } export const getReceiverRegister = ((receiverType: string) => { + if(receiverType === "IDigitalTubesReceiver") { + return IDigitalTubesReceiver_Binder.Instance; + } if(receiverType === "IJtagReceiver") { return IJtagReceiver_Binder.Instance; } @@ -72,6 +80,35 @@ export const getReceiverRegister = ((receiverType: string) => { // HubProxy +class IDigitalTubesHub_HubProxyFactory implements HubProxyFactory { + public static Instance = new IDigitalTubesHub_HubProxyFactory(); + + private constructor() { + } + + public readonly createHubProxy = (connection: HubConnection): IDigitalTubesHub => { + return new IDigitalTubesHub_HubProxy(connection); + } +} + +class IDigitalTubesHub_HubProxy implements IDigitalTubesHub { + + public constructor(private connection: HubConnection) { + } + + public readonly startScan = async (): Promise => { + return await this.connection.invoke("StartScan"); + } + + public readonly stopScan = async (): Promise => { + return await this.connection.invoke("StopScan"); + } + + public readonly setFrequency = async (frequency: number): Promise => { + return await this.connection.invoke("SetFrequency", frequency); + } +} + class IJtagHub_HubProxyFactory implements HubProxyFactory { public static Instance = new IJtagHub_HubProxyFactory(); @@ -125,6 +162,27 @@ class IProgressHub_HubProxy implements IProgressHub { // Receiver +class IDigitalTubesReceiver_Binder implements ReceiverRegister { + + public static Instance = new IDigitalTubesReceiver_Binder(); + + private constructor() { + } + + public readonly register = (connection: HubConnection, receiver: IDigitalTubesReceiver): Disposable => { + + const __onReceive = (...args: [string]) => receiver.onReceive(...args); + + connection.on("OnReceive", __onReceive); + + const methodList: ReceiverMethod[] = [ + { methodName: "OnReceive", method: __onReceive } + ] + + return new ReceiverMethodSubscription(connection, methodList); + } +} + class IJtagReceiver_Binder implements ReceiverRegister { public static Instance = new IJtagReceiver_Binder(); diff --git a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts index 1c4e068..fb87cd9 100644 --- a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts +++ b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts @@ -5,6 +5,22 @@ import type { IStreamResult, Subject } from '@microsoft/signalr'; import type { ProgressInfo } from '../server.Hubs'; +export type IDigitalTubesHub = { + /** + * @returns Transpiled from System.Threading.Tasks.Task + */ + startScan(): Promise; + /** + * @returns Transpiled from System.Threading.Tasks.Task + */ + stopScan(): Promise; + /** + * @param frequency Transpiled from int + * @returns Transpiled from System.Threading.Tasks.Task + */ + setFrequency(frequency: number): Promise; +} + export type IJtagHub = { /** * @param freq Transpiled from int @@ -30,6 +46,14 @@ export type IProgressHub = { join(taskId: string): Promise; } +export type IDigitalTubesReceiver = { + /** + * @param data Transpiled from byte[] + * @returns Transpiled from System.Threading.Tasks.Task + */ + onReceive(data: string): Promise; +} + export type IJtagReceiver = { /** * @param msg Transpiled from System.Collections.Generic.Dictionary diff --git a/src/views/Exam/ExamInfoModal.vue b/src/views/Exam/ExamInfoModal.vue index 77569eb..42cd2e8 100644 --- a/src/views/Exam/ExamInfoModal.vue +++ b/src/views/Exam/ExamInfoModal.vue @@ -250,7 +250,7 @@