Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab

This commit is contained in:
alivender
2025-08-17 14:55:41 +08:00
50 changed files with 3984 additions and 2970 deletions

2
.gitignore vendored
View File

@@ -29,7 +29,7 @@ DebuggerCmd.md
*.ntvs*
*.njsproj
*.sw?
prompt.md
*.tsbuildinfo
# Generated Files

13
TODO.md
View File

@@ -1,13 +0,0 @@
# TODO
1. 后端HTTP视频流
640*480, RGB565
0x0000_0000 + 25800
2. 信号发生器界面导入.dat文件
3. 示波器后端交互、前端界面
4. 逻辑分析仪后端交互、前端界面
5. 前端重构
6. 数据库 —— 用户登录、板卡资源分配、板卡IP地址分配

View File

@@ -180,8 +180,7 @@ try
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
// 添加进度跟踪服务
builder.Services.AddSingleton<ProgressTrackerService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<ProgressTrackerService>());
builder.Services.AddSingleton<ProgressTracker>();
// Application Settings
var app = builder.Build();
@@ -258,6 +257,8 @@ try
// Setup Program
MsgBus.Init();
var progressTracker = app.Services.GetRequiredService<ProgressTracker>();
MsgBus.SetProgressTracker(progressTracker);
// Generate API Client
app.MapGet("GetAPIClientCode", async (HttpContext context) =>

View File

@@ -368,11 +368,8 @@ public class ExamController : ControllerBase
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteCommit(string commitId)
public IActionResult DeleteCommit(Guid commitId)
{
if (!Guid.TryParse(commitId, out _))
return BadRequest("提交记录ID格式不正确");
try
{
// 获取当前用户信息

View File

@@ -16,17 +16,12 @@ public class JtagController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ProgressTrackerService _tracker;
private readonly ProgressTracker _tracker = MsgBus.ProgressTracker;
private readonly UserManager _userManager = new();
private readonly ResourceManager _resourceManager = new();
private const string BITSTREAM_PATH = "bitstream/Jtag";
public JtagController(ProgressTrackerService tracker)
{
_tracker = tracker;
}
/// <summary>
/// 控制器首页信息
/// </summary>
@@ -137,7 +132,7 @@ public class JtagController : ControllerBase
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public IResult DownloadBitstream(string address, int port, string bitstreamId, CancellationToken cancelToken)
public IResult DownloadBitstream(string address, int port, Guid bitstreamId, CancellationToken cancelToken)
{
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
@@ -188,8 +183,8 @@ public class JtagController : ControllerBase
logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
// 定义进度跟踪
var (taskId, progress) = _tracker.CreateTask(cancelToken);
progress.Report(10);
var taskId = _tracker.CreateTask(10000);
_tracker.AdvanceProgress(taskId, 10);
_ = Task.Run(async () =>
{
@@ -210,7 +205,8 @@ public class JtagController : ControllerBase
if (!retBuffer.IsSuccessful)
{
logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
progress.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
_tracker.FailProgress(taskId,
$"User {username} failed to reverse bytes: {retBuffer.Error}");
return;
}
revBuffer = retBuffer.Value;
@@ -228,21 +224,22 @@ public class JtagController : ControllerBase
var processedBytes = outputStream.ToArray();
logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
progress.Report(20);
_tracker.AdvanceProgress(taskId, 20);
// 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(processedBytes);
var ret = await jtagCtrl.DownloadBitstream(processedBytes, taskId);
if (ret.IsSuccessful)
{
logger.Info($"User {username} successfully downloaded bitstream '{resource.ResourceName}' to device {address}");
progress.Finish();
_tracker.CompleteProgress(taskId);
}
else
{
logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
progress.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
_tracker.FailProgress(taskId,
$"User {username} failed to download bitstream to device {address}: {ret.Error}");
}
}
});

View File

@@ -78,7 +78,7 @@ public class ResourceController : ControllerBase
var resourceInfo = new ResourceInfo(result.Value);
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} ID: {resourceInfo.ID}");
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resourceInfo.ID }, resourceInfo);
}
catch (Exception ex)
@@ -187,7 +187,7 @@ public class ResourceController : ControllerBase
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceById(string resourceId)
public IActionResult GetResourceById(Guid resourceId)
{
try
{
@@ -231,7 +231,7 @@ public class ResourceController : ControllerBase
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteResource(string resourceId)
public IActionResult DeleteResource(Guid resourceId)
{
try
{
@@ -293,7 +293,7 @@ public class ResourceInfo
/// <summary>
/// 资源ID
/// </summary>
public string ID { get; set; } = string.Empty;
public Guid ID { get; set; }
/// <summary>
/// 资源名称
@@ -327,7 +327,7 @@ public class ResourceInfo
public ResourceInfo(Resource resource)
{
ID = resource.ID.ToString();
ID = resource.ID;
Name = resource.ResourceName;
Type = resource.ResourceType;
Purpose = resource.Purpose;

View File

@@ -0,0 +1,127 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Cors;
using Peripherals.SwitchClient;
namespace server.Controllers;
[ApiController]
[Route("api/[controller]")]
public class SwitchController : ControllerBase
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly Database.UserManager _userManager = new();
/// <summary>
/// 获取示波器实例
/// </summary>
private SwitchCtrl? GetSwitchCtrl()
{
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return null;
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return null;
var user = userRet.Value.Value;
if (user.BoardID == Guid.Empty)
return null;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
return null;
var board = boardRet.Value.Value;
return new SwitchCtrl(board.IpAddr, board.Port, 0);
}
/// <summary>
/// 启用或禁用 Switch 外设
/// </summary>
/// <param name="enable">是否启用</param>
/// <returns>操作结果</returns>
[HttpPost("enable")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetEnable([FromQuery] bool enable)
{
var switchCtrl = GetSwitchCtrl();
if (switchCtrl == null)
return BadRequest("Can't get user or board info");
var result = await switchCtrl.SetEnable(enable);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "SetEnable failed");
return StatusCode(500, result.Error);
}
return Ok(result.Value);
}
/// <summary>
/// 控制指定编号的 Switch 开关
/// </summary>
/// <param name="num">开关编号</param>
/// <param name="onOff">开/关</param>
/// <returns>操作结果</returns>
[HttpPost("switch")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetSwitchOnOff([FromQuery] int num, [FromQuery] bool onOff)
{
if (num <= 0 || num > 6)
return BadRequest(new ArgumentException($"Switch num should be 1~5, instead of {num}"));
var switchCtrl = GetSwitchCtrl();
if (switchCtrl == null)
return BadRequest("Can't get user or board info");
var result = await switchCtrl.SetSwitchOnOff(num, onOff);
if (!result.IsSuccessful)
{
logger.Error(result.Error, $"SetSwitchOnOff({num}, {onOff}) failed");
return StatusCode(500, result.Error);
}
return Ok(result.Value);
}
/// <summary>
/// 控制 Switch 开关
/// </summary>
/// <param name="keyStatus">开关状态</param>
/// <returns>操作结果</returns>
[HttpPost("MultiSwitch")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetMultiSwitchsOnOff(bool[] keyStatus)
{
if (keyStatus.Length == 0 || keyStatus.Length > 6) return BadRequest(
new ArgumentException($"Switch num should be 1~5, instead of {keyStatus.Length}"));
var switchCtrl = GetSwitchCtrl();
if (switchCtrl == null)
return BadRequest("Can't get user or board info");
for (int i = 0; i < keyStatus.Length; i++)
{
var result = await switchCtrl.SetSwitchOnOff(i + 1, keyStatus[i]);
if (!result.IsSuccessful)
{
logger.Error(result.Error, $"SetSwitchOnOff({i}, {keyStatus[i]}) failed");
return StatusCode(500, result.Error);
}
if (!result.Value) return Ok(false);
}
return Ok(true);
}
}

View File

@@ -159,7 +159,7 @@ public class ResourceManager
var duplicateResource = _db.ResourceTable.Where(r => r.SHA256 == sha256).FirstOrDefault();
if (duplicateResource != null && duplicateResource.ResourceName == resourceName)
{
logger.Info($"资源已存在: {resourceName}");
logger.Info($"资源已存在: {resourceName}, ID: {duplicateResource.ID}, UserID: {duplicateResource.UserID}");
return duplicateResource;
}
@@ -311,9 +311,9 @@ public class ResourceManager
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源数据</returns>
public Optional<Resource> GetResourceById(string resourceId)
public Optional<Resource> GetResourceById(Guid resourceId)
{
var resource = _db.ResourceTable.Where(r => r.ID.ToString() == resourceId).FirstOrDefault();
var resource = _db.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
if (resource == null)
{
@@ -330,11 +330,11 @@ public class ResourceManager
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除的记录数</returns>
public Result<int> DeleteResource(string resourceId)
public Result<int> DeleteResource(Guid resourceId)
{
try
{
var result = _db.ResourceTable.Where(r => r.ID.ToString() == resourceId).Delete();
var result = _db.ResourceTable.Where(r => r.ID == resourceId).Delete();
logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
return new(result);
}

View File

@@ -8,6 +8,8 @@ using DotNext;
using Peripherals.SevenDigitalTubesClient;
using System.Collections.Concurrent;
#pragma warning disable 1998
namespace server.Hubs;
[Hub]
@@ -16,7 +18,7 @@ public interface IDigitalTubesHub
Task<bool> StartScan();
Task<bool> StopScan();
Task<bool> SetFrequency(int frequency);
Task<DigitalTubeTaskStatus> GetStatus();
Task<DigitalTubeTaskStatus?> GetStatus();
}
[Receiver]
@@ -31,23 +33,27 @@ public class DigitalTubeTaskStatus
public int Frequency { get; set; } = 100;
public bool IsRunning { get; set; } = false;
public DigitalTubeTaskStatus(DigitalTubeInfo info)
public DigitalTubeTaskStatus(ScanTaskInfo info)
{
Frequency = info.Frequency;
IsRunning = info.IsRunning;
}
}
public class DigitalTubeInfo
public class ScanTaskInfo
{
public string BoardID { get; set; }
public string ClientID { get; set; }
public Task? ScanTask { get; set; }
public SevenDigitalTubesCtrl TubeClient { get; set; }
public CancellationTokenSource CTS { get; set; } = new CancellationTokenSource();
public CancellationTokenSource CTS { get; set; } = new();
public int Frequency { get; set; } = 100;
public bool IsRunning { get; set; } = false;
public DigitalTubeInfo(string clientID, SevenDigitalTubesCtrl client)
public ScanTaskInfo(
string boardID, string clientID, SevenDigitalTubesCtrl client)
{
BoardID = boardID;
ClientID = clientID;
TubeClient = client;
}
@@ -58,11 +64,10 @@ public class DigitalTubeInfo
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 = new();
private ConcurrentDictionary<string, DigitalTubeInfo> _infoDict = new();
private ConcurrentDictionary<(string, string), ScanTaskInfo> _scanTasks = new();
public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
{
@@ -95,17 +100,18 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
return boardRet.Value.Value;
}
private Task ScanAllTubes(DigitalTubeInfo info)
private Task ScanAllTubes(ScanTaskInfo scanInfo)
{
var token = scanInfo.CTS.Token;
return Task.Run(async () =>
{
var cntError = 0;
while (!info.CTS.IsCancellationRequested)
while (!token.IsCancellationRequested)
{
var beginTime = DateTime.Now;
var waitTime = TimeSpan.FromMilliseconds(1000 / info.Frequency);
var waitTime = TimeSpan.FromMilliseconds(1000 / scanInfo.Frequency);
var dataRet = await info.TubeClient.ScanAllTubes();
var dataRet = await scanInfo.TubeClient.ScanAllTubes();
if (!dataRet.IsSuccessful)
{
logger.Error($"Failed to scan tubes: {dataRet.Error}");
@@ -113,126 +119,138 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
if (cntError > 3)
{
logger.Error($"Too many errors, stopping scan");
info.IsRunning = false;
break;
}
}
await _hubContext.Clients.Client(info.ClientID).OnReceive(dataRet.Value);
await _hubContext.Clients.Client(scanInfo.ClientID).OnReceive(dataRet.Value);
var processTime = DateTime.Now - beginTime;
if (processTime < waitTime)
{
await Task.Delay(waitTime - processTime);
await Task.Delay(waitTime - processTime, token);
}
}
}, info.CTS.Token);
scanInfo.IsRunning = false;
}, token)
.ContinueWith((task) =>
{
if (task.IsFaulted)
{
logger.Error(
$"Digital tubes scan operation failesj for board {task.Exception}");
}
else if (task.IsCanceled)
{
logger.Info(
$"Digital tubes scan operation cancelled for board {scanInfo.BoardID}");
}
else
{
logger.Info(
$"Digital tubes scan completed successfully for board {scanInfo.BoardID}");
}
});
}
public Task<bool> StartScan()
public async 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;
if (info.CTS.IsCancellationRequested)
{
info.CTS.Dispose();
info.CTS = new CancellationTokenSource();
}
_ = ScanAllTubes(info);
}
}
var key = (board.ID.ToString(), Context.ConnectionId);
return Task.FromResult(true);
if (_scanTasks.TryGetValue(key, out var existing) && existing.IsRunning)
return true;
var cts = new CancellationTokenSource();
var scanTaskInfo = new ScanTaskInfo(
board.ID.ToString(), Context.ConnectionId,
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 0)
);
scanTaskInfo.ScanTask = ScanAllTubes(scanTaskInfo);
_scanTasks[key] = scanTaskInfo;
return true;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to start scan");
return Task.FromResult(false);
return false;
}
}
public Task<bool> StopScan()
public async 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)
{
info.IsRunning = false;
info.CTS.Cancel();
}
var key = (board.ID.ToString(), Context.ConnectionId);
return Task.FromResult(true);
if (_scanTasks.TryRemove(key, out var scanInfo))
{
scanInfo.IsRunning = false;
scanInfo.CTS.Cancel();
if (scanInfo.ScanTask != null)
await scanInfo.ScanTask;
scanInfo.CTS.Dispose();
}
return true;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to stop scan");
return Task.FromResult(false);
return false;
}
}
public Task<bool> SetFrequency(int frequency)
public async Task<bool> SetFrequency(int frequency)
{
try
{
if (frequency < 1 || frequency > 1000) return Task.FromException<bool>(
new ArgumentException("Frequency must be between 1 and 1000"));
if (frequency < 1 || frequency > 1000)
return false;
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;
}
var key = (board.ID.ToString(), Context.ConnectionId);
return Task.FromResult(true);
if (_scanTasks.TryGetValue(key, out var scanInfo) && scanInfo.IsRunning)
{
scanInfo.Frequency = frequency;
return true;
}
else
{
logger.Warn($"SetFrequency called but no running scan for board {board.ID} and client {Context.ConnectionId}");
return false;
}
}
catch (Exception ex)
{
logger.Error(ex, "Failed to set frequency");
return Task.FromResult(false);
return false;
}
}
public Task<DigitalTubeTaskStatus> GetStatus()
public async Task<DigitalTubeTaskStatus?> GetStatus()
{
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)
{
return Task.FromResult(new DigitalTubeTaskStatus(info));
}
var key = (board.ID.ToString(), Context.ConnectionId);
return Task.FromException<DigitalTubeTaskStatus>(new ArgumentException("Wrong argument"));
if (_scanTasks.TryGetValue(key, out var scanInfo))
{
return new DigitalTubeTaskStatus(scanInfo);
}
else
{
return null;
}
}
catch (Exception ex)
{
logger.Error(ex, "Failed to get status");
return Task.FromException<DigitalTubeTaskStatus>(new Exception("Failed to get status"));
throw new Exception("Failed to get status", ex);
}
}

View File

@@ -1,17 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using Tapper;
using server.Services;
#pragma warning disable 1998
namespace server.Hubs;
[Hub]
public interface IProgressHub
{
Task<bool> Join(string taskId);
Task<bool> Leave(string taskId);
Task<ProgressInfo?> GetProgress(string taskId);
}
[Receiver]
@@ -23,8 +26,7 @@ public interface IProgressReceiver
[TranspilationSource]
public enum ProgressStatus
{
Pending,
InProgress,
Running,
Completed,
Canceled,
Failed
@@ -33,10 +35,10 @@ public enum ProgressStatus
[TranspilationSource]
public class ProgressInfo
{
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;
public required string TaskId { get; set; }
public required ProgressStatus Status { get; set; }
public required double ProgressPercent { get; set; }
public required string ErrorMessage { get; set; }
};
[Authorize]
@@ -44,18 +46,32 @@ public class ProgressInfo
public class ProgressHub : Hub<IProgressReceiver>, IProgressHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
private readonly ProgressTrackerService _tracker;
public ProgressHub(IHubContext<ProgressHub, IProgressReceiver> hubContext, ProgressTrackerService tracker)
{
_hubContext = hubContext;
_tracker = tracker;
}
private readonly ProgressTracker _progressTracker = MsgBus.ProgressTracker;
public async Task<bool> Join(string taskId)
{
return await Task.Run(() => _tracker.BindTask(taskId, Context.ConnectionId));
await Groups.AddToGroupAsync(Context.ConnectionId, taskId);
// 发送当前状态(如果存在)
var task = _progressTracker.GetTask(taskId);
if (task != null)
{
await Clients.Caller.OnReceiveProgress(task.Value.ToProgressInfo());
}
logger.Info($"Client {Context.ConnectionId} joined task {taskId}");
return true;
}
public async Task<bool> Leave(string taskId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, taskId);
logger.Info($"Client {Context.ConnectionId} left task {taskId}");
return true;
}
public async Task<ProgressInfo?> GetProgress(string taskId)
{
return _progressTracker.GetTask(taskId)?.ToProgressInfo();
}
}

View File

@@ -1,7 +1,8 @@
using server.Services;
/// <summary>
/// 多线程通信总线
/// </summary>
public static class MsgBus
public sealed class MsgBus
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -11,12 +12,39 @@ public static class MsgBus
/// </summary>
public static UDPServer UDPServer { get { return udpServer; } }
// 添加静态ProgressTracker引用
private static ProgressTracker? _progressTracker;
/// <summary>
/// 设置全局ProgressTracker实例
/// </summary>
public static void SetProgressTracker(ProgressTracker progressTracker)
{
_progressTracker = progressTracker;
}
public static ProgressTracker ProgressTracker
{
get
{
if (_progressTracker == null)
{
throw new InvalidOperationException("ProgressTracker is not set.");
}
return _progressTracker;
}
}
private static bool isRunning = false;
/// <summary>
/// 获取通信总线运行状态
/// </summary>
public static bool IsRunning { get { return isRunning; } }
private MsgBus() { }
static MsgBus() { }
/// <summary>
/// 通信总线初始化
/// </summary>

View File

@@ -380,6 +380,7 @@ public class JtagStatusReg
public class Jtag
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ProgressTracker _progressTracker = MsgBus.ProgressTracker;
private const int CLOCK_FREQ = 50; // MHz
@@ -392,6 +393,7 @@ public class Jtag
public readonly string address;
private IPEndPoint ep;
/// <summary>
/// Jtag 构造函数
/// </summary>
@@ -444,10 +446,10 @@ public class Jtag
async ValueTask<Result<bool>> WriteFIFO(
UInt32 devAddr, UInt32 data, UInt32 result,
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, ProgressReporter? progress = null)
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "")
{
{
var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progress?.CreateChild(80));
var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progressId);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed"));
}
@@ -458,17 +460,18 @@ public class Jtag
{
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
progress?.Finish();
_progressTracker?.AdvanceProgress(progressId, 10);
return ret.Value;
}
}
async ValueTask<Result<bool>> WriteFIFO(
UInt32 devAddr, byte[] data, UInt32 result,
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, ProgressReporter? progress = null)
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "")
{
{
var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progress?.CreateChild(80));
var ret = await UDPClientPool.WriteAddr(
this.ep, 0, devAddr, data, this.timeout, progressId);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed"));
}
@@ -479,7 +482,7 @@ public class Jtag
{
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
progress?.Finish();
_progressTracker.AdvanceProgress(progressId, 10);
return ret.Value;
}
}
@@ -564,7 +567,7 @@ public class Jtag
}
async ValueTask<Result<bool>> LoadDRCareInput(
byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, ProgressReporter? progress = null)
byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, string progressId = "")
{
var bytesLen = ((uint)(bytesArray.Length * 8));
if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)"));
@@ -579,14 +582,15 @@ public class Jtag
else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed"));
}
progress?.Report(10);
_progressTracker.AdvanceProgress(progressId, 10);
{
var ret = await WriteFIFO(
JtagAddr.WRITE_DATA,
bytesArray, 0x01_00_00_00,
JtagState.CMD_EXEC_FINISH,
progress: progress?.CreateChild(90)
0,
progressId
);
if (!ret.IsSuccessful) return new(ret.Error);
@@ -709,58 +713,55 @@ public class Jtag
/// 下载比特流到 JTAG 设备
/// </summary>
/// <param name="bitstream">比特流数据</param>
/// <param name="progress">进度报告器</param>
/// <param name="progressId">进度ID</param>
/// <returns>指示下载是否成功的异步结果</returns>
public async ValueTask<Result<bool>> DownloadBitstream(
byte[] bitstream, ProgressReporter? progress = null)
byte[] bitstream, string progressId = "")
{
// Clear Data
MsgBus.UDPServer.ClearUDPData(this.address, 0);
logger.Trace($"Clear up udp server {this.address,0} receive data");
if (progress != null)
{
progress.ExpectedSteps = 25;
progress.Increase();
}
_progressTracker.AdvanceProgress(progressId, 10);
Result<bool> ret;
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
logger.Trace("Jtag initialize");
ret = await ExecRDCmd(JtagCmd.JTAG_DR_JRST);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
ret = await ExecRDCmd(JtagCmd.JTAG_DR_CFGI);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_CFGI Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
logger.Trace("Jtag ready to write bitstream");
ret = await IdleDelay(100000);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
ret = await LoadDRCareInput(bitstream, progress: progress?.CreateChild(50));
ret = await LoadDRCareInput(bitstream, progressId: progressId);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Load Data Failed"));
@@ -769,40 +770,40 @@ public class Jtag
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
ret = await ExecRDCmd(JtagCmd.JTAG_DR_JWAKEUP);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JWAKEUP Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
logger.Trace("Jtag reset device");
ret = await IdleDelay(10000);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
var retCode = await ReadStatusReg();
if (!retCode.IsSuccessful) return new(retCode.Error);
var jtagStatus = new JtagStatusReg(retCode.Value);
if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete))
return new(new Exception("Jtag download bitstream failed"));
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
logger.Trace("Jtag download bitstream successfully");
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 10);
// Finish
progress?.Finish();
_progressTracker.AdvanceProgress(progressId, 10);
return true;
}

View File

@@ -6,7 +6,7 @@ namespace Peripherals.SevenDigitalTubesClient;
static class SevenDigitalTubesAddr
{
public const UInt32 BASE = 0x0000_0000;
public const UInt32 BASE = 0xB000_0000;
}
public class SevenDigitalTubesCtrl

View File

@@ -0,0 +1,65 @@
using System.Collections;
using System.Net;
using DotNext;
namespace Peripherals.SwitchClient;
class SwitchCtrlAddr
{
public const UInt32 BASE = 0xB0_00_00_20;
public const UInt32 ENABLE = BASE;
}
/// <summary>
/// 矩阵键盘外设类,用于控制和管理矩阵键盘的功能。
/// </summary>
public class SwitchCtrl
{
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;
public SwitchCtrl(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<bool>> SetEnable(bool enable)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, SwitchCtrlAddr.ENABLE, enable ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
public async ValueTask<Result<bool>> SetSwitchOnOff(int num, bool onOff)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, SwitchCtrlAddr.BASE + (UInt32)num, onOff ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Set Switch {onOff} failed: {ret.Error}");
return new(ret.Error);
}
return ret.Value;
}
}

View File

@@ -0,0 +1,147 @@
using System.Collections.Concurrent;
using Microsoft.AspNetCore.SignalR;
using server.Hubs;
namespace server.Services;
public enum TaskState { Running, Completed, Failed, Cancelled }
public readonly struct TaskProgress
{
public string Id { get; }
public int Current { get; }
public int Total { get; }
public TaskState State { get; }
public long Timestamp { get; }
public string? Error { get; }
public TaskProgress(string id, int current, int total, TaskState state, long timestamp, string? error = null)
{
Id = id;
Current = current;
Total = total;
State = state;
Timestamp = timestamp;
Error = error;
}
public TaskProgress WithUpdate(int? current = null, TaskState? state = null, string? error = null)
{
return new TaskProgress(
Id,
current ?? Current,
Total,
state ?? State,
DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
error ?? Error
);
}
public ProgressInfo ToProgressInfo()
{
return new ProgressInfo
{
TaskId = Id,
Status = State switch
{
TaskState.Running => ProgressStatus.Running,
TaskState.Completed => ProgressStatus.Completed,
TaskState.Failed => ProgressStatus.Failed,
TaskState.Cancelled => ProgressStatus.Canceled,
_ => ProgressStatus.Failed
},
ProgressPercent = Total > 0 ? ((double)Current * 100) / (double)Total : 0,
ErrorMessage = Error ?? string.Empty
};
}
}
public sealed class ProgressTracker
{
private readonly ConcurrentDictionary<string, TaskProgress> _tasks = new();
private readonly Timer _cleaner;
private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
// 构造器支持可选的Hub注入
public ProgressTracker(IHubContext<ProgressHub, IProgressReceiver> hubContext)
{
_hubContext = hubContext;
_cleaner = new Timer(CleanExpiredTasks, null,
TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
public void CleanExpiredTasks(object? obj)
{
var cutoff = DateTimeOffset.Now.AddMinutes(-3).ToUnixTimeSeconds();
var expired = _tasks.Where(kvp => kvp.Value.Timestamp < cutoff).Select(kvp => kvp.Key).ToList();
foreach (var id in expired)
{
_tasks.TryRemove(id, out _);
}
}
public string CreateTask(int total)
{
var id = Guid.NewGuid().ToString();
var task = new TaskProgress(id, 0, total, TaskState.Running, DateTimeOffset.UtcNow.ToUnixTimeSeconds());
_tasks[id] = task;
NotifyIfNeeded(task);
return id;
}
// 核心更新方法,现在包含自动通知
public bool UpdateTask(string id, Func<TaskProgress, TaskProgress> updater)
{
if (!_tasks.TryGetValue(id, out var current))
return false;
var updated = updater(current);
if (_tasks.TryUpdate(id, updated, current))
{
NotifyIfNeeded(updated);
return true;
}
return false;
}
// 自动通知逻辑 - 简单直接
private void NotifyIfNeeded(TaskProgress task)
{
_hubContext.Clients.Group(task.Id).OnReceiveProgress(task.ToProgressInfo());
}
public bool UpdateProgress(string id, int current)
{
return UpdateTask(id, p => p.WithUpdate(
current: Math.Min(current, p.Total)));
}
public bool AdvanceProgress(string id, int steps)
{
return UpdateTask(id, p => p.WithUpdate(
current: Math.Min(p.Current + steps, p.Total)));
}
public bool CancelProgress(string id)
{
return UpdateTask(id, p => p.WithUpdate(state: TaskState.Cancelled));
}
public bool CompleteProgress(string id)
{
return UpdateTask(id, p => p.WithUpdate(
current: p.Total, state: TaskState.Completed));
}
public bool FailProgress(string id, string? error)
{
return UpdateTask(id, p => p.WithUpdate(
state: TaskState.Failed, error: error));
}
public TaskProgress? GetTask(string id)
{
_tasks.TryGetValue(id, out var task);
return task.Id == null ? null : task;
}
}

View File

@@ -1,294 +0,0 @@
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
using DotNext;
using Common;
using server.Hubs;
namespace server.Services;
public class ProgressReporter : ProgressInfo, IProgress<int>
{
private int _progress = 0;
private int _stepProgress = 1;
private int _expectedSteps = 100;
private int _parentProportion = 100;
public int Progress => _progress;
public int MaxProgress { get; set; } = 100;
public int StepProgress
{
get => _stepProgress;
set
{
_stepProgress = value;
_expectedSteps = MaxProgress / value;
}
}
public int ExpectedSteps
{
get => _expectedSteps;
set
{
_expectedSteps = value;
MaxProgress = Number.IntPow(10, Number.GetLength(value));
_stepProgress = MaxProgress / value;
}
}
public Func<int, Task>? ReporterFunc { get; set; } = null;
public ProgressReporter? Parent { get; set; }
public ProgressReporter? Child { get; set; }
private ProgressStatus _status = ProgressStatus.Pending;
private string _errorMessage = string.Empty;
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)
{
_progress = initProgress;
MaxProgress = maxProgress;
StepProgress = step;
ReporterFunc = reporter;
}
public ProgressReporter(int parentProportion, int expectedSteps = 100, Func<int, Task>? reporter = null)
{
this._parentProportion = parentProportion;
MaxProgress = Number.IntPow(10, Number.GetLength(expectedSteps));
StepProgress = MaxProgress / expectedSteps;
ReporterFunc = reporter;
}
private async void ForceReport(int value)
{
try
{
if (ReporterFunc != null)
await ReporterFunc(value);
if (Parent != null)
Parent.Increase((value - _progress) / StepProgress * _parentProportion / (MaxProgress / StepProgress));
_progress = value;
}
catch (OperationCanceledException ex)
{
_errorMessage = ex.Message;
this._status = ProgressStatus.Canceled;
}
catch (Exception ex)
{
_errorMessage = ex.Message;
this._status = ProgressStatus.Failed;
}
}
public void Report(int value)
{
if (this._status == ProgressStatus.Pending)
this._status = ProgressStatus.InProgress;
else if (this.Status != ProgressStatus.InProgress)
return;
if (value > MaxProgress) return;
ForceReport(value);
}
public void Increase(int? value = null)
{
if (this._status == ProgressStatus.Pending)
this._status = ProgressStatus.InProgress;
else if (this.Status != ProgressStatus.InProgress)
return;
if (value.HasValue)
{
if (_progress + value.Value >= MaxProgress) return;
this.Report(_progress + value.Value);
}
else
{
if (_progress + StepProgress >= MaxProgress) return;
this.Report(_progress + StepProgress);
}
}
public void Finish()
{
this._status = ProgressStatus.Completed;
this.ForceReport(MaxProgress);
}
public void Cancel()
{
this._status = ProgressStatus.Canceled;
this._errorMessage = "User Cancelled";
this.ForceReport(_progress);
}
public void Error(string message)
{
this._status = ProgressStatus.Failed;
this._errorMessage = message;
this.ForceReport(_progress);
}
public ProgressReporter CreateChild(int proportion, int expectedSteps = 100)
{
var child = new ProgressReporter(proportion, expectedSteps);
child.Parent = this;
this.Child = child;
return child;
}
}
public class ProgressTrackerService : BackgroundService
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ConcurrentDictionary<string, TaskProgressInfo> _taskMap = new();
private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
private class TaskProgressInfo
{
public ProgressReporter? Reporter { get; set; }
public string? ConnectionId { get; set; }
public required CancellationToken CancellationToken { get; set; }
public required CancellationTokenSource CancellationTokenSource { get; set; }
public required DateTime UpdatedAt { get; set; }
}
public ProgressTrackerService(IHubContext<ProgressHub, IProgressReceiver> hubContext)
{
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var now = DateTime.UtcNow;
foreach (var kvp in _taskMap)
{
var info = kvp.Value;
// 超过 1 分钟且任务已完成/失败/取消
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}");
}
}
}
catch (Exception ex)
{
logger.Error(ex, "Error during ProgressTracker cleanup");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
public (string, ProgressReporter) CreateTask(CancellationToken? cancellationToken = null)
{
CancellationTokenSource? cancellationTokenSource;
if (cancellationToken.HasValue)
{
cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value);
}
else
{
cancellationTokenSource = new CancellationTokenSource();
}
var progressInfo = new TaskProgressInfo
{
ConnectionId = null,
UpdatedAt = DateTime.UtcNow,
CancellationToken = cancellationTokenSource.Token,
CancellationTokenSource = cancellationTokenSource,
};
var progress = new ProgressReporter(async value =>
{
cancellationTokenSource.Token.ThrowIfCancellationRequested();
// 通过 SignalR 推送进度
if (progressInfo.ConnectionId != null && progressInfo.Reporter != null)
await _hubContext.Clients.Client(progressInfo.ConnectionId).OnReceiveProgress(progressInfo.Reporter);
});
progressInfo.Reporter = progress;
_taskMap.TryAdd(progressInfo.Reporter.TaskId, progressInfo);
return (progressInfo.Reporter.TaskId, progress);
}
public Optional<ProgressReporter> GetReporter(string taskId)
{
if (_taskMap.TryGetValue(taskId, out var info))
{
return info.Reporter;
}
return Optional<ProgressReporter>.None;
}
public Optional<ProgressStatus> GetProgressStatus(string taskId)
{
if (_taskMap.TryGetValue(taskId, out var info))
{
if (info.Reporter != null)
return info.Reporter.Status;
}
return Optional<ProgressStatus>.None;
}
public bool BindTask(string taskId, string connectionId)
{
if (_taskMap.TryGetValue(taskId, out var info) && info != null)
{
lock (info)
{
info.ConnectionId = connectionId;
}
return true;
}
return false;
}
public bool CancelTask(string taskId)
{
try
{
if (_taskMap.TryGetValue(taskId, out var info) && info != null && info.Reporter != null)
{
lock (info)
{
info.CancellationTokenSource.Cancel();
info.Reporter.Cancel();
info.UpdatedAt = DateTime.UtcNow;
}
return true;
}
return false;
}
catch (Exception ex)
{
logger.Error(ex, $"Failed to cancel task {taskId}");
return false;
}
}
}

View File

@@ -8,11 +8,11 @@ using server.Services;
/// <summary>
/// UDP客户端发送池
/// </summary>
public class UDPClientPool
public sealed class UDPClientPool
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private static IPAddress localhost = IPAddress.Parse("127.0.0.1");
private static ProgressTracker _progressTracker = MsgBus.ProgressTracker;
/// <summary>
/// 发送字符串
@@ -183,40 +183,6 @@ public class UDPClientPool
return await Task.Run(() => { return SendDataPack(endPoint, pkg); });
}
/// <summary>
/// 发送字符串到本地
/// </summary>
/// <param name="port">端口</param>
/// <param name="stringArray">字符串数组</param>
/// <returns>是否成功</returns>
public static bool SendStringLocalHost(int port, string[] stringArray)
{
return SendString(new IPEndPoint(localhost, port), stringArray);
}
/// <summary>
/// 循环发送字符串到本地
/// </summary>
/// <param name="times">发送总次数</param>
/// <param name="sleepMilliSeconds">间隔时间</param>
/// <param name="port">端口</param>
/// <param name="stringArray">字符串数组</param>
/// <returns>是否成功</returns>
public static bool CycleSendStringLocalHost(int times, int sleepMilliSeconds, int port, string[] stringArray)
{
var isSuccessful = true;
while (times-- >= 0)
{
isSuccessful = SendStringLocalHost(port, stringArray);
if (!isSuccessful) break;
Thread.Sleep(sleepMilliSeconds);
}
return isSuccessful;
}
/// <summary>
/// 读取设备地址数据
/// </summary>
@@ -607,11 +573,11 @@ public class UDPClientPool
/// <param name="devAddr">设备地址</param>
/// <param name="data">要写入的32位数据</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <param name="progress">进度报告器</param>
/// <param name="progressId">进度报告器</param>
/// <returns>写入结果true表示写入成功</returns>
public static async ValueTask<Result<bool>> WriteAddr(
IPEndPoint endPoint, int taskID, UInt32 devAddr,
UInt32 data, int timeout = 1000, ProgressReporter? progress = null)
UInt32 data, int timeout = 1000, string progressId = "")
{
var ret = false;
var opts = new SendAddrPackOptions()
@@ -622,17 +588,18 @@ public class UDPClientPool
Address = devAddr,
IsWrite = true,
};
progress?.Report(20);
_progressTracker.AdvanceProgress(progressId, 10);
// Write Register
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
progress?.Report(40);
_progressTracker.AdvanceProgress(progressId, 10);
// Send Data Package
ret = await UDPClientPool.SendDataPackAsync(endPoint,
new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value));
if (!ret) return new(new Exception("Send data package failed!"));
progress?.Report(60);
_progressTracker.AdvanceProgress(progressId, 10);
// Check Msg Bus
if (!MsgBus.IsRunning)
@@ -642,7 +609,7 @@ public class UDPClientPool
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(
endPoint.Address.ToString(), taskID, endPoint.Port, timeout);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
progress?.Finish();
_progressTracker.AdvanceProgress(progressId, 10);
return udpWriteAck.Value.IsSuccessful;
}
@@ -655,11 +622,11 @@ public class UDPClientPool
/// <param name="devAddr">设备地址</param>
/// <param name="dataArray">要写入的字节数组</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <param name="progress">进度报告器</param>
/// <param name="progressId">进度报告器</param>
/// <returns>写入结果true表示写入成功</returns>
public static async ValueTask<Result<bool>> WriteAddr(
IPEndPoint endPoint, int taskID, UInt32 devAddr,
byte[] dataArray, int timeout = 1000, ProgressReporter? progress = null)
byte[] dataArray, int timeout = 1000, string progressId = "")
{
var ret = false;
var opts = new SendAddrPackOptions()
@@ -681,8 +648,6 @@ public class UDPClientPool
var writeTimes = hasRest ?
dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 :
dataArray.Length / (max4BytesPerRead * (32 / 8));
if (progress != null)
progress.ExpectedSteps = writeTimes;
for (var i = 0; i < writeTimes; i++)
{
// Sperate Data Array
@@ -712,10 +677,9 @@ public class UDPClientPool
if (!udpWriteAck.Value.IsSuccessful)
return false;
progress?.Increase();
_progressTracker.AdvanceProgress(progressId, 1);
}
progress?.Finish();
return true;
}

View File

@@ -6936,6 +6936,255 @@ export class ResourceClient {
}
}
export class SwitchClient {
protected instance: AxiosInstance;
protected baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, instance?: AxiosInstance) {
this.instance = instance || axios.create();
this.baseUrl = baseUrl ?? "http://127.0.0.1:5000";
}
/**
* 启用或禁用 Switch 外设
* @param enable (optional) 是否启用
* @return 操作结果
*/
setEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Switch/enable?";
if (enable === null)
throw new Error("The parameter 'enable' cannot be null.");
else if (enable !== undefined)
url_ += "enable=" + encodeURIComponent("" + enable) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
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.processSetEnable(_response);
});
}
protected processSetEnable(response: AxiosResponse): Promise<boolean> {
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;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 500) {
const _responseText = response.data;
let result500: any = null;
let resultData500 = _responseText;
result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
/**
* 控制指定编号的 Switch 开关
* @param num (optional) 开关编号
* @param onOff (optional) 开/关
* @return 操作结果
*/
setSwitchOnOff(num: number | undefined, onOff: boolean | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Switch/switch?";
if (num === null)
throw new Error("The parameter 'num' cannot be null.");
else if (num !== undefined)
url_ += "num=" + encodeURIComponent("" + num) + "&";
if (onOff === null)
throw new Error("The parameter 'onOff' cannot be null.");
else if (onOff !== undefined)
url_ += "onOff=" + encodeURIComponent("" + onOff) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
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.processSetSwitchOnOff(_response);
});
}
protected processSetSwitchOnOff(response: AxiosResponse): Promise<boolean> {
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;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ArgumentException.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 500) {
const _responseText = response.data;
let result500: any = null;
let resultData500 = _responseText;
result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
/**
* 控制 Switch 开关
* @param keyStatus 开关状态
* @return 操作结果
*/
setMultiSwitchsOnOff(keyStatus: boolean[], cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Switch/MultiSwitch";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(keyStatus);
let options_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
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.processSetMultiSwitchsOnOff(_response);
});
}
protected processSetMultiSwitchsOnOff(response: AxiosResponse): Promise<boolean> {
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;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ArgumentException.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 500) {
const _responseText = response.data;
let result500: any = null;
let resultData500 = _responseText;
result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
}
export class TutorialClient {
protected instance: AxiosInstance;
protected baseUrl: string;

View File

@@ -1,3 +1,6 @@
import { ResourceClient, ResourcePurpose } from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager";
// 定义 diagram.json 的类型结构
export interface DiagramData {
version: number;
@@ -26,40 +29,43 @@ export interface DiagramPart {
// 连接类型定义 - 使用元组类型表示四元素数组
export type ConnectionArray = [string, string, number, string[]];
import { AuthManager } from '@/utils/AuthManager';
// 解析连接字符串为组件ID和引脚ID
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
const [componentId, pinId] = connectionPin.split(':');
export function parseConnectionPin(connectionPin: string): {
componentId: string;
pinId: string;
} {
const [componentId, pinId] = connectionPin.split(":");
return { componentId, pinId };
}
// 将连接数组转换为适用于渲染的格式
export function connectionArrayToWireItem(
connection: ConnectionArray,
index: number,
startPos = { x: 0, y: 0 },
endPos = { x: 0, y: 0 }
connection: ConnectionArray,
index: number,
startPos = { x: 0, y: 0 },
endPos = { x: 0, y: 0 },
): WireItem {
const [startPinStr, endPinStr, width, path] = connection;
const { componentId: startComponentId, pinId: startPinId } = parseConnectionPin(startPinStr);
const { componentId: endComponentId, pinId: endPinId } = parseConnectionPin(endPinStr);
const { componentId: startComponentId, pinId: startPinId } =
parseConnectionPin(startPinStr);
const { componentId: endComponentId, pinId: endPinId } =
parseConnectionPin(endPinStr);
return {
id: `wire-${index}`,
startX: startPos.x,
startY: startPos.y,
endX: endPos.x,
endX: endPos.x,
endY: endPos.y,
startComponentId,
startPinId,
endComponentId,
endPinId,
strokeWidth: width,
color: '#4a5568', // 默认颜色
routingMode: 'path',
color: "#4a5568", // 默认颜色
routingMode: "path",
pathCommands: path,
showLabel: false
showLabel: false,
};
}
@@ -76,7 +82,7 @@ export interface WireItem {
endPinId?: string;
strokeWidth: number;
color: string;
routingMode: 'orthogonal' | 'path';
routingMode: "orthogonal" | "path";
constraint?: string;
pathCommands?: string[];
showLabel: boolean;
@@ -88,58 +94,64 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
// 如果提供了examId优先从API加载实验的diagram
if (examId) {
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resourceClient = AuthManager.createClient(ResourceClient);
// 获取diagram类型的资源列表
const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
const resources = await resourceClient.getResourceList(
examId,
"canvas",
ResourcePurpose.Template,
);
if (resources && resources.length > 0) {
// 获取第一个diagram资源
const diagramResource = resources[0];
// 使用动态API获取资源文件内容
const response = await resourceClient.getResourceById(diagramResource.id);
const response = await resourceClient.getResourceById(
diagramResource.id,
);
if (response && response.data) {
const text = await response.data.text();
const data = JSON.parse(text);
// 验证数据格式
const validation = validateDiagramData(data);
if (validation.isValid) {
console.log('成功从API加载实验diagram:', examId);
console.log("成功从API加载实验diagram:", examId);
return data;
} else {
console.warn('API返回的diagram数据格式无效:', validation.errors);
console.warn("API返回的diagram数据格式无效:", validation.errors);
}
}
} else {
console.log('未找到实验diagram资源使用默认加载方式');
console.log("未找到实验diagram资源使用默认加载方式");
}
} catch (error) {
console.warn('从API加载实验diagram失败使用默认加载方式:', error);
console.warn("从API加载实验diagram失败使用默认加载方式:", error);
}
}
// 如果没有examId或API加载失败尝试从静态文件加载不再使用本地存储
// 从静态文件加载(作为备选方案)
const response = await fetch('/src/components/diagram.json');
const response = await fetch("/src/components/diagram.json");
if (!response.ok) {
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
}
const data = await response.json();
// 验证静态文件数据
const validation = validateDiagramData(data);
if (validation.isValid) {
return data;
} else {
console.warn('静态diagram文件数据格式无效:', validation.errors);
throw new Error('所有diagram数据源都无效');
console.warn("静态diagram文件数据格式无效:", validation.errors);
throw new Error("所有diagram数据源都无效");
}
} catch (error) {
console.error('Error loading diagram data:', error);
console.error("Error loading diagram data:", error);
// 返回空的默认数据结构
return createEmptyDiagram();
}
@@ -149,33 +161,31 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
export function createEmptyDiagram(): DiagramData {
return {
version: 1,
author: 'user',
editor: 'user',
author: "user",
editor: "user",
parts: [],
connections: []
connections: [],
};
}
// 保存图表数据(已禁用本地存储)
export function saveDiagramData(data: DiagramData): void {
// 本地存储功能已禁用 - 不再保存到localStorage
console.debug('saveDiagramData called but localStorage saving is disabled');
console.debug("saveDiagramData called but localStorage saving is disabled");
}
// 更新组件位置
export function updatePartPosition(
data: DiagramData,
partId: string,
x: number,
y: number
data: DiagramData,
partId: string,
x: number,
y: number,
): DiagramData {
return {
...data,
parts: data.parts.map(part =>
part.id === partId
? { ...part, x, y }
: part
)
parts: data.parts.map((part) =>
part.id === partId ? { ...part, x, y } : part,
),
};
}
@@ -184,21 +194,21 @@ export function updatePartAttribute(
data: DiagramData,
partId: string,
attrName: string,
value: any
value: any,
): DiagramData {
return {
...data,
parts: data.parts.map(part =>
part.id === partId
? {
...part,
attrs: {
...part.attrs,
[attrName]: value
}
}
: part
)
parts: data.parts.map((part) =>
part.id === partId
? {
...part,
attrs: {
...part.attrs,
[attrName]: value,
},
}
: part,
),
};
}
@@ -210,72 +220,79 @@ export function addConnection(
endComponentId: string,
endPinId: string,
width: number = 2,
path: string[] = []
path: string[] = [],
): DiagramData {
const newConnection: ConnectionArray = [
`${startComponentId}:${startPinId}`,
`${endComponentId}:${endPinId}`,
width,
path
path,
];
return {
...data,
connections: [...data.connections, newConnection]
connections: [...data.connections, newConnection],
};
}
// 删除连接
export function deleteConnection(
data: DiagramData,
connectionIndex: number
connectionIndex: number,
): DiagramData {
return {
...data,
connections: data.connections.filter((_, index) => index !== connectionIndex)
connections: data.connections.filter(
(_, index) => index !== connectionIndex,
),
};
}
// 查找与组件关联的所有连接
export function findConnectionsByPart(
data: DiagramData,
partId: string
partId: string,
): { connection: ConnectionArray; index: number }[] {
return data.connections
.map((connection, index) => ({ connection, index }))
.filter(({ connection }) => {
const [startPin, endPin] = connection;
const startCompId = startPin.split(':')[0];
const endCompId = endPin.split(':')[0];
const startCompId = startPin.split(":")[0];
const endCompId = endPin.split(":")[0];
return startCompId === partId || endCompId === partId;
});
}
// 添加验证diagram.json文件的函数
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
export function validateDiagramData(data: any): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
// 检查版本号
if (!data.version) {
errors.push('缺少version字段');
errors.push("缺少version字段");
}
// 检查parts数组
if (!Array.isArray(data.parts)) {
errors.push('parts字段不是数组');
errors.push("parts字段不是数组");
} else {
// 验证parts中的每个对象
data.parts.forEach((part: any, index: number) => {
if (!part.id) errors.push(`parts[${index}]缺少id`);
if (!part.type) errors.push(`parts[${index}]缺少type`);
if (typeof part.x !== 'number') errors.push(`parts[${index}]缺少有效的x坐标`);
if (typeof part.y !== 'number') errors.push(`parts[${index}]缺少有效的y坐标`);
if (typeof part.x !== "number")
errors.push(`parts[${index}]缺少有效的x坐标`);
if (typeof part.y !== "number")
errors.push(`parts[${index}]缺少有效的y坐标`);
});
}
// 检查connections数组
if (!Array.isArray(data.connections)) {
errors.push('connections字段不是数组');
errors.push("connections字段不是数组");
} else {
// 验证connections中的每个数组
data.connections.forEach((conn: any, index: number) => {
@@ -283,25 +300,25 @@ export function validateDiagramData(data: any): { isValid: boolean; errors: stri
errors.push(`connections[${index}]不是有效的连接数组`);
return;
}
const [startPin, endPin, width] = conn;
if (typeof startPin !== 'string' || !startPin.includes(':')) {
if (typeof startPin !== "string" || !startPin.includes(":")) {
errors.push(`connections[${index}]的起始针脚格式无效`);
}
if (typeof endPin !== 'string' || !endPin.includes(':')) {
if (typeof endPin !== "string" || !endPin.includes(":")) {
errors.push(`connections[${index}]的结束针脚格式无效`);
}
if (typeof width !== 'number') {
if (typeof width !== "number") {
errors.push(`connections[${index}]的宽度不是有效的数字`);
}
});
}
return {
isValid: errors.length === 0,
errors
errors,
};
}

View File

@@ -32,7 +32,7 @@ export const previewSizes: Record<string, number> = {
EC11RotaryEncoder: 0.4,
Pin: 0.8,
SMT_LED: 0.7,
SevenSegmentDisplay: 0.4,
SevenSegmentDisplayUltimate: 0.4,
HDMI: 0.5,
DDR: 0.5,
ETH: 0.5,
@@ -52,7 +52,7 @@ export const availableComponents: ComponentConfig[] = [
{ type: "EC11RotaryEncoder", name: "EC11旋转编码器" },
{ type: "Pin", name: "引脚" },
{ type: "SMT_LED", name: "贴片LED" },
{ type: "SevenSegmentDisplay", name: "数码管" },
{ type: "SevenSegmentDisplayUltimate", name: "数码管" },
{ type: "HDMI", name: "HDMI接口" },
{ type: "DDR", name: "DDR内存" },
{ type: "ETH", name: "以太网接口" },

View File

@@ -31,8 +31,16 @@ export type Channel = {
// 全局模式选项
const globalModes = [
{value: GlobalCaptureMode.AND,label: "AND",description: "所有条件都满足时触发",},
{value: GlobalCaptureMode.OR,label: "OR",description: "任一条件满足时触发",},
{
value: GlobalCaptureMode.AND,
label: "AND",
description: "所有条件都满足时触发",
},
{
value: GlobalCaptureMode.OR,
label: "OR",
description: "任一条件满足时触发",
},
{ value: GlobalCaptureMode.NAND, label: "NAND", description: "AND的非" },
{ value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" },
];
@@ -70,21 +78,53 @@ const channelDivOptions = [
];
const ClockDivOptions = [
{ value: AnalyzerClockDiv.DIV1, label: "120MHz", description: "采样频率120MHz" },
{ value: AnalyzerClockDiv.DIV2, label: "60MHz", description: "采样频率60MHz" },
{ value: AnalyzerClockDiv.DIV4, label: "30MHz", description: "采样频率30MHz" },
{ value: AnalyzerClockDiv.DIV8, label: "15MHz", description: "采样频率15MHz" },
{ value: AnalyzerClockDiv.DIV16, label: "7.5MHz", description: "采样频率7.5MHz" },
{ value: AnalyzerClockDiv.DIV32, label: "3.75MHz", description: "采样频率3.75MHz" },
{ value: AnalyzerClockDiv.DIV64, label: "1.875MHz", description: "采样频率1.875MHz" },
{ value: AnalyzerClockDiv.DIV128, label: "937.5KHz", description: "采样频率937.5KHz" },
{
value: AnalyzerClockDiv.DIV1,
label: "120MHz",
description: "采样频率120MHz",
},
{
value: AnalyzerClockDiv.DIV2,
label: "60MHz",
description: "采样频率60MHz",
},
{
value: AnalyzerClockDiv.DIV4,
label: "30MHz",
description: "采样频率30MHz",
},
{
value: AnalyzerClockDiv.DIV8,
label: "15MHz",
description: "采样频率15MHz",
},
{
value: AnalyzerClockDiv.DIV16,
label: "7.5MHz",
description: "采样频率7.5MHz",
},
{
value: AnalyzerClockDiv.DIV32,
label: "3.75MHz",
description: "采样频率3.75MHz",
},
{
value: AnalyzerClockDiv.DIV64,
label: "1.875MHz",
description: "采样频率1.875MHz",
},
{
value: AnalyzerClockDiv.DIV128,
label: "937.5KHz",
description: "采样频率937.5KHz",
},
];
// 捕获深度限制常量
const CAPTURE_LENGTH_MIN = 1024; // 最小捕获深度 1024
const CAPTURE_LENGTH_MAX = 0x10000000 - 0x01000000; // 最大捕获深度
// 预捕获深度限制常量
// 预捕获深度限制常量
const PRE_CAPTURE_LENGTH_MIN = 2; // 最小预捕获深度 2
// 默认颜色数组
@@ -170,40 +210,64 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 转换通道数字到枚举值
const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
switch (channelCount) {
case 1: return AnalyzerChannelDiv.ONE;
case 2: return AnalyzerChannelDiv.TWO;
case 4: return AnalyzerChannelDiv.FOUR;
case 8: return AnalyzerChannelDiv.EIGHT;
case 16: return AnalyzerChannelDiv.XVI;
case 32: return AnalyzerChannelDiv.XXXII;
default: return AnalyzerChannelDiv.EIGHT;
case 1:
return AnalyzerChannelDiv.ONE;
case 2:
return AnalyzerChannelDiv.TWO;
case 4:
return AnalyzerChannelDiv.FOUR;
case 8:
return AnalyzerChannelDiv.EIGHT;
case 16:
return AnalyzerChannelDiv.XVI;
case 32:
return AnalyzerChannelDiv.XXXII;
default:
return AnalyzerChannelDiv.EIGHT;
}
};
// 验证捕获深度
const validateCaptureLength = (value: number): { valid: boolean; message?: string } => {
const validateCaptureLength = (
value: number,
): { valid: boolean; message?: string } => {
if (!Number.isInteger(value)) {
return { valid: false, message: "捕获深度必须是整数" };
}
if (value < CAPTURE_LENGTH_MIN) {
return { valid: false, message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}` };
return {
valid: false,
message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}`,
};
}
if (value > CAPTURE_LENGTH_MAX) {
return { valid: false, message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}` };
return {
valid: false,
message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}`,
};
}
return { valid: true };
};
// 验证预捕获深度
const validatePreCaptureLength = (value: number, currentCaptureLength: number): { valid: boolean; message?: string } => {
const validatePreCaptureLength = (
value: number,
currentCaptureLength: number,
): { valid: boolean; message?: string } => {
if (!Number.isInteger(value)) {
return { valid: false, message: "预捕获深度必须是整数" };
}
if (value < PRE_CAPTURE_LENGTH_MIN) {
return { valid: false, message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}` };
return {
valid: false,
message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}`,
};
}
if (value >= currentCaptureLength) {
return { valid: false, message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})` };
return {
valid: false,
message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})`,
};
}
return { valid: true };
};
@@ -215,13 +279,13 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
alert?.error(validation.message!, 3000);
return false;
}
// 检查预捕获深度是否仍然有效
if (preCaptureLength.value >= value) {
preCaptureLength.value = Math.max(0, value - 1);
alert?.warn(`预捕获深度已自动调整为 ${preCaptureLength.value}`, 3000);
}
captureLength.value = value;
return true;
};
@@ -233,7 +297,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
alert?.error(validation.message!, 3000);
return false;
}
preCaptureLength.value = value;
return true;
};
@@ -241,12 +305,12 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 设置通道组
const setChannelDiv = (channelCount: number) => {
// 验证通道数量是否有效
if (!channelDivOptions.find(option => option.value === channelCount)) {
if (!channelDivOptions.find((option) => option.value === channelCount)) {
console.error(`无效的通道组设置: ${channelCount}`);
return;
}
currentChannelDiv.value = channelCount;
// 禁用所有通道
channels.forEach((channel) => {
channel.enabled = false;
@@ -257,7 +321,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
channels[i].enabled = true;
}
const option = channelDivOptions.find(opt => opt.value === channelCount);
const option = channelDivOptions.find(
(opt) => opt.value === channelCount,
);
alert?.success(`已设置为${option?.label}`, 2000);
};
@@ -294,7 +360,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
const getCaptureData = async () => {
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
const client = AuthManager.createClient(LogicAnalyzerClient);
// 获取捕获数据,使用当前设置的捕获长度
const base64Data = await client.getCaptureData(captureLength.value);
@@ -308,7 +374,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 根据当前通道数量解析数据
const channelCount = currentChannelDiv.value;
const timeStepNs = currentSamplePeriodNs.value;
let sampleCount: number;
let x: number[];
let y: number[][];
@@ -316,19 +382,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
if (channelCount === 1) {
// 1通道每个字节包含8个时间单位的数据
sampleCount = bytes.length * 8;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建通道数据数组
y = Array.from(
{ length: 1 },
() => new Array(sampleCount),
);
y = Array.from({ length: 1 }, () => new Array(sampleCount));
// 解析数据每个字节的8个位对应8个时间单位
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
const byte = bytes[byteIndex];
@@ -340,19 +403,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
} else if (channelCount === 2) {
// 2通道每个字节包含4个时间单位的数据
sampleCount = bytes.length * 4;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建通道数据数组
y = Array.from(
{ length: 2 },
() => new Array(sampleCount),
);
y = Array.from({ length: 2 }, () => new Array(sampleCount));
// 解析数据每个字节的8个位对应4个时间单位的2通道数据
// 位分布:[T3_CH1, T3_CH0, T2_CH1, T2_CH0, T1_CH1, T1_CH0, T0_CH1, T0_CH0]
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
@@ -360,37 +420,34 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
for (let timeUnit = 0; timeUnit < 4; timeUnit++) {
const timeIndex = byteIndex * 4 + timeUnit;
const bitOffset = timeUnit * 2;
y[0][timeIndex] = (byte >> bitOffset) & 1; // CH0
y[0][timeIndex] = (byte >> bitOffset) & 1; // CH0
y[1][timeIndex] = (byte >> (bitOffset + 1)) & 1; // CH1
}
}
} else if (channelCount === 4) {
// 4通道每个字节包含2个时间单位的数据
sampleCount = bytes.length * 2;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建通道数据数组
y = Array.from(
{ length: 4 },
() => new Array(sampleCount),
);
y = Array.from({ length: 4 }, () => new Array(sampleCount));
// 解析数据每个字节的8个位对应2个时间单位的4通道数据
// 位分布:[T1_CH3, T1_CH2, T1_CH1, T1_CH0, T0_CH3, T0_CH2, T0_CH1, T0_CH0]
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
const byte = bytes[byteIndex];
// 处理第一个时间单位低4位
const timeIndex1 = byteIndex * 2;
for (let channel = 0; channel < 4; channel++) {
y[channel][timeIndex1] = (byte >> channel) & 1;
}
// 处理第二个时间单位高4位
const timeIndex2 = byteIndex * 2 + 1;
for (let channel = 0; channel < 4; channel++) {
@@ -400,19 +457,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
} else if (channelCount === 8) {
// 8通道每个字节包含1个时间单位的8个通道数据
sampleCount = bytes.length;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建8个通道的数据
y = Array.from(
{ length: 8 },
() => new Array(sampleCount),
);
y = Array.from({ length: 8 }, () => new Array(sampleCount));
// 解析每个字节的8个位到对应通道
for (let i = 0; i < sampleCount; i++) {
const byte = bytes[i];
@@ -424,30 +478,27 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
} else if (channelCount === 16) {
// 16通道每2个字节包含1个时间单位的16个通道数据
sampleCount = bytes.length / 2;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建16个通道的数据
y = Array.from(
{ length: 16 },
() => new Array(sampleCount),
);
y = Array.from({ length: 16 }, () => new Array(sampleCount));
// 解析数据每2个字节为一个时间单位
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
const byteIndex = timeIndex * 2;
const byte1 = bytes[byteIndex]; // [7:0]
const byte1 = bytes[byteIndex]; // [7:0]
const byte2 = bytes[byteIndex + 1]; // [15:8]
// 处理低8位通道 [7:0]
for (let channel = 0; channel < 8; channel++) {
y[channel][timeIndex] = (byte1 >> channel) & 1;
}
// 处理高8位通道 [15:8]
for (let channel = 0; channel < 8; channel++) {
y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
@@ -456,42 +507,39 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
} else if (channelCount === 32) {
// 32通道每4个字节包含1个时间单位的32个通道数据
sampleCount = bytes.length / 4;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建32个通道的数据
y = Array.from(
{ length: 32 },
() => new Array(sampleCount),
);
y = Array.from({ length: 32 }, () => new Array(sampleCount));
// 解析数据每4个字节为一个时间单位
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
const byteIndex = timeIndex * 4;
const byte1 = bytes[byteIndex]; // [7:0]
const byte1 = bytes[byteIndex]; // [7:0]
const byte2 = bytes[byteIndex + 1]; // [15:8]
const byte3 = bytes[byteIndex + 2]; // [23:16]
const byte4 = bytes[byteIndex + 3]; // [31:24]
// 处理 [7:0]
for (let channel = 0; channel < 8; channel++) {
y[channel][timeIndex] = (byte1 >> channel) & 1;
}
// 处理 [15:8]
for (let channel = 0; channel < 8; channel++) {
y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
}
// 处理 [23:16]
for (let channel = 0; channel < 8; channel++) {
y[channel + 16][timeIndex] = (byte3 >> channel) & 1;
}
// 处理 [31:24]
for (let channel = 0; channel < 8; channel++) {
y[channel + 24][timeIndex] = (byte4 >> channel) & 1;
@@ -525,11 +573,11 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
isCapturing.value = true;
const release = await operationMutex.acquire();
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
const client = AuthManager.createClient(LogicAnalyzerClient);
// 1. 先应用配置
alert?.info("正在应用配置...", 2000);
// 准备配置数据 - 包含所有32个通道未启用的通道设置为默认值
const allSignals = signalConfigs.map((signal, index) => {
if (channels[index].enabled) {
@@ -632,7 +680,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
const release = await operationMutex.acquire();
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
const client = AuthManager.createClient(LogicAnalyzerClient);
// 执行强制捕获来停止当前捕获
const forceSuccess = await client.setCaptureMode(false, false);
@@ -661,7 +709,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
const release = await operationMutex.acquire();
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
const client = AuthManager.createClient(LogicAnalyzerClient);
// 执行强制捕获来停止当前捕获
const forceSuccess = await client.setCaptureMode(true, true);
@@ -677,7 +725,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
`强制捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
3000,
);
} finally{
} finally {
release();
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -145,6 +145,7 @@ import {
ChevronDownIcon,
} from "lucide-vue-next";
import { AuthManager } from "@/utils/AuthManager";
import { DataClient } from "@/APIClient";
const router = useRouter();
@@ -158,7 +159,7 @@ const loadUserInfo = async () => {
try {
const authenticated = await AuthManager.isAuthenticated();
if (authenticated) {
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
const userInfo = await client.getUserInfo();
userName.value = userInfo.name;
isLoggedIn.value = true;

View File

@@ -4,6 +4,7 @@ import { Mutex } from "async-mutex";
import {
OscilloscopeFullConfig,
OscilloscopeDataResponse,
OscilloscopeApiClient,
} from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager";
import { useAlertStore } from "@/components/Alert";
@@ -31,257 +32,269 @@ const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
});
// 采样频率常量(后端返回)
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(() => {
const oscData = shallowRef<OscilloscopeDataType>();
const alert = useRequiredInjection(useAlertStore);
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
() => {
const oscData = shallowRef<OscilloscopeDataType>();
const alert = useRequiredInjection(useAlertStore);
// 互斥锁
const operationMutex = new Mutex();
// 互斥锁
const operationMutex = new Mutex();
// 状态
const isApplying = ref(false);
const isCapturing = ref(false);
// 状态
const isApplying = ref(false);
const isCapturing = ref(false);
// 配置
const config = reactive<OscilloscopeFullConfig>(new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }));
// 配置
const config = reactive<OscilloscopeFullConfig>(
new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }),
);
// 采样点数(由后端数据决定)
const sampleCount = ref(0);
// 采样点数(由后端数据决定)
const sampleCount = ref(0);
// 采样周期ns由adFrequency计算
const samplePeriodNs = computed(() =>
oscData.value?.adFrequency ? 1_000_000_000 / oscData.value.adFrequency : 200
);
// 采样周期ns由adFrequency计算
const samplePeriodNs = computed(() =>
oscData.value?.adFrequency
? 1_000_000_000 / oscData.value.adFrequency
: 200,
);
// 应用配置
const applyConfiguration = async () => {
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
const release = await operationMutex.acquire();
isApplying.value = true;
try {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
const success = await client.initialize({ ...config });
if (success) {
alert.success("示波器配置已应用", 2000);
} else {
throw new Error("应用失败");
// 应用配置
const applyConfiguration = async () => {
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
} catch (error) {
alert.error("应用配置失败", 3000);
} finally {
isApplying.value = false;
release();
}
};
// 重置配置
const resetConfiguration = () => {
Object.assign(config, { ...DEFAULT_CONFIG });
alert.info("配置已重置", 2000);
};
const clearOscilloscopeData = () => {
oscData.value = undefined;
}
// 获取数据
const getOscilloscopeData = async () => {
try {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
const resp: OscilloscopeDataResponse = await client.getData();
// 解析波形数据
const binaryString = atob(resp.waveformData);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
const release = await operationMutex.acquire();
isApplying.value = true;
try {
const client = AuthManager.createClient(OscilloscopeApiClient);
const success = await client.initialize({ ...config });
if (success) {
alert.success("示波器配置已应用", 2000);
} else {
throw new Error("应用失败");
}
} catch (error) {
alert.error("应用配置失败", 3000);
} finally {
isApplying.value = false;
release();
}
sampleCount.value = bytes.length;
};
// 构建时间轴
// 重置配置
const resetConfiguration = () => {
Object.assign(config, { ...DEFAULT_CONFIG });
alert.info("配置已重置", 2000);
};
const clearOscilloscopeData = () => {
oscData.value = undefined;
};
// 获取数据
const getOscilloscopeData = async () => {
try {
const client = AuthManager.createClient(OscilloscopeApiClient);
const resp: OscilloscopeDataResponse = await client.getData();
// 解析波形数据
const binaryString = atob(resp.waveformData);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
sampleCount.value = bytes.length;
// 构建时间轴
const x = Array.from(
{ length: bytes.length },
(_, i) => (i * samplePeriodNs.value) / 1000, // us
);
const y = Array.from(bytes);
oscData.value = {
x,
y,
xUnit: "us",
yUnit: "V",
adFrequency: resp.adFrequency,
adVpp: resp.adVpp,
adMax: resp.adMax,
adMin: resp.adMin,
};
} catch (error) {
alert.error("获取示波器数据失败", 3000);
}
};
// 定时器引用
let refreshIntervalId: number | undefined;
// 刷新间隔(毫秒),可根据需要调整
const refreshIntervalMs = ref(1000);
// 定时刷新函数
const startAutoRefresh = () => {
if (refreshIntervalId !== undefined) return;
refreshIntervalId = window.setInterval(async () => {
await refreshRAM();
await getOscilloscopeData();
}, refreshIntervalMs.value);
};
const stopAutoRefresh = () => {
if (refreshIntervalId !== undefined) {
clearInterval(refreshIntervalId);
refreshIntervalId = undefined;
isCapturing.value = false;
}
};
// 启动捕获
const startCapture = async () => {
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
isCapturing.value = true;
const release = await operationMutex.acquire();
try {
const client = AuthManager.createClient(OscilloscopeApiClient);
const started = await client.startCapture();
if (!started) throw new Error("无法启动捕获");
alert.info("开始捕获...", 2000);
// 启动定时刷新
startAutoRefresh();
} catch (error) {
alert.error("捕获失败", 3000);
isCapturing.value = false;
stopAutoRefresh();
} finally {
release();
}
};
// 停止捕获
const stopCapture = async () => {
if (!isCapturing.value) {
alert.warn("当前没有正在进行的捕获操作", 2000);
return;
}
isCapturing.value = false;
stopAutoRefresh();
const release = await operationMutex.acquire();
try {
const client = AuthManager.createClient(OscilloscopeApiClient);
const stopped = await client.stopCapture();
if (!stopped) throw new Error("无法停止捕获");
alert.info("捕获已停止", 2000);
} catch (error) {
alert.error("停止捕获失败", 3000);
} finally {
release();
}
};
// 更新触发参数
const updateTrigger = async (level: number, risingEdge: boolean) => {
const client = AuthManager.createClient(OscilloscopeApiClient);
try {
const ok = await client.updateTrigger(level, risingEdge);
if (ok) {
config.triggerLevel = level;
config.triggerRisingEdge = risingEdge;
alert.success("触发参数已更新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("更新触发参数失败", 2000);
}
};
// 更新采样参数
const updateSampling = async (
horizontalShift: number,
decimationRate: number,
) => {
const client = AuthManager.createClient(OscilloscopeApiClient);
try {
const ok = await client.updateSampling(horizontalShift, decimationRate);
if (ok) {
config.horizontalShift = horizontalShift;
config.decimationRate = decimationRate;
alert.success("采样参数已更新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("更新采样参数失败", 2000);
}
};
// 手动刷新RAM
const refreshRAM = async () => {
const client = AuthManager.createClient(OscilloscopeApiClient);
try {
const ok = await client.refreshRAM();
if (ok) {
// alert.success("RAM已刷新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("刷新RAM失败", 2000);
}
};
// 生成测试数据
const generateTestData = () => {
const freq = 5_000_000;
const duration = 0.001; // 1ms
const points = Math.floor(freq * duration);
const x = Array.from(
{ length: bytes.length },
(_, i) => (i * samplePeriodNs.value) / 1000 // us
{ length: points },
(_, i) => (i * 1_000_000_000) / freq / 1000,
);
const y = Array.from({ length: points }, (_, i) =>
Math.floor(Math.sin(i * 0.01) * 127 + 128),
);
const y = Array.from(bytes);
oscData.value = {
x,
y,
xUnit: "us",
yUnit: "V",
adFrequency: resp.adFrequency,
adVpp: resp.adVpp,
adMax: resp.adMax,
adMin: resp.adMin,
adFrequency: freq,
adVpp: 2.0,
adMax: 255,
adMin: 0,
};
} catch (error) {
alert.error("获取示波器数据失败", 3000);
}
};
// 定时器引用
let refreshIntervalId: number | undefined;
// 刷新间隔(毫秒),可根据需要调整
const refreshIntervalMs = ref(1000);
// 定时刷新函数
const startAutoRefresh = () => {
if (refreshIntervalId !== undefined) return;
refreshIntervalId = window.setInterval(async () => {
await refreshRAM();
await getOscilloscopeData();
}, refreshIntervalMs.value);
};
const stopAutoRefresh = () => {
if (refreshIntervalId !== undefined) {
clearInterval(refreshIntervalId);
refreshIntervalId = undefined;
isCapturing.value = false;
}
};
// 启动捕获
const startCapture = async () => {
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
isCapturing.value = true;
const release = await operationMutex.acquire();
try {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
const started = await client.startCapture();
if (!started) throw new Error("无法启动捕获");
alert.info("开始捕获...", 2000);
// 启动定时刷新
startAutoRefresh();
} catch (error) {
alert.error("捕获失败", 3000);
isCapturing.value = false;
stopAutoRefresh();
} finally {
release();
}
};
// 停止捕获
const stopCapture = async () => {
if (!isCapturing.value) {
alert.warn("当前没有正在进行的捕获操作", 2000);
return;
}
isCapturing.value = false;
stopAutoRefresh();
const release = await operationMutex.acquire();
try {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
const stopped = await client.stopCapture();
if (!stopped) throw new Error("无法停止捕获");
alert.info("捕获已停止", 2000);
} catch (error) {
alert.error("停止捕获失败", 3000);
} finally {
release();
}
};
// 更新触发参数
const updateTrigger = async (level: number, risingEdge: boolean) => {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
try {
const ok = await client.updateTrigger(level, risingEdge);
if (ok) {
config.triggerLevel = level;
config.triggerRisingEdge = risingEdge;
alert.success("触发参数已更新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("更新触发参数失败", 2000);
}
};
// 更新采样参数
const updateSampling = async (horizontalShift: number, decimationRate: number) => {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
try {
const ok = await client.updateSampling(horizontalShift, decimationRate);
if (ok) {
config.horizontalShift = horizontalShift;
config.decimationRate = decimationRate;
alert.success("采样参数已更新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("更新采样参数失败", 2000);
}
};
// 手动刷新RAM
const refreshRAM = async () => {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
try {
const ok = await client.refreshRAM();
if (ok) {
// alert.success("RAM已刷新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("刷新RAM失败", 2000);
}
};
// 生成测试数据
const generateTestData = () => {
const freq = 5_000_000;
const duration = 0.001; // 1ms
const points = Math.floor(freq * duration);
const x = Array.from({ length: points }, (_, i) => (i * 1_000_000_000 / freq) / 1000);
const y = Array.from({ length: points }, (_, i) =>
Math.floor(Math.sin(i * 0.01) * 127 + 128)
);
oscData.value = {
x,
y,
xUnit: "us",
yUnit: "V",
adFrequency: freq,
adVpp: 2.0,
adMax: 255,
adMin: 0,
alert.success("测试数据生成成功", 2000);
};
alert.success("测试数据生成成功", 2000);
};
return {
oscData,
config,
isApplying,
isCapturing,
sampleCount,
samplePeriodNs,
refreshIntervalMs,
return {
oscData,
config,
isApplying,
isCapturing,
sampleCount,
samplePeriodNs,
refreshIntervalMs,
applyConfiguration,
resetConfiguration,
clearOscilloscopeData,
getOscilloscopeData,
startCapture,
stopCapture,
updateTrigger,
updateSampling,
refreshRAM,
generateTestData,
};
});
applyConfiguration,
resetConfiguration,
clearOscilloscopeData,
getOscilloscopeData,
startCapture,
stopCapture,
updateTrigger,
updateSampling,
refreshRAM,
generateTestData,
};
},
);
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };

View File

@@ -81,7 +81,7 @@
import { ref, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { AuthManager } from "@/utils/AuthManager";
import type { ExamInfo } from "@/APIClient";
import { ExamClient, ResourceClient, type ExamInfo } from "@/APIClient";
// 接口定义
interface Tutorial {
@@ -121,7 +121,7 @@ onMounted(async () => {
console.log("正在从数据库加载实验数据...");
// 创建认证客户端
const client = AuthManager.createAuthenticatedExamClient();
const client = AuthManager.createClient(ExamClient);
// 获取实验列表
const examList: ExamInfo[] = await client.getExamList();
@@ -142,7 +142,7 @@ onMounted(async () => {
try {
// 获取实验的封面资源(模板资源)
const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resourceClient = AuthManager.createClient(ResourceClient);
const resourceList = await resourceClient.getResourceList(
exam.id,
"cover",

View File

@@ -16,22 +16,32 @@
<span class="text-sm">{{ bitstream.name }}</span>
<div class="flex gap-2">
<button
@click="downloadExampleBitstream(bitstream)"
@click="handleExampleBitstream('download', bitstream)"
class="btn btn-sm btn-secondary"
:disabled="isDownloading || isProgramming"
:disabled="currentTask !== 'none'"
>
<div v-if="isDownloading">
<div
v-if="
currentTask === 'downloading' &&
currentBitstreamId === bitstream.id
"
>
<span class="loading loading-spinner loading-xs"></span>
{{ downloadProgress }}%
下载中...
</div>
<div v-else>下载示例</div>
</button>
<button
@click="programExampleBitstream(bitstream)"
@click="handleExampleBitstream('program', bitstream)"
class="btn btn-sm btn-primary"
:disabled="isDownloading || isProgramming"
:disabled="currentTask !== 'none'"
>
<div v-if="isProgramming">
<div
v-if="
currentTask === 'programming' &&
currentBitstreamId === bitstream.id
"
>
<span class="loading loading-spinner loading-xs"></span>
烧录中...
</div>
@@ -63,14 +73,18 @@
<!-- Upload Button -->
<div class="card-actions w-full">
<button
@click="handleClick"
@click="handleUploadAndDownload"
class="btn btn-primary grow"
:disabled="isUploading || isProgramming"
:disabled="currentTask !== 'none'"
>
<div v-if="isUploading">
<div v-if="currentTask === 'uploading'">
<span class="loading loading-spinner"></span>
上传中...
</div>
<div v-else-if="currentTask === 'programming'">
<span class="loading loading-spinner"></span>
{{ currentProgressPercent }}% ...
</div>
<div v-else>上传并下载</div>
</button>
</div>
@@ -78,27 +92,19 @@
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef, onMounted } from "vue";
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
import { ref, useTemplateRef, onMounted } from "vue";
import { AuthManager } from "@/utils/AuthManager";
import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash";
import { useEquipments } from "@/stores/equipments";
import type { HubConnection } from "@microsoft/signalr";
import type {
IProgressHub,
IProgressReceiver,
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
import {
getHubProxyFactory,
getReceiverRegister,
} from "@/utils/signalR/TypedSignalR.Client";
import { ProgressStatus } from "@/utils/signalR/server.Hubs";
import { useRequiredInjection } from "@/utils/Common";
import { useAlertStore } from "./Alert";
import { ResourceClient, ResourcePurpose } from "@/APIClient";
import { useProgressStore } from "@/stores/progress";
import { ProgressStatus, type ProgressInfo } from "@/utils/signalR/server.Hubs";
interface Props {
maxMemory?: number;
examId?: string; // 新增examId属性
examId?: string;
}
const props = withDefaults(defineProps<Props>(), {
@@ -111,203 +117,166 @@ const emits = defineEmits<{
}>();
const alert = useRequiredInjection(useAlertStore);
const progressTracker = useProgressStore();
const dialog = useDialogStore();
const eqps = useEquipments();
const isUploading = ref(false);
const isDownloading = ref(false);
const isProgramming = ref(false);
const availableBitstreams = ref<{ id: number; name: string }[]>([]);
// Progress
const downloadTaskId = ref("");
const downloadProgress = ref(0);
const progressHubConnection = ref<HubConnection>();
const progressHubProxy = ref<IProgressHub>();
const progressHubReceiver: IProgressReceiver = {
onReceiveProgress: async (msg) => {
if (msg.taskId == downloadTaskId.value) {
if (msg.status == ProgressStatus.InProgress) {
downloadProgress.value = msg.progressPercent;
} else if (msg.status == ProgressStatus.Failed) {
dialog.error(msg.errorMessage);
} else if (msg.status == ProgressStatus.Completed) {
alert.info("比特流下载成功");
}
}
},
};
onMounted(async () => {
progressHubConnection.value =
AuthManager.createAuthenticatedProgressHubConnection();
progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy(
progressHubConnection.value,
);
getReceiverRegister("IProgressReceiver").register(
progressHubConnection.value,
progressHubReceiver,
);
});
const availableBitstreams = ref<{ id: string; name: string }[]>([]);
const fileInput = useTemplateRef("fileInput");
const bitstream = defineModel("bitstreamFile", {
type: File,
default: undefined,
});
const bitstream = ref<File | undefined>(undefined);
// 用一个状态变量替代多个
const currentTask = ref<"none" | "uploading" | "downloading" | "programming">(
"none",
);
const currentBitstreamId = ref<string>("");
const currentProgressId = ref<string>("");
const currentProgressPercent = ref<number>(0);
// 初始化时加载示例比特流
onMounted(async () => {
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
if (bitstream.value && fileInput.value) {
let fileList = new DataTransfer();
fileList.items.add(bitstream.value);
fileInput.value.files = fileList.files;
}
await loadAvailableBitstreams();
});
// 加载可用的比特流文件列表
async function loadAvailableBitstreams() {
console.log("加载可用比特流文件examId:", props.examId);
if (!props.examId) {
availableBitstreams.value = [];
return;
}
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取比特流模板资源列表
const resources = await resourceClient.getResourceList(
props.examId,
"bitstream",
"template",
);
availableBitstreams.value =
resources.map((r) => ({ id: r.id, name: r.name })) || [];
} catch (error) {
console.error("加载比特流列表失败:", error);
availableBitstreams.value = [];
}
}
// 下载示例比特流
async function downloadExampleBitstream(bitstream: {
id: number;
name: string;
}) {
if (isDownloading.value) return;
isDownloading.value = true;
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取资源文件
const response = await resourceClient.getResourceById(bitstream.id);
if (response && response.data) {
// 创建下载链接
const url = URL.createObjectURL(response.data);
const link = document.createElement("a");
link.href = url;
link.download = response.fileName || bitstream.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
dialog.info("示例比特流下载成功");
} else {
dialog.error("下载失败:响应数据为空");
}
} catch (error) {
console.error("下载示例比特流失败:", error);
dialog.error("下载示例比特流失败");
} finally {
isDownloading.value = false;
}
}
// 直接烧录示例比特流
async function programExampleBitstream(bitstream: {
id: number;
name: string;
}) {
if (isProgramming.value) return;
isProgramming.value = true;
try {
const downloadTaskId = await eqps.jtagDownloadBitstream(bitstream.id);
} catch (error) {
console.error("烧录示例比特流失败:", error);
dialog.error("烧录示例比特流失败");
} finally {
isProgramming.value = false;
}
}
function handleFileChange(event: Event): void {
const target = event.target as HTMLInputElement;
const file = target.files?.[0]; // 获取选中的第一个文件
if (!file) {
return;
}
bitstream.value = file;
const file = target.files?.[0];
bitstream.value = file || undefined;
}
function checkFile(file: File): boolean {
const maxBytes = props.maxMemory! * 1024 * 1024; // 将最大容量从 MB 转换为字节
if (file.size > maxBytes) {
function checkFileInput(): boolean {
if (!bitstream.value) {
dialog.error(`未选择文件`);
return false;
}
const maxBytes = props.maxMemory! * 1024 * 1024;
if (bitstream.value.size > maxBytes) {
dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
return false;
}
return true;
}
async function handleClick(event: Event): Promise<void> {
console.log("上传按钮被点击");
if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
dialog.error(`未选择文件`);
async function downloadBitstream() {
currentTask.value = "programming";
try {
currentProgressId.value = await eqps.jtagDownloadBitstream(
currentBitstreamId.value,
);
progressTracker.register(
currentProgressId.value,
"programBitstream",
handleProgressUpdate,
);
} catch {
dialog.error("比特流烧录失败");
cleanProgressTracker();
}
}
function cleanProgressTracker() {
currentTask.value = "none";
currentProgressId.value = "";
currentBitstreamId.value = "";
currentProgressPercent.value = 0;
progressTracker.unregister(currentProgressId.value, "programBitstream");
}
async function loadAvailableBitstreams() {
if (!props.examId) {
availableBitstreams.value = [];
return;
}
if (!checkFile(bitstream.value)) return;
isUploading.value = true;
let uploadedBitstreamId: number | null = null;
try {
console.log("开始上传比特流文件:", bitstream.value.name);
const bitstreamId = await eqps.jtagUploadBitstream(
bitstream.value,
const resourceClient = AuthManager.createClient(ResourceClient);
const resources = await resourceClient.getResourceList(
props.examId,
"bitstream",
ResourcePurpose.Template,
);
availableBitstreams.value =
resources.map((r) => ({ id: r.id, name: r.name })) || [];
} catch (error) {
availableBitstreams.value = [];
}
}
// 统一处理示例比特流的下载/烧录
async function handleExampleBitstream(
action: "download" | "program",
bitstreamObj: { id: string; name: string },
) {
if (currentTask.value !== "none") return;
currentBitstreamId.value = bitstreamObj.id;
if (action === "download") {
currentTask.value = "downloading";
try {
const resourceClient = AuthManager.createClient(ResourceClient);
const response = await resourceClient.getResourceById(bitstreamObj.id);
if (response && response.data) {
const url = URL.createObjectURL(response.data);
const link = document.createElement("a");
link.href = url;
link.download = response.fileName || bitstreamObj.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
alert.info("示例比特流下载成功");
} else {
alert.error("下载失败:响应数据为空");
}
} catch {
alert.error("下载示例比特流失败");
} finally {
currentTask.value = "none";
currentBitstreamId.value = "";
}
} else if (action === "program") {
currentBitstreamId.value = bitstreamObj.id;
await downloadBitstream();
}
}
// 上传并下载
async function handleUploadAndDownload() {
if (currentTask.value !== "none") return;
if (!checkFileInput()) return;
currentTask.value = "uploading";
let uploadedBitstreamId: string | null = null;
try {
uploadedBitstreamId = await eqps.jtagUploadBitstream(
bitstream.value!,
props.examId || "",
);
console.log("上传结果ID:", bitstreamId);
if (bitstreamId === null || bitstreamId === undefined) {
isUploading.value = false;
return;
}
uploadedBitstreamId = bitstreamId;
} catch (e) {
if (!uploadedBitstreamId) throw new Error("上传失败");
emits("finishedUpload", bitstream.value!);
} catch {
dialog.error("上传失败");
console.error(e);
currentTask.value = "none";
return;
}
isUploading.value = false;
// Download
try {
console.log("开始下载比特流ID:", uploadedBitstreamId);
if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
dialog.error("uploadedBitstreamId is null or undefined");
} else {
isDownloading.value = true;
downloadTaskId.value =
await eqps.jtagDownloadBitstream(uploadedBitstreamId);
}
} catch (e) {
dialog.error("下载失败");
console.error(e);
currentBitstreamId.value = uploadedBitstreamId;
await downloadBitstream();
}
function handleProgressUpdate(msg: ProgressInfo) {
// console.log(msg);
if (msg.status === ProgressStatus.Running)
currentProgressPercent.value = msg.progressPercent;
else if (msg.status === ProgressStatus.Failed) {
dialog.error(`比特流烧录失败: ${msg.errorMessage}`);
cleanProgressTracker();
} else if (msg.status === ProgressStatus.Completed) {
dialog.info("比特流烧录成功");
cleanProgressTracker();
}
}
</script>

View File

@@ -212,6 +212,7 @@ import { useEquipments } from "@/stores/equipments";
import { useDialogStore } from "@/stores/dialog";
import { toInteger } from "lodash";
import { AuthManager } from "@/utils/AuthManager";
import { DDSClient } from "@/APIClient";
// Component Attributes
const props = defineProps<{
@@ -221,7 +222,7 @@ const props = defineProps<{
const emit = defineEmits(["update:modelValue"]);
// Global varibles
const dds = AuthManager.createAuthenticatedDDSClient();
const dds = AuthManager.createClient(DDSClient);
const eqps = useEquipments();
const dialog = useDialogStore();

View File

@@ -1,65 +1,114 @@
<template>
<div class="seven-segment-display" :style="{
width: width + 'px',
height: height + 'px',
position: 'relative',
}">
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 120 220" class="display">
<div
class="seven-segment-display"
:style="{
width: width + 'px',
height: height + 'px',
position: 'relative',
}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 120 220"
class="display"
>
<!-- 数码管基座 -->
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
<!-- 7 + 小数点每个段由多边形表示重新设计点位置使其更接近实际数码管 -->
<!-- a段 (顶部横线) -->
<polygon :points="'30,20 90,20 98,28 82,36 38,36 22,28'"
<polygon
:points="'30,20 90,20 98,28 82,36 38,36 22,28'"
:fill="isSegmentActive('a') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" class="segment" />
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }"
class="segment"
/>
<!-- b段 (右上竖线) -->
<polygon :points="'100,30 108,38 108,82 100,90 92,82 92,38'"
<polygon
:points="'100,30 108,38 108,82 100,90 92,82 92,38'"
:fill="isSegmentActive('b') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" class="segment" />
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }"
class="segment"
/>
<!-- c段 (右下竖线) -->
<polygon :points="'100,90 108,98 108,142 100,150 92,142 92,98'"
<polygon
:points="'100,90 108,98 108,142 100,150 92,142 92,98'"
:fill="isSegmentActive('c') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" class="segment" />
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }"
class="segment"
/>
<!-- d段 (底部横线) -->
<polygon :points="'30,160 90,160 98,152 82,144 38,144 22,152'"
<polygon
:points="'30,160 90,160 98,152 82,144 38,144 22,152'"
:fill="isSegmentActive('d') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" class="segment" />
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }"
class="segment"
/>
<!-- e段 (左下竖线) -->
<polygon :points="'20,90 28,98 28,142 20,150 12,142 12,98'"
<polygon
:points="'20,90 28,98 28,142 20,150 12,142 12,98'"
:fill="isSegmentActive('e') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" class="segment" />
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }"
class="segment"
/>
<!-- f段 (左上竖线) -->
<polygon :points="'20,30 28,38 28,82 20,90 12,82 12,38'"
<polygon
:points="'20,30 28,38 28,82 20,90 12,82 12,38'"
:fill="isSegmentActive('f') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" class="segment" />
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }"
class="segment"
/>
<!-- g段 (中间横线) -->
<polygon :points="'30,90 38,82 82,82 90,90 82,98 38,98'"
<polygon
:points="'30,90 38,82 82,82 90,90 82,98 38,98'"
:fill="isSegmentActive('g') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" class="segment" />
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }"
class="segment"
/>
<!-- dp段 (小数点) -->
<circle cx="108" cy="154" r="6" :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }" class="segment" />
<circle
cx="108"
cy="154"
r="6"
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
class="segment"
/>
</svg>
<!-- 引脚 -->
<div v-for="pin in pins" :key="pin.pinId" :style="{
position: 'absolute',
left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)',
}" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`">
<Pin :ref="(el) => {
if (el) pinRefs[pin.pinId] = el;
}
" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" @pin-click="$emit('pin-click', $event)" />
<div
v-for="pin in pins"
:key="pin.pinId"
:style="{
position: 'absolute',
left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)',
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`"
>
<Pin
:ref="
(el) => {
if (el) pinRefs[pin.pinId] = el;
}
"
:label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
@pin-click="$emit('pin-click', $event)"
/>
</div>
</div>
</template>
@@ -217,12 +266,12 @@ function isSegmentActive(
if (isInAfterglowMode.value) {
return afterglowStates.value[segment];
}
// 如果COM口未激活所有段都不显示
if (!currentComActive.value) {
return false;
}
// 否则使用稳定状态
return stableSegmentStates.value[segment];
}
@@ -232,7 +281,7 @@ function updateSegmentStates() {
// 先获取COM口状态
const comPin = props.pins.find((p) => p.pinId === "COM");
let comActive = false; // 默认未激活
if (comPin && comPin.constraint) {
const comState = getConstraintState(comPin.constraint);
if (props.cathodeType === "anode") {
@@ -274,7 +323,8 @@ function updateSegmentStates() {
for (const pin of props.pins) {
if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
if (!pin.constraint) {
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
false;
continue;
}
const pinState = getConstraintState(pin.constraint);
@@ -285,7 +335,8 @@ function updateSegmentStates() {
newState = pinState === "low";
}
// 段状态只有在COM激活时才有效
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState;
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
newState;
}
}
@@ -328,22 +379,25 @@ function updateAfterglowBuffers() {
// 进入余晖模式
function enterAfterglowMode() {
isInAfterglowMode.value = true;
// 保存当前稳定状态作为余晖状态
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value;
afterglowStates.value[typedSegmentId] = stableSegmentStates.value[typedSegmentId];
afterglowStates.value[typedSegmentId] =
stableSegmentStates.value[typedSegmentId];
// 设置定时器,在余晖持续时间后退出余晖模式
if (afterglowTimers.value[segmentId]) {
clearTimeout(afterglowTimers.value[segmentId]!);
}
afterglowTimers.value[segmentId] = setTimeout(() => {
afterglowStates.value[typedSegmentId] = false;
// 检查是否所有段都已经关闭
const allSegmentsOff = Object.values(afterglowStates.value).every(state => !state);
const allSegmentsOff = Object.values(afterglowStates.value).every(
(state) => !state,
);
if (allSegmentsOff) {
exitAfterglowMode();
}
@@ -354,14 +408,14 @@ function enterAfterglowMode() {
// 退出余晖模式
function exitAfterglowMode() {
isInAfterglowMode.value = false;
// 清除所有定时器
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
if (afterglowTimers.value[segmentId]) {
clearTimeout(afterglowTimers.value[segmentId]!);
afterglowTimers.value[segmentId] = null;
}
// 重置余晖状态
const typedSegmentId = segmentId as keyof typeof afterglowStates.value;
afterglowStates.value[typedSegmentId] = false;
@@ -397,11 +451,6 @@ onUnmounted(() => {
}
}
});
// 暴露属性和方法
defineExpose({
updateSegmentStates,
});
</script>
<style scoped>
@@ -418,7 +467,8 @@ defineExpose({
/* 数码管发光效果 */
.segment[style*="opacity: 1"] {
filter: drop-shadow(0 0 4px v-bind(segmentColor)) drop-shadow(0 0 2px v-bind(segmentColor));
filter: drop-shadow(0 0 4px v-bind(segmentColor))
drop-shadow(0 0 2px v-bind(segmentColor));
}
</style>

View File

@@ -0,0 +1,413 @@
<template>
<div
class="seven-segment-display"
:style="{
width: width + 'px',
height: height + 'px',
position: 'relative',
}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 120 220"
class="display"
>
<!-- 数码管基座 -->
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
<!-- 7段显示 -->
<polygon
v-for="(segment, id) in segmentPaths"
:key="id"
:points="segment.points"
:fill="isSegmentActive(id) ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive(id) ? 1 : 0.15 }"
:class="{ segment: true, active: isSegmentActive(id) }"
/>
<!-- 小数点 -->
<circle
cx="108"
cy="154"
r="6"
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
:class="{ segment: true, active: isSegmentActive('dp') }"
/>
</svg>
<!-- 引脚仅在非数字孪生模式下显示 -->
<div
v-if="!props.enableDigitalTwin"
v-for="pin in props.pins"
:key="pin.pinId"
:style="{
position: 'absolute',
left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)',
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`"
>
<Pin
:ref="
(el) => {
if (el) pinRefs[pin.pinId] = el;
}
"
:label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
@pin-click="$emit('pin-click', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { useConstraintsStore } from "../../stores/constraints";
import Pin from "./Pin.vue";
import { useEquipments } from "@/stores/equipments";
// ============================================================================
// Linus式极简数据结构一个byte解决一切
// ============================================================================
interface Props {
size?: number;
color?: string;
enableDigitalTwin?: boolean;
digitalTwinNum?: number;
afterglowDuration?: number;
cathodeType?: "common" | "anode";
pins?: Array<{
pinId: string;
constraint: string;
x: number;
y: number;
}>;
}
const props = withDefaults(defineProps<Props>(), {
size: 1,
color: "red",
enableDigitalTwin: false,
digitalTwinNum: 0,
afterglowDuration: 500,
cathodeType: "common",
pins: () => [
{ pinId: "a", constraint: "", x: 10, y: 170 },
{ pinId: "b", constraint: "", x: 24, y: 170 },
{ pinId: "c", constraint: "", x: 38, y: 170 },
{ pinId: "d", constraint: "", x: 52, y: 170 },
{ pinId: "e", constraint: "", x: 66, y: 170 },
{ pinId: "f", constraint: "", x: 80, y: 170 },
{ pinId: "g", constraint: "", x: 94, y: 170 },
{ pinId: "dp", constraint: "", x: 108, y: 170 },
{ pinId: "COM", constraint: "", x: 60, y: 10 },
],
});
// ============================================================================
// 核心状态:简单到极致
// ============================================================================
// 当前显示状态 - 8bit对应8个段
const displayByte = ref<number>(0);
// 余晖状态
const afterglowByte = ref<number>(0);
const afterglowTimer = ref<number | null>(null);
// 约束系统状态(兼容模式)
const constraintStates = ref<Record<string, boolean>>({
a: false,
b: false,
c: false,
d: false,
e: false,
f: false,
g: false,
dp: false,
});
// ============================================================================
// Bit操作硬件工程师的好品味
// ============================================================================
// 段到bit位的映射 (标准7段数码管编码)
const SEGMENT_BITS = {
a: 0, // bit 0
b: 1, // bit 1
c: 2, // bit 2
d: 3, // bit 3
e: 4, // bit 4
f: 5, // bit 5
g: 6, // bit 6
dp: 7, // bit 7
} as const;
function isBitSet(byte: number, bit: number): boolean {
return (byte & (1 << bit)) !== 0;
}
function isSegmentActive(segmentId: keyof typeof SEGMENT_BITS): boolean {
if (props.enableDigitalTwin) {
// 数字孪生模式余晖优先然后是当前byte
const bit = SEGMENT_BITS[segmentId];
return (
isBitSet(afterglowByte.value, bit) || isBitSet(displayByte.value, bit)
);
} else {
// 约束模式:使用传统逻辑
return constraintStates.value[segmentId] || false;
}
}
// ============================================================================
// SignalR数字孪生集成
// ============================================================================
const eqps = useEquipments();
async function initDigitalTwin() {
if (
!props.enableDigitalTwin ||
props.digitalTwinNum < 0 ||
props.digitalTwinNum > 31
)
return;
try {
eqps.sevenSegmentDisplaySetOnOff(props.enableDigitalTwin);
console.log(
`Digital twin initialized for address: ${props.digitalTwinNum}`,
);
} catch (error) {
console.warn("Failed to initialize digital twin:", error);
}
}
watch(
() => [eqps.sevenSegmentDisplayData],
() => {
if (
!eqps.sevenSegmentDisplayData ||
props.digitalTwinNum < 0 ||
props.digitalTwinNum > 31
)
return;
handleDigitalTwinData(eqps.sevenSegmentDisplayData[props.digitalTwinNum]);
},
);
function handleDigitalTwinData(data: any) {
let newByte = 0;
if (typeof data === "number") {
// 直接是byte数据
newByte = data & 0xff; // 确保只取低8位
} else if (data && typeof data.value === "number") {
// 包装在对象中的byte数据
newByte = data.value & 0xff;
} else if (data && data.segments) {
// 段状态对象格式
Object.keys(SEGMENT_BITS).forEach((segment) => {
if (data.segments[segment]) {
newByte |= 1 << SEGMENT_BITS[segment as keyof typeof SEGMENT_BITS];
}
});
}
updateDisplayByte(newByte);
}
function updateDisplayByte(newByte: number) {
const oldByte = displayByte.value;
displayByte.value = newByte;
// 启动余晖效果
if (oldByte !== 0 && newByte !== oldByte) {
startAfterglow(oldByte);
}
}
function startAfterglow(byte: number) {
afterglowByte.value = byte;
if (afterglowTimer.value) {
clearTimeout(afterglowTimer.value);
}
afterglowTimer.value = setTimeout(() => {
afterglowByte.value = 0;
afterglowTimer.value = null;
}, props.afterglowDuration);
}
function cleanupDigitalTwin() {
eqps.sevenSegmentDisplaySetOnOff(false);
}
// ============================================================================
// 约束系统兼容(传统模式)
// ============================================================================
const { getConstraintState, onConstraintStateChange } = useConstraintsStore();
let constraintUnsubscribe: (() => void) | null = null;
function updateConstraintStates() {
if (props.enableDigitalTwin) return; // 数字孪生模式下忽略约束
// 获取COM状态
const comPin = props.pins.find((p) => p.pinId === "COM");
const comActive = isComActive(comPin);
if (!comActive) {
// COM不活跃所有段关闭
Object.keys(constraintStates.value).forEach((key) => {
constraintStates.value[key] = false;
});
return;
}
// 更新各段状态
props.pins.forEach((pin) => {
if (Object.hasOwnProperty.call(SEGMENT_BITS, pin.pinId)) {
constraintStates.value[pin.pinId] = isPinActive(pin);
}
});
}
function isComActive(comPin: any): boolean {
if (!comPin?.constraint) return true;
const state = getConstraintState(comPin.constraint);
return props.cathodeType === "common" ? state === "low" : state === "low";
}
function isPinActive(pin: any): boolean {
if (!pin.constraint) return false;
const state = getConstraintState(pin.constraint);
return props.cathodeType === "common" ? state === "high" : state === "low";
}
// ============================================================================
// 渲染数据
// ============================================================================
const segmentPaths = {
a: { points: "30,20 90,20 98,28 82,36 38,36 22,28" },
b: { points: "100,30 108,38 108,82 100,90 92,82 92,38" },
c: { points: "100,90 108,98 108,142 100,150 92,142 92,98" },
d: { points: "30,160 90,160 98,152 82,144 38,144 22,152" },
e: { points: "20,90 28,98 28,142 20,150 12,142 12,98" },
f: { points: "20,30 28,38 28,82 20,90 12,82 12,38" },
g: { points: "30,90 38,82 82,82 90,90 82,98 38,98" },
} as const;
// ============================================================================
// 计算属性
// ============================================================================
const width = computed(() => 120 * props.size);
const height = computed(() => 220 * props.size);
const segmentColor = computed(() => props.color);
const inactiveColor = computed(() => "#FFFFFF");
const pinRefs = ref<Record<string, any>>({});
// ============================================================================
// 生命周期
// ============================================================================
onMounted(async () => {
if (props.enableDigitalTwin) {
await initDigitalTwin();
} else {
constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
updateConstraintStates();
}
});
onUnmounted(() => {
cleanupDigitalTwin();
if (constraintUnsubscribe) {
constraintUnsubscribe();
}
if (afterglowTimer.value) {
clearTimeout(afterglowTimer.value);
}
});
// 监听模式切换
watch(
() => [props.enableDigitalTwin],
async () => {
// 清理旧模式
cleanupDigitalTwin();
if (constraintUnsubscribe) {
constraintUnsubscribe();
constraintUnsubscribe = null;
}
// 初始化新模式
if (props.enableDigitalTwin) {
await initDigitalTwin();
} else {
constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
updateConstraintStates();
}
},
);
</script>
<style scoped>
.seven-segment-display {
display: inline-block;
position: relative;
}
.segment {
transition:
opacity 0.2s,
fill 0.2s;
}
.segment.active {
filter: drop-shadow(0 0 4px v-bind(segmentColor))
drop-shadow(0 0 2px v-bind(segmentColor));
}
</style>
<script lang="ts">
export function getDefaultProps() {
return {
size: 1,
color: "red",
enableDigitalTwin: false,
digitalTwinNum: 0,
afterglowDuration: 500,
cathodeType: "common",
pins: [
{ pinId: "a", constraint: "", x: 10, y: 170 },
{ pinId: "b", constraint: "", x: 24, y: 170 },
{ pinId: "c", constraint: "", x: 38, y: 170 },
{ pinId: "d", constraint: "", x: 52, y: 170 },
{ pinId: "e", constraint: "", x: 66, y: 170 },
{ pinId: "f", constraint: "", x: 80, y: 170 },
{ pinId: "g", constraint: "", x: 94, y: 170 },
{ pinId: "dp", constraint: "", x: 108, y: 170 },
{ pinId: "COM", constraint: "", x: 60, y: 10 },
],
};
}
</script>

View File

@@ -1,17 +1,30 @@
// filepath: c:\_Project\FPGA_WebLab\FPGA_WebLab\src\components\equipments\Switch.vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
:viewBox="`4 6 ${props.switchCount + 2} 4`"
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
:viewBox="`4 6 ${switchCount + 2} 4`"
class="dip-switch"
>
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feFlood result="flood" flood-color="#f08a5d" flood-opacity="1"></feFlood>
<feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
<feMorphology in="mask" result="dilated" operator="dilate" radius="0.02"></feMorphology>
<feFlood
result="flood"
flood-color="#f08a5d"
flood-opacity="1"
></feFlood>
<feComposite
in="flood"
result="mask"
in2="SourceGraphic"
operator="in"
></feComposite>
<feMorphology
in="mask"
result="dilated"
operator="dilate"
radius="0.02"
></feMorphology>
<feGaussianBlur in="dilated" stdDeviation="0.05" result="blur1" />
<feGaussianBlur in="dilated" stdDeviation="0.1" result="blur2" />
<feGaussianBlur in="dilated" stdDeviation="0.2" result="blur3" />
@@ -23,29 +36,41 @@
</feMerge>
</filter>
</defs>
<g>
<!-- 红色背景随开关数量变化宽度 -->
<rect :width="props.switchCount + 2" height="4" x="4" y="6" fill="#c01401" rx="0.1" />
<text v-if="props.showLabels" fill="white" font-size="0.7" x="4.25" y="6.75">ON</text>
<rect
:width="switchCount + 2"
height="4"
x="4"
y="6"
fill="#c01401"
rx="0.1"
/>
<text
v-if="props.showLabels"
fill="white"
font-size="0.7"
x="4.25"
y="6.75"
>
ON
</text>
<g>
<template v-for="(_, index) in Array(props.switchCount)" :key="index">
<rect
class="glow interactive"
@click="toggleBtnStatus(index)"
width="0.7"
height="2"
fill="#68716f"
:x="5.15 + index"
y="7"
rx="0.1"
<template v-for="(_, index) in Array(switchCount)" :key="index">
<rect
class="glow interactive"
@click="toggleBtnStatus(index)"
width="0.7"
height="2"
fill="#68716f"
:x="5.15 + index"
y="7"
rx="0.1"
/>
<text
<text
v-if="props.showLabels"
:x="5.5 + index"
y="9.5"
font-size="0.4"
:x="5.5 + index"
y="9.5"
font-size="0.4"
text-anchor="middle"
fill="#444"
>
@@ -53,19 +78,21 @@
</text>
</template>
</g>
<g>
<template v-for="(location, index) in btnLocation" :key="`btn-${index}`">
<rect
<template
v-for="(location, index) in btnLocation"
:key="`btn-${index}`"
>
<rect
class="interactive"
@click="toggleBtnStatus(index)"
width="0.65"
height="0.65"
fill="white"
:x="5.175 + index"
:y="location"
@click="toggleBtnStatus(index)"
width="0.65"
height="0.65"
fill="white"
:x="5.175 + index"
:y="location"
rx="0.1"
opacity="1"
opacity="1"
/>
</template>
</g>
@@ -74,119 +101,112 @@
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue";
import { SwitchClient } from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager";
import { isUndefined } from "lodash";
import { ref, computed, watch, onMounted } from "vue";
interface Props {
size?: number;
enableDigitalTwin?: boolean;
switchCount?: number;
// 新增属性
initialValues?: boolean[] | string; // 开关的初始状态,可以是布尔数组或逗号分隔的字符串
showLabels?: boolean; // 是否显示标签
initialValues?: string;
showLabels?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
size: 1,
enableDigitalTwin: false,
switchCount: 6,
initialValues: () => [],
showLabels: true
initialValues: "",
showLabels: true,
});
// 计算实际宽高
const width = computed(() => {
// 每个开关占用25px宽度再加上两侧边距(20px)
return (props.switchCount * 25 + 20) * props.size;
const switchCount = computed(() => {
if (props.enableDigitalTwin) return 5;
else return props.switchCount;
});
const height = computed(() => 85 * props.size); // 高度保持固定比例
// 定义发出的事件
const emit = defineEmits(['change', 'switch-toggle']);
function getClient() {
return AuthManager.createClient(SwitchClient);
}
// 解析初始值,支持字符串和数组两种格式
const parseInitialValues = () => {
// 解析初始值
function parseInitialValues(): boolean[] {
if (Array.isArray(props.initialValues)) {
return [...props.initialValues].slice(0, props.switchCount);
} else if (typeof props.initialValues === 'string' && props.initialValues.trim() !== '') {
// 将逗号分隔的字符串转换为布尔数组
const values = props.initialValues.split(',')
.map(val => val.trim() === '1' || val.trim().toLowerCase() === 'true')
.slice(0, props.switchCount);
// 如果数组长度小于开关数量,用 false 填充
while (values.length < props.switchCount) {
values.push(false);
}
return values;
return [...props.initialValues].slice(0, switchCount.value);
}
// 默认返回全部为 false 的数组
return Array(props.switchCount).fill(false);
};
// 初始化按钮状态
const btnStatus = ref(parseInitialValues());
// 监听 switchCount 变化,调整开关状态数组
watch(() => props.switchCount, (newCount) => {
if (newCount !== btnStatus.value.length) {
// 如果新数量大于当前数量,则扩展数组
if (newCount > btnStatus.value.length) {
btnStatus.value = [
...btnStatus.value,
...Array(newCount - btnStatus.value.length).fill(false)
];
} else {
// 如果新数量小于当前数量,则截断数组
btnStatus.value = btnStatus.value.slice(0, newCount);
}
if (
typeof props.initialValues === "string" &&
props.initialValues.trim() !== ""
) {
const arr = props.initialValues
.split(",")
.map((val) => val.trim() === "1" || val.trim().toLowerCase() === "true");
while (arr.length < props.switchCount) arr.push(false);
return arr.slice(0, props.switchCount);
}
}, { immediate: true });
return Array(switchCount.value).fill(false);
}
// 监听 initialValues 变化,更新开关状态
watch(() => props.initialValues, () => {
btnStatus.value = parseInitialValues();
});
// 状态唯一真相
const btnStatus = ref<boolean[]>(parseInitialValues());
const btnLocation = computed(() => {
return btnStatus.value.map((status) => {
return status ? 7.025 : 8.325;
});
});
// 计算宽高
const width = computed(() => (switchCount.value * 25 + 20) * props.size);
const height = computed(() => 85 * props.size);
function setBtnStatus(btnNum: number, isOn: boolean): void {
if (btnNum >= 0 && btnNum < btnStatus.value.length) {
btnStatus.value[btnNum] = isOn;
emit('change', { index: btnNum, value: isOn, states: [...btnStatus.value] });
// 按钮位置
const btnLocation = computed(() =>
btnStatus.value.map((status) => (status ? 7.025 : 8.325)),
);
// 状态变更统一处理
function updateStatus(newStates: boolean[], index?: number) {
btnStatus.value = newStates.slice(0, switchCount.value);
if (props.enableDigitalTwin) {
try {
const client = getClient();
if (!isUndefined(index))
client.setSwitchOnOff(index + 1, newStates[index]);
else client.setMultiSwitchsOnOff(btnStatus.value);
} catch (error: any) {}
}
}
function toggleBtnStatus(btnNum: number): void {
if (btnNum >= 0 && btnNum < btnStatus.value.length) {
btnStatus.value[btnNum] = !btnStatus.value[btnNum];
emit('switch-toggle', {
index: btnNum,
value: btnStatus.value[btnNum],
states: [...btnStatus.value]
});
}
// 切换单个
function toggleBtnStatus(idx: number) {
if (idx < 0 || idx >= btnStatus.value.length) return;
const newStates = [...btnStatus.value];
newStates[idx] = !newStates[idx];
updateStatus(newStates, idx);
}
// 一次性设置所有开关状态
function setAllStates(states: boolean[]): void {
const newStates = states.slice(0, props.switchCount);
while (newStates.length < props.switchCount) {
newStates.push(false);
}
btnStatus.value = newStates;
emit('change', { states: [...btnStatus.value] });
// 单个设置
function setBtnStatus(idx: number, isOn: boolean) {
if (idx < 0 || idx >= btnStatus.value.length) return;
const newStates = [...btnStatus.value];
newStates[idx] = isOn;
updateStatus(newStates, idx);
}
// 暴露组件方法和状态
defineExpose({
setBtnStatus,
toggleBtnStatus,
setAllStates,
getBtnStatus: () => [...btnStatus.value]
});
// 监听 props 变化只同步一次
watch(
() => props.enableDigitalTwin,
(newVal) => {
const client = getClient();
client.setEnable(newVal);
},
{ immediate: true },
);
watch(
() => [switchCount.value, props.initialValues],
() => {
btnStatus.value = parseInitialValues();
updateStatus(btnStatus.value);
},
);
</script>
<style scoped lang="postcss">
@@ -194,17 +214,27 @@ defineExpose({
display: block;
padding: 0;
margin: 0;
line-height: 0; /* 移除行高导致的额外间距 */
font-size: 0; /* 防止文本节点造成的间距 */
line-height: 0;
font-size: 0;
box-sizing: content-box;
overflow: visible;
}
rect {
transition: all 100ms ease-in-out;
}
.interactive {
cursor: pointer;
}
</style>
<script lang="ts">
export function getDefaultProps() {
return {
size: 1,
enableDigitalTwin: false,
switchCount: 6,
initialValues: "",
showLabels: true,
};
}
</script>

View File

@@ -7,15 +7,28 @@ import { isNumber } from "mathjs";
import { Mutex, withTimeout } from "async-mutex";
import { useConstraintsStore } from "@/stores/constraints";
import { useDialogStore } from "./dialog";
import { toFileParameterOrUndefined } from "@/utils/Common";
import {
base64ToArrayBuffer,
toFileParameterOrUndefined,
} from "@/utils/Common";
import { AuthManager } from "@/utils/AuthManager";
import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
import { HubConnection, HubConnectionState } from "@microsoft/signalr";
import {
getHubProxyFactory,
getReceiverRegister,
} from "@/utils/signalR/TypedSignalR.Client";
import { ResourcePurpose, type ResourceInfo } from "@/APIClient";
import type { IJtagHub } from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
import {
JtagClient,
MatrixKeyClient,
PowerClient,
ResourceClient,
ResourcePurpose,
type ResourceInfo,
} from "@/APIClient";
import type {
IDigitalTubesHub,
IJtagHub,
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
export const useEquipments = defineStore("equipments", () => {
// Global Stores
@@ -26,6 +39,7 @@ export const useEquipments = defineStore("equipments", () => {
const boardPort = useLocalStorage("fpga-board-port", 1234);
// Jtag
const enableJtagBoundaryScan = ref(false);
const jtagBitstream = ref<File>();
const jtagBoundaryScanFreq = ref(100);
const jtagUserBitstreams = ref<ResourceInfo[]>([]);
@@ -39,8 +53,7 @@ export const useEquipments = defineStore("equipments", () => {
onMounted(async () => {
// 每次挂载都重新创建连接
jtagHubConnection.value =
AuthManager.createAuthenticatedJtagHubConnection();
jtagHubConnection.value = AuthManager.createHubConnection("JtagHub");
jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
jtagHubConnection.value,
);
@@ -62,46 +75,6 @@ export const useEquipments = defineStore("equipments", () => {
}
});
// Matrix Key
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
const matrixKeypadClientMutex = withTimeout(
new Mutex(),
1000,
new Error("Matrixkeyclient Mutex Timeout!"),
);
// Power
const powerClientMutex = withTimeout(
new Mutex(),
1000,
new Error("Matrixkeyclient Mutex Timeout!"),
);
// Enable Setting
const enableJtagBoundaryScan = ref(false);
const enableMatrixKey = ref(false);
const enablePower = ref(false);
function setMatrixKey(
keyNum: number | string | undefined,
keyValue: boolean,
): boolean {
let _keyNum: number;
if (isString(keyNum)) {
_keyNum = toNumber(keyNum);
} else if (isNumber(keyNum)) {
_keyNum = keyNum;
} else {
return false;
}
if (z.number().nonnegative().max(16).safeParse(_keyNum).success) {
matrixKeyStates[_keyNum] = keyValue;
return true;
}
return false;
}
async function jtagBoundaryScanSetOnOff(enable: boolean) {
if (isUndefined(jtagHubProxy.value)) {
console.error("JtagHub Not Initialize...");
@@ -134,7 +107,7 @@ export const useEquipments = defineStore("equipments", () => {
// 自动开启电源
await powerSetOnOff(true);
const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resourceClient = AuthManager.createClient(ResourceClient);
const resp = await resourceClient.addResource(
"bitstream",
ResourcePurpose.User,
@@ -166,7 +139,7 @@ export const useEquipments = defineStore("equipments", () => {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient();
const jtagClient = AuthManager.createClient(JtagClient);
const resp = await jtagClient.downloadBitstream(
boardAddr.value,
boardPort.value,
@@ -188,7 +161,7 @@ export const useEquipments = defineStore("equipments", () => {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient();
const jtagClient = AuthManager.createClient(JtagClient);
const resp = await jtagClient.getDeviceIDCode(
boardAddr.value,
boardPort.value,
@@ -208,7 +181,7 @@ export const useEquipments = defineStore("equipments", () => {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient();
const jtagClient = AuthManager.createClient(JtagClient);
const resp = await jtagClient.setSpeed(
boardAddr.value,
boardPort.value,
@@ -223,12 +196,38 @@ export const useEquipments = defineStore("equipments", () => {
}
}
// Matrix Key
const enableMatrixKey = ref(false);
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
const matrixKeypadClientMutex = withTimeout(
new Mutex(),
1000,
new Error("Matrixkeyclient Mutex Timeout!"),
);
function setMatrixKey(
keyNum: number | string | undefined,
keyValue: boolean,
): boolean {
let _keyNum: number;
if (isString(keyNum)) {
_keyNum = toNumber(keyNum);
} else if (isNumber(keyNum)) {
_keyNum = keyNum;
} else {
return false;
}
if (z.number().nonnegative().max(16).safeParse(_keyNum).success) {
matrixKeyStates[_keyNum] = keyValue;
return true;
}
return false;
}
async function matrixKeypadSetKeyStates(keyStates: boolean[]) {
const release = await matrixKeypadClientMutex.acquire();
console.log("set Key !!!!!!!!!!!!");
try {
const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const matrixKeypadClient = AuthManager.createClient(MatrixKeyClient);
const resp = await matrixKeypadClient.setMatrixKeyStatus(
boardAddr.value,
boardPort.value,
@@ -246,9 +245,8 @@ export const useEquipments = defineStore("equipments", () => {
async function matrixKeypadEnable(enable: boolean) {
const release = await matrixKeypadClientMutex.acquire();
try {
const matrixKeypadClient = AuthManager.createClient(MatrixKeyClient);
if (enable) {
const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const resp = await matrixKeypadClient.enabelMatrixKey(
boardAddr.value,
boardPort.value,
@@ -256,8 +254,6 @@ export const useEquipments = defineStore("equipments", () => {
enableMatrixKey.value = resp;
return resp;
} else {
const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const resp = await matrixKeypadClient.disableMatrixKey(
boardAddr.value,
boardPort.value,
@@ -274,10 +270,17 @@ export const useEquipments = defineStore("equipments", () => {
}
}
// Power
const powerClientMutex = withTimeout(
new Mutex(),
1000,
new Error("Matrixkeyclient Mutex Timeout!"),
);
const enablePower = ref(false);
async function powerSetOnOff(enable: boolean) {
const release = await powerClientMutex.acquire();
try {
const powerClient = AuthManager.createAuthenticatedPowerClient();
const powerClient = AuthManager.createClient(PowerClient);
const resp = await powerClient.setPowerOnOff(
boardAddr.value,
boardPort.value,
@@ -293,6 +296,74 @@ export const useEquipments = defineStore("equipments", () => {
}
}
// Seven Segment Display
const enableSevenSegmentDisplay = ref(false);
const sevenSegmentDisplayFrequency = ref(100);
const sevenSegmentDisplayData = ref<Uint8Array>();
const sevenSegmentDisplayHub = ref<HubConnection>();
const sevenSegmentDisplayHubProxy = ref<IDigitalTubesHub>();
async function sevenSegmentDisplaySetOnOff(enable: boolean) {
if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
return;
if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected)
await sevenSegmentDisplayHub.value.start();
if (enable) {
await sevenSegmentDisplayHubProxy.value.startScan();
} else {
await sevenSegmentDisplayHubProxy.value.stopScan();
}
}
async function sevenSegmentDisplaySetFrequency(frequency: number) {
if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
return;
if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected)
await sevenSegmentDisplayHub.value.start();
await sevenSegmentDisplayHubProxy.value.setFrequency(frequency);
}
async function sevenSegmentDisplayGetStatus() {
if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
return;
if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected)
await sevenSegmentDisplayHub.value.start();
return await sevenSegmentDisplayHubProxy.value.getStatus();
}
async function handleSevenSegmentDisplayOnReceive(msg: string) {
const bytes = base64ToArrayBuffer(msg);
sevenSegmentDisplayData.value = new Uint8Array(bytes);
}
onMounted(async () => {
// 每次挂载都重新创建连接
sevenSegmentDisplayHub.value =
AuthManager.createHubConnection("DigitalTubesHub");
sevenSegmentDisplayHubProxy.value = getHubProxyFactory(
"IDigitalTubesHub",
).createHubProxy(sevenSegmentDisplayHub.value);
getReceiverRegister("IDigitalTubesReceiver").register(
sevenSegmentDisplayHub.value,
{
onReceive: handleSevenSegmentDisplayOnReceive,
},
);
});
onUnmounted(() => {
// 断开连接,清理资源
if (sevenSegmentDisplayHub.value) {
sevenSegmentDisplayHub.value.stop();
sevenSegmentDisplayHub.value = undefined;
sevenSegmentDisplayHubProxy.value = undefined;
}
});
return {
boardAddr,
boardPort,
@@ -320,5 +391,13 @@ export const useEquipments = defineStore("equipments", () => {
enablePower,
powerClientMutex,
powerSetOnOff,
// Seven Segment Display
enableSevenSegmentDisplay,
sevenSegmentDisplayData,
sevenSegmentDisplayFrequency,
sevenSegmentDisplaySetOnOff,
sevenSegmentDisplaySetFrequency,
sevenSegmentDisplayGetStatus,
};
});

83
src/stores/progress.ts Normal file
View File

@@ -0,0 +1,83 @@
import type { HubConnection } from "@microsoft/signalr";
import type {
IProgressHub,
IProgressReceiver,
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
import {
getHubProxyFactory,
getReceiverRegister,
} from "@/utils/signalR/TypedSignalR.Client";
import { ProgressStatus, type ProgressInfo } from "@/utils/signalR/server.Hubs";
import { onMounted, onUnmounted, ref, shallowRef } from "vue";
import { defineStore } from "pinia";
import { AuthManager } from "@/utils/AuthManager";
import { forEach, isUndefined } from "lodash";
export type ProgressCallback = (msg: ProgressInfo) => void;
export const useProgressStore = defineStore("progress", () => {
// taskId -> name -> callback
const progressCallbackFuncs = shallowRef<
Map<string, Map<string, ProgressCallback>>
>(new Map());
const progressHubConnection = shallowRef<HubConnection>();
const progressHubProxy = shallowRef<IProgressHub>();
const progressHubReceiver: IProgressReceiver = {
onReceiveProgress: async (msg) => {
const taskMap = progressCallbackFuncs.value.get(msg.taskId);
if (taskMap) {
for (const func of taskMap.values()) {
func(msg);
}
}
},
};
onMounted(async () => {
progressHubConnection.value =
AuthManager.createHubConnection("ProgressHub");
progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy(
progressHubConnection.value,
);
getReceiverRegister("IProgressReceiver").register(
progressHubConnection.value,
progressHubReceiver,
);
progressHubConnection.value.start();
});
onUnmounted(() => {
if (progressHubConnection.value) {
progressHubConnection.value.stop();
progressHubConnection.value = undefined;
progressHubProxy.value = undefined;
}
});
function register(progressId: string, name: string, func: ProgressCallback) {
progressHubProxy.value?.join(progressId);
let taskMap = progressCallbackFuncs.value.get(progressId);
if (!taskMap) {
taskMap = new Map();
progressCallbackFuncs.value?.set(progressId, taskMap);
}
taskMap.set(name, func);
}
function unregister(taskId: string, name: string) {
progressHubProxy.value?.leave(taskId);
const taskMap = progressCallbackFuncs.value.get(taskId);
if (taskMap) {
taskMap.delete(name);
if (taskMap.size === 0) {
progressCallbackFuncs.value?.delete(taskId);
}
}
}
return {
register,
unregister,
};
});

View File

@@ -1,313 +1,105 @@
import {
DataClient,
VideoStreamClient,
BsdlParserClient,
DDSClient,
JtagClient,
MatrixKeyClient,
PowerClient,
RemoteUpdateClient,
TutorialClient,
UDPClient,
LogicAnalyzerClient,
NetConfigClient,
OscilloscopeApiClient,
DebuggerClient,
ExamClient,
ResourceClient,
HdmiVideoStreamClient,
} from "@/APIClient";
import router from "@/router";
import { DataClient } from "@/APIClient";
import { HubConnectionBuilder } from "@microsoft/signalr";
import axios, { type AxiosInstance } from "axios";
import { isNull } from "lodash";
// 支持的客户端类型联合类型
type SupportedClient =
| DataClient
| VideoStreamClient
| BsdlParserClient
| DDSClient
| JtagClient
| MatrixKeyClient
| PowerClient
| RemoteUpdateClient
| TutorialClient
| LogicAnalyzerClient
| UDPClient
| NetConfigClient
| OscilloscopeApiClient
| DebuggerClient
| ExamClient
| ResourceClient
| HdmiVideoStreamClient;
// 简单到让人想哭的认证管理器
export class AuthManager {
// 存储token到localStorage
public static setToken(token: string): void {
localStorage.setItem("authToken", token);
private static readonly TOKEN_KEY = "authToken";
// 核心数据:就是个字符串
static getToken(): string | null {
return localStorage.getItem(this.TOKEN_KEY);
}
// 从localStorage获取token
public static getToken(): string | null {
return localStorage.getItem("authToken");
static setToken(token: string): void {
localStorage.setItem(this.TOKEN_KEY, token);
}
// 清除token
public static clearToken(): void {
localStorage.removeItem("authToken");
static clearToken(): void {
localStorage.removeItem(this.TOKEN_KEY);
}
// 检查是否已认证
public static async isAuthenticated(): Promise<boolean> {
return await AuthManager.verifyToken();
// 核心功能创建带认证的HTTP配置
static getAuthHeaders(): Record<string, string> {
const token = this.getToken();
return token ? { Authorization: `Bearer ${token}` } : {};
}
// 通用的为HTTP请求添加Authorization header的方法
public static addAuthHeader(client: SupportedClient): void {
const token = AuthManager.getToken();
if (token) {
// 创建一个自定义的 http 对象,包装原有的 fetch 方法
const customHttp = {
fetch: (url: RequestInfo, init?: RequestInit) => {
if (!init) init = {};
if (!init.headers) init.headers = {};
// 添加Authorization header
if (typeof init.headers === "object" && init.headers !== null) {
(init.headers as any)["Authorization"] = `Bearer ${token}`;
}
// 使用全局 fetch 或 window.fetch
return (window as any).fetch(url, init);
},
};
// 重新构造客户端,传入自定义的 http 对象
const ClientClass = client.constructor as new (
baseUrl?: string,
http?: any,
) => SupportedClient;
const newClient = new ClientClass(undefined, customHttp);
// 将新客户端的属性复制到原客户端(这是一个 workaround
// 更好的做法是返回新的客户端实例
Object.setPrototypeOf(client, Object.getPrototypeOf(newClient));
Object.assign(client, newClient);
}
}
// 私有方法创建带认证的HTTP客户端
private static createAuthenticatedHttp() {
const token = AuthManager.getToken();
if (!token) {
return null;
}
return {
fetch: (url: RequestInfo, init?: RequestInit) => {
if (!init) init = {};
if (!init.headers) init.headers = {};
if (typeof init.headers === "object" && init.headers !== null) {
(init.headers as any)["Authorization"] = `Bearer ${token}`;
}
return (window as any).fetch(url, init);
},
};
}
// 私有方法创建带认证的Axios实例
private static createAuthenticatedAxiosInstance(): AxiosInstance | null {
const token = AuthManager.getToken();
if (!token) return null;
const instance = axios.create();
instance.interceptors.request.use((config) => {
config.headers = config.headers || {};
(config.headers as any)["Authorization"] = `Bearer ${token}`;
return config;
});
return instance;
}
// 通用的创建已认证客户端的方法(使用泛型)
public static createAuthenticatedClient<T extends SupportedClient>(
ClientClass: new (baseUrl?: string, instance?: AxiosInstance) => T,
// 一个方法搞定所有客户端不要17个垃圾方法
static createClient<T>(
ClientClass: new (baseUrl?: string, config?: any) => T,
baseUrl?: string,
): T {
const axiosInstance = AuthManager.createAuthenticatedAxiosInstance();
return axiosInstance
? new ClientClass(undefined, axiosInstance)
: new ClientClass();
const token = this.getToken();
if (!token) {
return new ClientClass(baseUrl);
}
// 对于axios客户端
const axiosInstance = axios.create({
headers: this.getAuthHeaders(),
});
return new ClientClass(baseUrl, axiosInstance);
}
// 便捷方法:创建已配置认证的各种客户端
public static createAuthenticatedDataClient(): DataClient {
return AuthManager.createAuthenticatedClient(DataClient);
}
public static createAuthenticatedVideoStreamClient(): VideoStreamClient {
return AuthManager.createAuthenticatedClient(VideoStreamClient);
}
public static createAuthenticatedBsdlParserClient(): BsdlParserClient {
return AuthManager.createAuthenticatedClient(BsdlParserClient);
}
public static createAuthenticatedDDSClient(): DDSClient {
return AuthManager.createAuthenticatedClient(DDSClient);
}
public static createAuthenticatedJtagClient(): JtagClient {
return AuthManager.createAuthenticatedClient(JtagClient);
}
public static createAuthenticatedMatrixKeyClient(): MatrixKeyClient {
return AuthManager.createAuthenticatedClient(MatrixKeyClient);
}
public static createAuthenticatedPowerClient(): PowerClient {
return AuthManager.createAuthenticatedClient(PowerClient);
}
public static createAuthenticatedRemoteUpdateClient(): RemoteUpdateClient {
return AuthManager.createAuthenticatedClient(RemoteUpdateClient);
}
public static createAuthenticatedTutorialClient(): TutorialClient {
return AuthManager.createAuthenticatedClient(TutorialClient);
}
public static createAuthenticatedUDPClient(): UDPClient {
return AuthManager.createAuthenticatedClient(UDPClient);
}
public static createAuthenticatedLogicAnalyzerClient(): LogicAnalyzerClient {
return AuthManager.createAuthenticatedClient(LogicAnalyzerClient);
}
public static createAuthenticatedNetConfigClient(): NetConfigClient {
return AuthManager.createAuthenticatedClient(NetConfigClient);
}
public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient {
return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
}
public static createAuthenticatedDebuggerClient(): DebuggerClient {
return AuthManager.createAuthenticatedClient(DebuggerClient);
}
public static createAuthenticatedExamClient(): ExamClient {
return AuthManager.createAuthenticatedClient(ExamClient);
}
public static createAuthenticatedResourceClient(): ResourceClient {
return AuthManager.createAuthenticatedClient(ResourceClient);
}
public static createAuthenticatedHdmiVideoStreamClient(): HdmiVideoStreamClient {
return AuthManager.createAuthenticatedClient(HdmiVideoStreamClient);
}
public static createAuthenticatedJtagHubConnection() {
// SignalR连接 - 简单明了
static createHubConnection(
hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub",
) {
return new HubConnectionBuilder()
.withUrl("http://127.0.0.1:5000/hubs/JtagHub", {
.withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, {
accessTokenFactory: () => this.getToken() ?? "",
})
.withAutomaticReconnect()
.build();
}
public static createAuthenticatedProgressHubConnection() {
return new HubConnectionBuilder()
.withUrl("http://127.0.0.1:5000/hubs/ProgressHub", {
accessTokenFactory: () => this.getToken() ?? "",
})
.withAutomaticReconnect()
.build();
}
// 登录函数
public static async login(
username: string,
password: string,
): Promise<boolean> {
// 认证逻辑 - 去除所有废话
static async login(username: string, password: string): Promise<boolean> {
try {
const client = new DataClient();
const token = await client.login(username, password);
if (token) {
AuthManager.setToken(token);
if (!token) return false;
// 验证token
const authClient = AuthManager.createAuthenticatedDataClient();
await authClient.testAuth();
this.setToken(token);
return true;
}
return false;
} catch (error) {
AuthManager.clearToken();
throw error;
}
}
// 登出函数
public static logout(): void {
AuthManager.clearToken();
}
// 验证当前token是否有效
public static async verifyToken(): Promise<boolean> {
try {
const token = AuthManager.getToken();
if (!token) {
return false;
}
const client = AuthManager.createAuthenticatedDataClient();
await client.testAuth();
// 验证token - 如果失败直接抛异常
await this.createClient(DataClient).testAuth();
return true;
} catch (error) {
AuthManager.clearToken();
return false;
} catch {
this.clearToken();
throw new Error("Login failed");
}
}
// 验证管理员权限
public static async verifyAdminAuth(): Promise<boolean> {
static logout(): void {
this.clearToken();
}
// 简单的验证 - 不要搞复杂
static async isAuthenticated(): Promise<boolean> {
if (!this.getToken()) return false;
try {
const token = AuthManager.getToken();
if (!token) {
return false;
}
const client = AuthManager.createAuthenticatedDataClient();
await client.testAdminAuth();
await this.createClient(DataClient).testAuth();
return true;
} catch (error) {
// 只有在token完全无效的情况下才清除token
// 401错误表示token有效但权限不足不应清除token
if (error && typeof error === "object" && "status" in error) {
// 如果是403 (Forbidden) 或401 (Unauthorized)说明token有效但权限不足
if (error.status === 401 || error.status === 403) {
return false;
}
// 其他状态码可能表示token无效清除token
AuthManager.clearToken();
} else {
// 网络错误等不清除token
console.error("管理员权限验证失败:", error);
}
} catch {
this.clearToken();
return false;
}
}
// 检查客户端是否已配置认证
public static isClientAuthenticated(client: SupportedClient): boolean {
const token = AuthManager.getToken();
return !!token;
static async isAdminAuthenticated(): Promise<boolean> {
if (!this.getToken()) return false;
try {
await this.createClient(DataClient).testAdminAuth();
return true;
} catch {
this.clearToken();
return false;
}
}
}

View File

@@ -17,7 +17,7 @@ export interface BoardData extends Board {
const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
// 远程升级相关参数
const devPort = 1234;
const remoteUpdater = AuthManager.createAuthenticatedRemoteUpdateClient();
const remoteUpdater = AuthManager.createClient(RemoteUpdateClient);
// 统一的板卡数据
const boards = ref<BoardData[]>([]);
@@ -35,13 +35,13 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
async function getAllBoards(): Promise<{ success: boolean; error?: string }> {
try {
// 验证管理员权限
const hasAdminAuth = await AuthManager.verifyAdminAuth();
const hasAdminAuth = await AuthManager.isAdminAuthenticated();
if (!hasAdminAuth) {
console.error("权限验证失败");
return { success: false, error: "权限不足" };
}
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
const result = await client.getAllBoards();
if (result) {
@@ -77,7 +77,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
): Promise<{ success: boolean; error?: string; boardId?: string }> {
try {
// 验证管理员权限
const hasAdminAuth = await AuthManager.verifyAdminAuth();
const hasAdminAuth = await AuthManager.isAdminAuthenticated();
if (!hasAdminAuth) {
console.error("权限验证失败");
return { success: false, error: "权限不足" };
@@ -89,11 +89,11 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
return { success: false, error: "参数不完整" };
}
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
const boardId = await client.addBoard(name);
if (boardId) {
console.log("新增板卡成功", { boardId, name});
console.log("新增板卡成功", { boardId, name });
// 刷新板卡列表
await getAllBoards();
return { success: true };
@@ -119,7 +119,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
): Promise<{ success: boolean; error?: string }> {
try {
// 验证管理员权限
const hasAdminAuth = await AuthManager.verifyAdminAuth();
const hasAdminAuth = await AuthManager.isAdminAuthenticated();
if (!hasAdminAuth) {
console.error("权限验证失败");
return { success: false, error: "权限不足" };
@@ -130,7 +130,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
return { success: false, error: "板卡ID不能为空" };
}
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
const result = await client.deleteBoard(boardId);
if (result > 0) {

View File

@@ -59,3 +59,12 @@ export function formatDate(date: Date | string) {
minute: "2-digit",
});
}
export function base64ToArrayBuffer(base64: string) {
var binaryString = atob(base64);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}

View File

@@ -4,7 +4,7 @@
// @ts-nocheck
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
import type { IDigitalTubesHub, IJtagHub, IProgressHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver } from './server.Hubs';
import type { ProgressInfo } from '../server.Hubs';
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
// components
@@ -107,6 +107,10 @@ class IDigitalTubesHub_HubProxy implements IDigitalTubesHub {
public readonly setFrequency = async (frequency: number): Promise<boolean> => {
return await this.connection.invoke("SetFrequency", frequency);
}
public readonly getStatus = async (): Promise<DigitalTubeTaskStatus> => {
return await this.connection.invoke("GetStatus");
}
}
class IJtagHub_HubProxyFactory implements HubProxyFactory<IJtagHub> {
@@ -157,6 +161,14 @@ class IProgressHub_HubProxy implements IProgressHub {
public readonly join = async (taskId: string): Promise<boolean> => {
return await this.connection.invoke("Join", taskId);
}
public readonly leave = async (taskId: string): Promise<boolean> => {
return await this.connection.invoke("Leave", taskId);
}
public readonly getProgress = async (taskId: string): Promise<ProgressInfo> => {
return await this.connection.invoke("GetProgress", taskId);
}
}

View File

@@ -3,7 +3,7 @@
/* tslint:disable */
// @ts-nocheck
import type { IStreamResult, Subject } from '@microsoft/signalr';
import type { ProgressInfo } from '../server.Hubs';
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
export type IDigitalTubesHub = {
/**
@@ -19,6 +19,10 @@ export type IDigitalTubesHub = {
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
setFrequency(frequency: number): Promise<boolean>;
/**
* @returns Transpiled from System.Threading.Tasks.Task<server.Hubs.DigitalTubeTaskStatus?>
*/
getStatus(): Promise<DigitalTubeTaskStatus>;
}
export type IJtagHub = {
@@ -44,6 +48,16 @@ export type IProgressHub = {
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
join(taskId: string): Promise<boolean>;
/**
* @param taskId Transpiled from string
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
leave(taskId: string): Promise<boolean>;
/**
* @param taskId Transpiled from string
* @returns Transpiled from System.Threading.Tasks.Task<server.Hubs.ProgressInfo?>
*/
getProgress(taskId: string): Promise<ProgressInfo>;
}
export type IDigitalTubesReceiver = {

View File

@@ -2,13 +2,20 @@
/* eslint-disable */
/* tslint:disable */
/** Transpiled from server.Hubs.DigitalTubeTaskStatus */
export type DigitalTubeTaskStatus = {
/** Transpiled from int */
frequency: number;
/** Transpiled from bool */
isRunning: boolean;
}
/** Transpiled from server.Hubs.ProgressStatus */
export enum ProgressStatus {
Pending = 0,
InProgress = 1,
Completed = 2,
Canceled = 3,
Failed = 4,
Running = 0,
Completed = 1,
Canceled = 2,
Failed = 3,
}
/** Transpiled from server.Hubs.ProgressInfo */
@@ -17,7 +24,7 @@ export type ProgressInfo = {
taskId: string;
/** Transpiled from server.Hubs.ProgressStatus */
status: ProgressStatus;
/** Transpiled from int */
/** Transpiled from double */
progressPercent: number;
/** Transpiled from string */
errorMessage: string;

View File

@@ -274,7 +274,7 @@ const handleSignUp = async () => {
// 页面初始化时检查是否已有有效token
const checkExistingToken = async () => {
try {
const isValid = await AuthManager.verifyToken();
const isValid = await AuthManager.isAuthenticated();
if (isValid) {
// 如果token仍然有效直接跳转到project页面
router.go(-1);

View File

@@ -418,7 +418,12 @@ import {
FileArchiveIcon,
FileJsonIcon,
} from "lucide-vue-next";
import { ExamDto, type FileParameter } from "@/APIClient";
import {
ExamClient,
ExamDto,
ResourceClient,
type FileParameter,
} from "@/APIClient";
import { useAlertStore } from "@/components/Alert";
import { AuthManager } from "@/utils/AuthManager";
import { useRequiredInjection } from "@/utils/Common";
@@ -618,7 +623,7 @@ const submitCreateExam = async () => {
isUpdating.value = true;
try {
const client = AuthManager.createAuthenticatedExamClient();
const client = AuthManager.createClient(ExamClient);
let exam: ExamInfo;
if (mode.value === "create") {
@@ -671,7 +676,7 @@ const submitCreateExam = async () => {
// 上传实验资源
async function uploadExamResources(examId: string) {
const client = AuthManager.createAuthenticatedResourceClient();
const client = AuthManager.createClient(ResourceClient);
try {
// 上传MD文档
@@ -750,7 +755,7 @@ function close() {
}
async function editExam(examId: string) {
const client = AuthManager.createAuthenticatedExamClient();
const client = AuthManager.createClient(ExamClient);
const examInfo = await client.getExam(examId);
editExamInfo.value = {

View File

@@ -250,7 +250,13 @@
</div>
</template>
<script setup lang="ts">
import { ResourcePurpose, type ExamInfo, type ResourceInfo } from "@/APIClient";
import {
ExamClient,
ResourceClient,
ResourcePurpose,
type ExamInfo,
type ResourceInfo,
} from "@/APIClient";
import { useAlertStore } from "@/components/Alert";
import { AuthManager } from "@/utils/AuthManager";
import { useRequiredInjection } from "@/utils/Common";
@@ -274,7 +280,7 @@ const props = defineProps<{
const commitsList = ref<ResourceInfo[]>();
async function updateCommits() {
const client = AuthManager.createAuthenticatedExamClient();
const client = AuthManager.createClient(ExamClient);
const list = await client.getCommitsByExamId(props.selectedExam.id);
commitsList.value = list;
}
@@ -288,7 +294,7 @@ const downloadResources = async () => {
downloadingResources.value = true;
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resourceClient = AuthManager.createClient(ResourceClient);
// 获取资源包列表(模板资源)
const resourceList = await resourceClient.getResourceList(

View File

@@ -181,7 +181,7 @@
import { ref, onMounted, computed } from "vue";
import { useRoute } from "vue-router";
import { AuthManager } from "@/utils/AuthManager";
import { type ExamInfo } from "@/APIClient";
import { ExamClient, type ExamInfo } from "@/APIClient";
import { formatDate } from "@/utils/Common";
import ExamInfoModal from "./ExamInfoModal.vue";
import ExamEditModal from "./ExamEditModal.vue";
@@ -206,7 +206,7 @@ async function refreshExams() {
error.value = "";
try {
const client = AuthManager.createAuthenticatedExamClient();
const client = AuthManager.createClient(ExamClient);
exams.value = await client.getExamList();
} catch (err: any) {
error.value = err.message || "获取实验列表失败";
@@ -218,7 +218,7 @@ async function refreshExams() {
async function viewExam(examId: string) {
try {
const client = AuthManager.createAuthenticatedExamClient();
const client = AuthManager.createClient(ExamClient);
selectedExam.value = await client.getExam(examId);
showInfoModal.value = true;
} catch (err: any) {
@@ -248,7 +248,7 @@ onMounted(async () => {
router.push("/login");
}
isAdmin.value = await AuthManager.verifyAdminAuth();
isAdmin.value = await AuthManager.isAdminAuthenticated();
await refreshExams();

View File

@@ -266,7 +266,12 @@
</template>
<script setup lang="ts">
import { CaptureMode, ChannelConfig, DebuggerConfig } from "@/APIClient";
import {
CaptureMode,
ChannelConfig,
DebuggerClient,
DebuggerConfig,
} from "@/APIClient";
import { useAlertStore } from "@/components/Alert";
import BaseInputField from "@/components/InputField/BaseInputField.vue";
import type { LogicDataType } from "@/components/WaveformDisplay";
@@ -421,7 +426,7 @@ async function startCapture() {
}
isCapturing.value = true;
const client = AuthManager.createAuthenticatedDebuggerClient();
const client = AuthManager.createClient(DebuggerClient);
// 构造API配置
const channelConfigs = channels.value

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
@layout="handleVerticalSplitterResize"
>
<!-- 使用 v-show 替代 v-if -->
<SplitterPanel
<SplitterPanel
v-show="!isBottomBarFullscreen"
id="splitter-group-v-panel-project"
:default-size="verticalSplitterSize"
@@ -60,8 +60,8 @@
v-show="showDocPanel"
class="doc-panel overflow-y-auto h-full"
>
<MarkdownRenderer
:content="documentContent"
<MarkdownRenderer
:content="documentContent"
:examId="(route.query.examId as string) || ''"
/>
</div>
@@ -80,11 +80,13 @@
<!-- 功能底栏 -->
<SplitterPanel
id="splitter-group-v-panel-bar"
:default-size="isBottomBarFullscreen ? 100 : (100 - verticalSplitterSize)"
:default-size="
isBottomBarFullscreen ? 100 : 100 - verticalSplitterSize
"
:min-size="isBottomBarFullscreen ? 100 : 15"
class="w-full overflow-hidden pt-3"
>
<BottomBar
<BottomBar
:isFullscreen="isBottomBarFullscreen"
@toggle-fullscreen="handleToggleBottomBarFullscreen"
/>
@@ -106,22 +108,48 @@
/>
<!-- Navbar切换浮动按钮 -->
<div
<div
class="navbar-toggle-btn"
:class="{ 'with-navbar': navbarControl.showNavbar.value }"
>
<button
<button
@click="navbarControl.toggleNavbar"
class="btn btn-circle btn-primary shadow-lg hover:shadow-xl transition-all duration-300"
:class="{ 'btn-outline': navbarControl.showNavbar.value }"
:title="navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'"
:title="
navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'
"
>
<!-- 使用SVG图标表示菜单/关闭状态 -->
<svg v-if="navbarControl.showNavbar.value" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<svg
v-if="navbarControl.showNavbar.value"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
@@ -131,7 +159,7 @@
<script setup lang="ts">
import { ref, onMounted, watch, inject, type Ref } from "vue";
import { useRouter } from "vue-router";
import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入
import { useLocalStorage } from "@vueuse/core"; // 添加VueUse导入
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
import DiagramCanvas from "@/components/LabCanvas/DiagramCanvas.vue";
import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
@@ -143,7 +171,7 @@ import { useProvideComponentManager } from "@/components/LabCanvas";
import { useAlertStore } from "@/components/Alert";
import { AuthManager } from "@/utils/AuthManager";
import { useEquipments } from "@/stores/equipments";
import type { Board } from "@/APIClient";
import { DataClient, ResourceClient, type Board } from "@/APIClient";
import { useRoute } from "vue-router";
const route = useRoute();
@@ -158,20 +186,29 @@ const equipments = useEquipments();
const alert = useAlertStore();
// --- Navbar控制 ---
const navbarControl = inject('navbar') as {
const navbarControl = inject("navbar") as {
showNavbar: Ref<boolean>;
toggleNavbar: () => void;
};
// --- 使用VueUse保存分栏状态 ---
// 左右分栏比例默认60%
const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60);
const horizontalSplitterSize = useLocalStorage(
"project-horizontal-splitter-size",
60,
);
// 上下分栏比例默认80%
const verticalSplitterSize = useLocalStorage('project-vertical-splitter-size', 80);
const verticalSplitterSize = useLocalStorage(
"project-vertical-splitter-size",
80,
);
// 底栏全屏状态
const isBottomBarFullscreen = useLocalStorage('project-bottom-bar-fullscreen', false);
const isBottomBarFullscreen = useLocalStorage(
"project-bottom-bar-fullscreen",
false,
);
// 文档面板显示状态
const showDocPanel = useLocalStorage('project-show-doc-panel', false);
const showDocPanel = useLocalStorage("project-show-doc-panel", false);
function handleToggleBottomBarFullscreen() {
isBottomBarFullscreen.value = !isBottomBarFullscreen.value;
@@ -216,25 +253,25 @@ async function loadDocumentContent() {
const examId = route.query.examId as string;
if (examId) {
// 如果有实验ID从API加载实验文档
console.log('加载实验文档:', examId);
const client = AuthManager.createAuthenticatedResourceClient();
console.log("加载实验文档:", examId);
const client = AuthManager.createClient(ResourceClient);
// 获取markdown类型的模板资源列表
const resources = await client.getResourceList(examId, 'doc', 'template');
const resources = await client.getResourceList(examId, "doc", "template");
if (resources && resources.length > 0) {
// 获取第一个markdown资源
const markdownResource = resources[0];
// 使用新的ResourceClient API获取资源文件内容
const response = await client.getResourceById(markdownResource.id);
if (!response || !response.data) {
throw new Error('获取markdown文件失败');
throw new Error("获取markdown文件失败");
}
const content = await response.data.text();
// 更新文档内容暂时不处理图片路径由MarkdownRenderer处理
documentContent.value = content;
} else {
@@ -279,17 +316,17 @@ function updateComponentDirectProp(
// 检查并初始化用户实验板
async function checkAndInitializeBoard() {
try {
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
const userInfo = await client.getUserInfo();
if (userInfo.boardID && userInfo.boardID.trim() !== '') {
if (userInfo.boardID && userInfo.boardID.trim() !== "") {
// 用户已绑定实验板获取实验板信息并更新到equipment
try {
const board = await client.getBoardByID(userInfo.boardID);
updateEquipmentFromBoard(board);
alert?.show(`实验板 ${board.boardName} 已连接`, "success");
} catch (boardError) {
console.error('获取实验板信息失败:', boardError);
console.error("获取实验板信息失败:", boardError);
alert?.show("获取实验板信息失败", "error");
showRequestBoardDialog.value = true;
}
@@ -298,7 +335,7 @@ async function checkAndInitializeBoard() {
showRequestBoardDialog.value = true;
}
} catch (error) {
console.error('检查用户实验板失败:', error);
console.error("检查用户实验板失败:", error);
alert?.show("检查用户信息失败", "error");
showRequestBoardDialog.value = true;
}
@@ -308,12 +345,12 @@ async function checkAndInitializeBoard() {
function updateEquipmentFromBoard(board: Board) {
equipments.boardAddr = board.ipAddr;
equipments.boardPort = board.port;
console.log(`实验板信息已更新到equipment store:`, {
address: board.ipAddr,
port: board.port,
boardName: board.boardName,
boardId: board.id
boardId: board.id,
});
}
@@ -321,7 +358,7 @@ function updateEquipmentFromBoard(board: Board) {
function handleRequestBoardClose() {
showRequestBoardDialog.value = false;
// 如果用户取消申请,可以选择返回上一页或显示警告
router.push('/');
router.push("/");
}
// 处理申请实验板成功
@@ -338,12 +375,12 @@ onMounted(async () => {
const isAuthenticated = await AuthManager.isAuthenticated();
if (!isAuthenticated) {
// 验证失败,跳转到登录页面
router.push('/login');
router.push("/login");
return;
}
} catch (error) {
console.error('身份验证失败:', error);
router.push('/login');
console.error("身份验证失败:", error);
router.push("/login");
return;
}

View File

@@ -75,7 +75,7 @@ import { ref, watch } from "vue";
import { CheckCircle } from "lucide-vue-next";
import { AuthManager } from "@/utils/AuthManager";
import { useAlertStore } from "@/components/Alert";
import type { Board } from "@/APIClient";
import { DataClient, type Board } from "@/APIClient";
interface Props {
open: boolean;
@@ -113,7 +113,7 @@ async function checkUserBoard() {
boardInfo.value = null;
try {
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
const userInfo = await client.getUserInfo();
if (userInfo.boardID && userInfo.boardID.trim() !== "") {
@@ -140,7 +140,7 @@ async function requestBoard() {
requesting.value = true;
try {
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
const board = await client.getAvailableBoard(undefined);
if (board) {

View File

@@ -433,7 +433,7 @@ const currentVideoSource = ref("");
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
// API 客户端
const videoClient = AuthManager.createAuthenticatedVideoStreamClient();
const videoClient = AuthManager.createClient(VideoStreamClient);
// 添加日志
const addLog = (level: string, message: string) => {

View File

@@ -174,7 +174,12 @@
import { ref, reactive, watch } from "vue";
import { AuthManager } from "../../utils/AuthManager";
import { useAlertStore } from "../../components/Alert";
import { BoardStatus, type NetworkConfigDto } from "../../APIClient";
import {
BoardStatus,
DataClient,
NetConfigClient,
type NetworkConfigDto,
} from "../../APIClient";
import { useRequiredInjection } from "@/utils/Common";
import { useBoardManager } from "@/utils/BoardManager";
@@ -267,8 +272,7 @@ async function handleSubmit() {
isSubmitting.value = true;
try {
// 通过 AuthManager 获取认证的 DataClient
const dataClient = AuthManager.createAuthenticatedDataClient();
const dataClient = AuthManager.createClient(DataClient);
// 添加板卡到数据库
const boardId = await dataClient.addBoard(form.name.trim());
@@ -293,8 +297,7 @@ async function handleCancelPairing() {
if (!addedBoardId.value) return;
try {
// 通过 AuthManager 获取认证的 DataClient
const dataClient = AuthManager.createAuthenticatedDataClient();
const dataClient = AuthManager.createClient(DataClient);
// 删除添加的板卡
await dataClient.deleteBoard(addedBoardId.value);
@@ -317,8 +320,8 @@ async function handlePairingConfirm() {
try {
// 通过 AuthManager 获取认证的客户端
const dataClient = AuthManager.createAuthenticatedDataClient();
const netConfigClient = AuthManager.createAuthenticatedNetConfigClient();
const dataClient = AuthManager.createClient(DataClient);
const netConfigClient = AuthManager.createClient(NetConfigClient);
// 获取数据库中对应分配的板卡信息
const boardInfo = await dataClient.getBoardByID(addedBoardId.value);
@@ -365,7 +368,7 @@ async function handlePairingConfirm() {
// 配置失败,删除数据库中的板卡信息
try {
const dataClient = AuthManager.createAuthenticatedDataClient();
const dataClient = AuthManager.createClient(DataClient);
await dataClient.deleteBoard(addedBoardId.value);
} catch (deleteError) {
console.error("删除板卡失败:", deleteError);

View File

@@ -42,12 +42,12 @@ const isAdmin = ref(false);
function setActivePage(event: Event) {
const target = event.currentTarget as HTMLLinkElement;
const newPage = toNumber(target.id);
// 如果用户不是管理员但试图访问管理员页面,则忽略
if (newPage === 100 && !isAdmin.value) {
return;
}
activePage.value = newPage;
}
@@ -60,16 +60,16 @@ onMounted(async () => {
// 这里可以使用路由跳转
return;
}
// 验证管理员权限
isAdmin.value = await AuthManager.verifyAdminAuth();
isAdmin.value = await AuthManager.isAdminAuthenticated();
// 如果当前页面是管理员页面但用户不是管理员,切换到用户信息页面
if (activePage.value === 100 && !isAdmin.value) {
activePage.value = 1;
}
} catch (error) {
console.error('用户认证检查失败:', error);
console.error("用户认证检查失败:", error);
// 可以在这里处理错误,比如显示错误信息或重定向到登录页面
}
});

View File

@@ -273,7 +273,13 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { AuthManager } from "@/utils/AuthManager";
import { UserInfo, Board, BoardStatus } from "@/APIClient";
import {
UserInfo,
Board,
BoardStatus,
DataClient,
JtagClient,
} from "@/APIClient";
import { Alert, useAlertStore } from "@/components/Alert";
import {
User,
@@ -319,7 +325,7 @@ const loadBoardInfo = async () => {
}
try {
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
boardInfo.value = await client.getBoardByID(userInfo.value.boardID);
} catch (err) {
console.error("加载实验板信息失败:", err);
@@ -335,7 +341,7 @@ const loadUserInfo = async (showSuccessMessage = false) => {
try {
await new Promise((resolve) => setTimeout(resolve, 200));
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
userInfo.value = await client.getUserInfo();
// 如果有绑定的实验板ID加载实验板信息
@@ -370,7 +376,7 @@ const applyBoard = async () => {
alertStore?.info("正在申请实验板...");
try {
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
// 获取可用的实验板
const availableBoard = await client.getAvailableBoard(undefined);
@@ -407,7 +413,7 @@ const testBoardConnection = async () => {
alertStore?.info("正在测试连接...");
try {
const jtagClient = AuthManager.createAuthenticatedJtagClient();
const jtagClient = AuthManager.createClient(JtagClient);
// 使用JTAG客户端读取设备ID Code
const idCode = await jtagClient.getDeviceIDCode(
@@ -444,7 +450,7 @@ const unbindBoard = async () => {
alertStore?.info("正在解绑实验板...");
try {
const client = AuthManager.createAuthenticatedDataClient();
const client = AuthManager.createClient(DataClient);
const success = await client.unbindBoard();
if (success) {