feat: 使用SignalR实时发送示波器数据,并美化示波器界面
This commit is contained in:
@@ -88,7 +88,8 @@ try
|
|||||||
path.StartsWithSegments("/hubs/JtagHub") ||
|
path.StartsWithSegments("/hubs/JtagHub") ||
|
||||||
path.StartsWithSegments("/hubs/ProgressHub") ||
|
path.StartsWithSegments("/hubs/ProgressHub") ||
|
||||||
path.StartsWithSegments("/hubs/DigitalTubesHub") ||
|
path.StartsWithSegments("/hubs/DigitalTubesHub") ||
|
||||||
path.StartsWithSegments("/hubs/RotaryEncoderHub")
|
path.StartsWithSegments("/hubs/RotaryEncoderHub") ||
|
||||||
|
path.StartsWithSegments("/hubs/OscilloscopeHub")
|
||||||
))
|
))
|
||||||
{
|
{
|
||||||
// Read the token out of the query string
|
// Read the token out of the query string
|
||||||
@@ -256,6 +257,7 @@ try
|
|||||||
app.MapHub<server.Hubs.ProgressHub>("/hubs/ProgressHub");
|
app.MapHub<server.Hubs.ProgressHub>("/hubs/ProgressHub");
|
||||||
app.MapHub<server.Hubs.DigitalTubesHub>("/hubs/DigitalTubesHub");
|
app.MapHub<server.Hubs.DigitalTubesHub>("/hubs/DigitalTubesHub");
|
||||||
app.MapHub<server.Hubs.RotaryEncoderHub>("/hubs/RotaryEncoderHub");
|
app.MapHub<server.Hubs.RotaryEncoderHub>("/hubs/RotaryEncoderHub");
|
||||||
|
app.MapHub<server.Hubs.OscilloscopeHub>("/hubs/OscilloscopeHub");
|
||||||
|
|
||||||
// Setup Program
|
// Setup Program
|
||||||
MsgBus.Init();
|
MsgBus.Init();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Peripherals.OscilloscopeClient;
|
using Peripherals.OscilloscopeClient;
|
||||||
|
using server.Hubs;
|
||||||
|
|
||||||
namespace server.Controllers;
|
namespace server.Controllers;
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ namespace server.Controllers;
|
|||||||
/// 示波器API控制器 - 普通用户权限
|
/// 示波器API控制器 - 普通用户权限
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[EnableCors("Development")]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class OscilloscopeApiController : ControllerBase
|
public class OscilloscopeApiController : ControllerBase
|
||||||
@@ -20,7 +22,7 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取示波器实例
|
/// 获取示波器实例
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Oscilloscope? GetOscilloscope()
|
private OscilloscopeCtrl? GetOscilloscope()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -41,7 +43,7 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var board = boardRet.Value.Value;
|
var board = boardRet.Value.Value;
|
||||||
return new Oscilloscope(board.IpAddr, board.Port);
|
return new OscilloscopeCtrl(board.IpAddr, board.Port);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -56,12 +58,11 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
/// <param name="config">示波器配置</param>
|
/// <param name="config">示波器配置</param>
|
||||||
/// <returns>操作结果</returns>
|
/// <returns>操作结果</returns>
|
||||||
[HttpPost("Initialize")]
|
[HttpPost("Initialize")]
|
||||||
[EnableCors("Users")]
|
|
||||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
public async Task<IActionResult> Initialize([FromBody] OscilloscopeFullConfig config)
|
public async Task<IActionResult> Initialize([FromBody] OscilloscopeConfig config)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -151,7 +152,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>操作结果</returns>
|
/// <returns>操作结果</returns>
|
||||||
[HttpPost("StartCapture")]
|
[HttpPost("StartCapture")]
|
||||||
[EnableCors("Users")]
|
|
||||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -185,7 +185,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>操作结果</returns>
|
/// <returns>操作结果</returns>
|
||||||
[HttpPost("StopCapture")]
|
[HttpPost("StopCapture")]
|
||||||
[EnableCors("Users")]
|
|
||||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -219,7 +218,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>示波器数据和状态信息</returns>
|
/// <returns>示波器数据和状态信息</returns>
|
||||||
[HttpGet("GetData")]
|
[HttpGet("GetData")]
|
||||||
[EnableCors("Users")]
|
|
||||||
[ProducesResponseType(typeof(OscilloscopeDataResponse), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(OscilloscopeDataResponse), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -293,7 +291,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
/// <param name="risingEdge">触发边沿(true为上升沿,false为下降沿)</param>
|
/// <param name="risingEdge">触发边沿(true为上升沿,false为下降沿)</param>
|
||||||
/// <returns>操作结果</returns>
|
/// <returns>操作结果</returns>
|
||||||
[HttpPost("UpdateTrigger")]
|
[HttpPost("UpdateTrigger")]
|
||||||
[EnableCors("Users")]
|
|
||||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -338,7 +335,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
/// <param name="decimationRate">抽样率(0-1023)</param>
|
/// <param name="decimationRate">抽样率(0-1023)</param>
|
||||||
/// <returns>操作结果</returns>
|
/// <returns>操作结果</returns>
|
||||||
[HttpPost("UpdateSampling")]
|
[HttpPost("UpdateSampling")]
|
||||||
[EnableCors("Users")]
|
|
||||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -387,7 +383,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>操作结果</returns>
|
/// <returns>操作结果</returns>
|
||||||
[HttpPost("RefreshRAM")]
|
[HttpPost("RefreshRAM")]
|
||||||
[EnableCors("Users")]
|
|
||||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -415,72 +410,4 @@ public class OscilloscopeApiController : ControllerBase
|
|||||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 示波器完整配置
|
|
||||||
/// </summary>
|
|
||||||
public class OscilloscopeFullConfig
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 是否启动捕获
|
|
||||||
/// </summary>
|
|
||||||
public bool CaptureEnabled { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 触发电平(0-255)
|
|
||||||
/// </summary>
|
|
||||||
public byte TriggerLevel { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 触发边沿(true为上升沿,false为下降沿)
|
|
||||||
/// </summary>
|
|
||||||
public bool TriggerRisingEdge { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 水平偏移量(0-1023)
|
|
||||||
/// </summary>
|
|
||||||
public ushort HorizontalShift { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 抽样率(0-1023)
|
|
||||||
/// </summary>
|
|
||||||
public ushort DecimationRate { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否自动刷新RAM
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoRefreshRAM { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 示波器状态和数据
|
|
||||||
/// </summary>
|
|
||||||
public class OscilloscopeDataResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// AD采样频率
|
|
||||||
/// </summary>
|
|
||||||
public uint ADFrequency { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AD采样幅度
|
|
||||||
/// </summary>
|
|
||||||
public byte ADVpp { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AD采样最大值
|
|
||||||
/// </summary>
|
|
||||||
public byte ADMax { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AD采样最小值
|
|
||||||
/// </summary>
|
|
||||||
public byte ADMin { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 波形数据(Base64编码)
|
|
||||||
/// </summary>
|
|
||||||
public string WaveformData { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,15 +32,9 @@ public class DigitalTubeTaskStatus
|
|||||||
{
|
{
|
||||||
public int Frequency { get; set; } = 100;
|
public int Frequency { get; set; } = 100;
|
||||||
public bool IsRunning { get; set; } = false;
|
public bool IsRunning { get; set; } = false;
|
||||||
|
|
||||||
public DigitalTubeTaskStatus(ScanTaskInfo info)
|
|
||||||
{
|
|
||||||
Frequency = info.Frequency;
|
|
||||||
IsRunning = info.IsRunning;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ScanTaskInfo
|
class DigitalTubesScanTaskInfo
|
||||||
{
|
{
|
||||||
public string BoardID { get; set; }
|
public string BoardID { get; set; }
|
||||||
public string ClientID { get; set; }
|
public string ClientID { get; set; }
|
||||||
@@ -50,13 +44,22 @@ public class ScanTaskInfo
|
|||||||
public int Frequency { get; set; } = 100;
|
public int Frequency { get; set; } = 100;
|
||||||
public bool IsRunning { get; set; } = false;
|
public bool IsRunning { get; set; } = false;
|
||||||
|
|
||||||
public ScanTaskInfo(
|
public DigitalTubesScanTaskInfo(
|
||||||
string boardID, string clientID, SevenDigitalTubesCtrl client)
|
string boardID, string clientID, SevenDigitalTubesCtrl client)
|
||||||
{
|
{
|
||||||
BoardID = boardID;
|
BoardID = boardID;
|
||||||
ClientID = clientID;
|
ClientID = clientID;
|
||||||
TubeClient = client;
|
TubeClient = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DigitalTubeTaskStatus ToDigitalTubeTaskStatus()
|
||||||
|
{
|
||||||
|
return new DigitalTubeTaskStatus
|
||||||
|
{
|
||||||
|
Frequency = Frequency,
|
||||||
|
IsRunning = IsRunning
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@@ -67,7 +70,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
|
|||||||
private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
|
private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
|
||||||
private readonly Database.UserManager _userManager = new();
|
private readonly Database.UserManager _userManager = new();
|
||||||
|
|
||||||
private ConcurrentDictionary<(string, string), ScanTaskInfo> _scanTasks = new();
|
private ConcurrentDictionary<(string, string), DigitalTubesScanTaskInfo> _scanTasks = new();
|
||||||
|
|
||||||
public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
|
public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
|
||||||
{
|
{
|
||||||
@@ -100,7 +103,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
|
|||||||
return boardRet.Value.Value;
|
return boardRet.Value.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ScanAllTubes(ScanTaskInfo scanInfo)
|
private Task ScanAllTubes(DigitalTubesScanTaskInfo scanInfo)
|
||||||
{
|
{
|
||||||
var token = scanInfo.CTS.Token;
|
var token = scanInfo.CTS.Token;
|
||||||
return Task.Run(async () =>
|
return Task.Run(async () =>
|
||||||
@@ -163,7 +166,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
var scanTaskInfo = new ScanTaskInfo(
|
var scanTaskInfo = new DigitalTubesScanTaskInfo(
|
||||||
board.ID.ToString(), Context.ConnectionId,
|
board.ID.ToString(), Context.ConnectionId,
|
||||||
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 0)
|
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 0)
|
||||||
);
|
);
|
||||||
@@ -240,7 +243,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
|
|||||||
|
|
||||||
if (_scanTasks.TryGetValue(key, out var scanInfo))
|
if (_scanTasks.TryGetValue(key, out var scanInfo))
|
||||||
{
|
{
|
||||||
return new DigitalTubeTaskStatus(scanInfo);
|
return scanInfo.ToDigitalTubeTaskStatus();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
386
server/src/Hubs/OscilloscopeHub.cs
Normal file
386
server/src/Hubs/OscilloscopeHub.cs
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using TypedSignalR.Client;
|
||||||
|
using DotNext;
|
||||||
|
using Tapper;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Peripherals.OscilloscopeClient;
|
||||||
|
|
||||||
|
#pragma warning disable 1998
|
||||||
|
|
||||||
|
namespace server.Hubs;
|
||||||
|
|
||||||
|
[Hub]
|
||||||
|
public interface IOscilloscopeHub
|
||||||
|
{
|
||||||
|
Task<bool> Initialize(OscilloscopeFullConfig config);
|
||||||
|
Task<bool> StartCapture();
|
||||||
|
Task<bool> StopCapture();
|
||||||
|
Task<OscilloscopeDataResponse?> GetData();
|
||||||
|
Task<bool> SetTrigger(byte level);
|
||||||
|
Task<bool> SetRisingEdge(bool risingEdge);
|
||||||
|
Task<bool> SetSampling(ushort decimationRate);
|
||||||
|
Task<bool> SetFrequency(int frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Receiver]
|
||||||
|
public interface IOscilloscopeReceiver
|
||||||
|
{
|
||||||
|
Task OnDataReceived(OscilloscopeDataResponse data);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TranspilationSource]
|
||||||
|
public class OscilloscopeDataResponse
|
||||||
|
{
|
||||||
|
public uint ADFrequency { get; set; }
|
||||||
|
public byte ADVpp { get; set; }
|
||||||
|
public byte ADMax { get; set; }
|
||||||
|
public byte ADMin { get; set; }
|
||||||
|
public string WaveformData { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[TranspilationSource]
|
||||||
|
public class OscilloscopeFullConfig
|
||||||
|
{
|
||||||
|
public bool CaptureEnabled { get; set; }
|
||||||
|
public byte TriggerLevel { get; set; }
|
||||||
|
public bool TriggerRisingEdge { get; set; }
|
||||||
|
public ushort HorizontalShift { get; set; }
|
||||||
|
public ushort DecimationRate { get; set; }
|
||||||
|
public int CaptureFrequency { get; set; }
|
||||||
|
// public bool AutoRefreshRAM { get; set; }
|
||||||
|
|
||||||
|
public OscilloscopeConfig ToOscilloscopeConfig()
|
||||||
|
{
|
||||||
|
return new OscilloscopeConfig
|
||||||
|
{
|
||||||
|
CaptureEnabled = CaptureEnabled,
|
||||||
|
TriggerLevel = TriggerLevel,
|
||||||
|
TriggerRisingEdge = TriggerRisingEdge,
|
||||||
|
HorizontalShift = HorizontalShift,
|
||||||
|
DecimationRate = DecimationRate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OscilloscopeScanTaskInfo
|
||||||
|
{
|
||||||
|
public Task? ScanTask { get; set; }
|
||||||
|
public OscilloscopeCtrl Client { get; set; }
|
||||||
|
public CancellationTokenSource CTS { get; set; } = new();
|
||||||
|
public int Frequency { get; set; } = 100;
|
||||||
|
public bool IsRunning { get; set; } = false;
|
||||||
|
|
||||||
|
public OscilloscopeScanTaskInfo(OscilloscopeCtrl client)
|
||||||
|
{
|
||||||
|
Client = client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[EnableCors("SignalR")]
|
||||||
|
public class OscilloscopeHub : Hub<IOscilloscopeReceiver>, IOscilloscopeHub
|
||||||
|
{
|
||||||
|
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||||
|
private readonly IHubContext<OscilloscopeHub, IOscilloscopeReceiver> _hubContext;
|
||||||
|
private readonly Database.UserManager _userManager = new();
|
||||||
|
|
||||||
|
private ConcurrentDictionary<(string, string), OscilloscopeScanTaskInfo> _scanTasks = new();
|
||||||
|
|
||||||
|
public OscilloscopeHub(IHubContext<OscilloscopeHub, IOscilloscopeReceiver> hubContext)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Database.Board> TryGetBoard()
|
||||||
|
{
|
||||||
|
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
|
||||||
|
if (string.IsNullOrEmpty(userName))
|
||||||
|
{
|
||||||
|
logger.Error("User name is null or empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var boardRet = _userManager.GetBoardByUserName(userName);
|
||||||
|
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
|
||||||
|
{
|
||||||
|
logger.Error($"Board not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return boardRet.Value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<OscilloscopeCtrl> GetOscilloscope()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
|
||||||
|
var client = new OscilloscopeCtrl(board.IpAddr, board.Port, 0);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Failed to get oscilloscope");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Initialize(OscilloscopeFullConfig config)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
|
||||||
|
|
||||||
|
var result = await client.Init(config.ToOscilloscopeConfig());
|
||||||
|
if (!result.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error(result.Error, "Initialize failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Failed to initialize oscilloscope");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> StartCapture()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
|
||||||
|
var key = (board.ID.ToString(), Context.ConnectionId);
|
||||||
|
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
|
||||||
|
|
||||||
|
if (_scanTasks.TryGetValue(key, out var existing) && existing.IsRunning)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var result = await client.SetCaptureEnable(true);
|
||||||
|
if (!result.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error(result.Error, "StartCapture failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanTaskInfo = new OscilloscopeScanTaskInfo(client);
|
||||||
|
var token = scanTaskInfo.CTS.Token;
|
||||||
|
scanTaskInfo.ScanTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var data = await GetData();
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
logger.Error("GetData failed");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Clients.Client(Context.ConnectionId).OnDataReceived(data);
|
||||||
|
await Task.Delay(1000 / scanTaskInfo.Frequency, token);
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
_scanTasks[key] = scanTaskInfo;
|
||||||
|
|
||||||
|
return result.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Failed to start capture");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> StopCapture()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
|
||||||
|
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
|
||||||
|
|
||||||
|
var key = (board.ID.ToString(), Context.ConnectionId);
|
||||||
|
|
||||||
|
if (_scanTasks.TryRemove(key, out var scanInfo))
|
||||||
|
{
|
||||||
|
scanInfo.IsRunning = false;
|
||||||
|
scanInfo.CTS.Cancel();
|
||||||
|
if (scanInfo.ScanTask != null) await scanInfo.ScanTask;
|
||||||
|
scanInfo.CTS.Dispose();
|
||||||
|
|
||||||
|
var result = await client.SetCaptureEnable(false);
|
||||||
|
if (!result.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error(result.Error, "StopCapture failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Task not found");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Failed to stop capture");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OscilloscopeDataResponse?> GetData()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var oscilloscope = GetOscilloscope().OrThrow(() => new Exception("用户未绑定有效的实验板"));
|
||||||
|
|
||||||
|
var freqResult = await oscilloscope.GetADFrequency();
|
||||||
|
var vppResult = await oscilloscope.GetADVpp();
|
||||||
|
var maxResult = await oscilloscope.GetADMax();
|
||||||
|
var minResult = await oscilloscope.GetADMin();
|
||||||
|
var waveformResult = await oscilloscope.GetWaveformData();
|
||||||
|
|
||||||
|
if (!freqResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"获取AD采样频率失败: {freqResult.Error}");
|
||||||
|
throw new Exception($"获取AD采样频率失败: {freqResult.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vppResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"获取AD采样幅度失败: {vppResult.Error}");
|
||||||
|
throw new Exception($"获取AD采样幅度失败: {vppResult.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!maxResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"获取AD采样最大值失败: {maxResult.Error}");
|
||||||
|
throw new Exception($"获取AD采样最大值失败: {maxResult.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!minResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"获取AD采样最小值失败: {minResult.Error}");
|
||||||
|
throw new Exception($"获取AD采样最小值失败: {minResult.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!waveformResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"获取波形数据失败: {waveformResult.Error}");
|
||||||
|
throw new Exception($"获取波形数据失败: {waveformResult.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = new OscilloscopeDataResponse
|
||||||
|
{
|
||||||
|
ADFrequency = freqResult.Value,
|
||||||
|
ADVpp = vppResult.Value,
|
||||||
|
ADMax = maxResult.Value,
|
||||||
|
ADMin = minResult.Value,
|
||||||
|
WaveformData = Convert.ToBase64String(waveformResult.Value)
|
||||||
|
};
|
||||||
|
|
||||||
|
return new OscilloscopeDataResponse
|
||||||
|
{
|
||||||
|
ADFrequency = freqResult.Value,
|
||||||
|
ADVpp = vppResult.Value,
|
||||||
|
ADMax = maxResult.Value,
|
||||||
|
ADMin = minResult.Value,
|
||||||
|
WaveformData = Convert.ToBase64String(waveformResult.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "获取示波器数据时发生异常");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetTrigger(byte level)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
|
||||||
|
var ret = await client.SetTriggerLevel(level);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error(ret.Error, "UpdateTrigger failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ret.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Failed to update trigger");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetRisingEdge(bool risingEdge)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
|
||||||
|
var ret = await client.SetTriggerEdge(risingEdge);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error(ret.Error, "Update Rising Edge failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ret.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "SetRisingEdge failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetSampling(ushort decimationRate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
|
||||||
|
var result = await client.SetDecimationRate(decimationRate);
|
||||||
|
if (!result.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error(result.Error, "UpdateSampling failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Failed to update sampling");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetFrequency(int frequency)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (frequency < 1 || frequency > 1000)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
|
||||||
|
var key = (board.ID.ToString(), Context.ConnectionId);
|
||||||
|
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,20 @@ using System.Net;
|
|||||||
using Common;
|
using Common;
|
||||||
using DotNext;
|
using DotNext;
|
||||||
using WebProtocol;
|
using WebProtocol;
|
||||||
|
using Tapper;
|
||||||
|
|
||||||
namespace Peripherals.OscilloscopeClient;
|
namespace Peripherals.OscilloscopeClient;
|
||||||
|
|
||||||
|
public class OscilloscopeConfig
|
||||||
|
{
|
||||||
|
public bool CaptureEnabled { get; set; }
|
||||||
|
public byte TriggerLevel { get; set; }
|
||||||
|
public bool TriggerRisingEdge { get; set; }
|
||||||
|
public ushort HorizontalShift { get; set; }
|
||||||
|
public ushort DecimationRate { get; set; }
|
||||||
|
// public bool AutoRefreshRAM { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
static class OscilloscopeAddr
|
static class OscilloscopeAddr
|
||||||
{
|
{
|
||||||
const UInt32 BASE = 0x8000_0000;
|
const UInt32 BASE = 0x8000_0000;
|
||||||
@@ -71,7 +82,7 @@ static class OscilloscopeAddr
|
|||||||
public const UInt32 RD_DATA_LENGTH = 0x0000_0400;
|
public const UInt32 RD_DATA_LENGTH = 0x0000_0400;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Oscilloscope
|
class OscilloscopeCtrl
|
||||||
{
|
{
|
||||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
@@ -88,7 +99,7 @@ class Oscilloscope
|
|||||||
/// <param name="address">示波器设备IP地址</param>
|
/// <param name="address">示波器设备IP地址</param>
|
||||||
/// <param name="port">示波器设备端口</param>
|
/// <param name="port">示波器设备端口</param>
|
||||||
/// <param name="timeout">超时时间(毫秒)</param>
|
/// <param name="timeout">超时时间(毫秒)</param>
|
||||||
public Oscilloscope(string address, int port, int timeout = 2000)
|
public OscilloscopeCtrl(string address, int port, int timeout = 2000)
|
||||||
{
|
{
|
||||||
if (timeout < 0)
|
if (timeout < 0)
|
||||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||||
@@ -98,6 +109,49 @@ class Oscilloscope
|
|||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一次性初始化/配置示波器
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">完整配置</param>
|
||||||
|
/// <returns>操作结果,全部成功返回true,否则返回异常信息</returns>
|
||||||
|
public async ValueTask<Result<bool>> Init(OscilloscopeConfig config)
|
||||||
|
{
|
||||||
|
// 1. 捕获使能
|
||||||
|
var ret = await SetCaptureEnable(config.CaptureEnabled);
|
||||||
|
if (!ret.IsSuccessful || !ret.Value)
|
||||||
|
return new(ret.Error ?? new Exception("Failed to set capture enable"));
|
||||||
|
|
||||||
|
// 2. 触发电平
|
||||||
|
ret = await SetTriggerLevel(config.TriggerLevel);
|
||||||
|
if (!ret.IsSuccessful || !ret.Value)
|
||||||
|
return new(ret.Error ?? new Exception("Failed to set trigger level"));
|
||||||
|
|
||||||
|
// 3. 触发边沿
|
||||||
|
ret = await SetTriggerEdge(config.TriggerRisingEdge);
|
||||||
|
if (!ret.IsSuccessful || !ret.Value)
|
||||||
|
return new(ret.Error ?? new Exception("Failed to set trigger edge"));
|
||||||
|
|
||||||
|
// 4. 水平偏移
|
||||||
|
ret = await SetHorizontalShift(config.HorizontalShift);
|
||||||
|
if (!ret.IsSuccessful || !ret.Value)
|
||||||
|
return new(ret.Error ?? new Exception("Failed to set horizontal shift"));
|
||||||
|
|
||||||
|
// 5. 抽样率
|
||||||
|
ret = await SetDecimationRate(config.DecimationRate);
|
||||||
|
if (!ret.IsSuccessful || !ret.Value)
|
||||||
|
return new(ret.Error ?? new Exception("Failed to set decimation rate"));
|
||||||
|
|
||||||
|
// 6. RAM刷新(如果需要)
|
||||||
|
// if (config.AutoRefreshRAM)
|
||||||
|
// {
|
||||||
|
// ret = await RefreshRAM();
|
||||||
|
// if (!ret.IsSuccessful || !ret.Value)
|
||||||
|
// return new(ret.Error ?? new Exception("Failed to refresh RAM"));
|
||||||
|
// }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 控制示波器的捕获开关
|
/// 控制示波器的捕获开关
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ export class VideoStreamClient {
|
|||||||
return Promise.resolve<boolean>(null as any);
|
return Promise.resolve<boolean>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
setVideoStreamEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<any> {
|
setVideoStreamEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<string> {
|
||||||
let url_ = this.baseUrl + "/api/VideoStream/SetVideoStreamEnable?";
|
let url_ = this.baseUrl + "/api/VideoStream/SetVideoStreamEnable?";
|
||||||
if (enable === null)
|
if (enable === null)
|
||||||
throw new Error("The parameter 'enable' cannot be null.");
|
throw new Error("The parameter 'enable' cannot be null.");
|
||||||
@@ -327,7 +327,7 @@ export class VideoStreamClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected processSetVideoStreamEnable(response: AxiosResponse): Promise<any> {
|
protected processSetVideoStreamEnable(response: AxiosResponse): Promise<string> {
|
||||||
const status = response.status;
|
const status = response.status;
|
||||||
let _headers: any = {};
|
let _headers: any = {};
|
||||||
if (response.headers && typeof response.headers === "object") {
|
if (response.headers && typeof response.headers === "object") {
|
||||||
@@ -343,7 +343,7 @@ export class VideoStreamClient {
|
|||||||
let resultData200 = _responseText;
|
let resultData200 = _responseText;
|
||||||
result200 = resultData200 !== undefined ? resultData200 : <any>null;
|
result200 = resultData200 !== undefined ? resultData200 : <any>null;
|
||||||
|
|
||||||
return Promise.resolve<any>(result200);
|
return Promise.resolve<string>(result200);
|
||||||
|
|
||||||
} else if (status === 500) {
|
} else if (status === 500) {
|
||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
@@ -357,7 +357,7 @@ export class VideoStreamClient {
|
|||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
}
|
}
|
||||||
return Promise.resolve<any>(null as any);
|
return Promise.resolve<string>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -5569,7 +5569,7 @@ export class OscilloscopeApiClient {
|
|||||||
* @param config 示波器配置
|
* @param config 示波器配置
|
||||||
* @return 操作结果
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
initialize(config: OscilloscopeFullConfig, cancelToken?: CancelToken): Promise<boolean> {
|
initialize(config: OscilloscopeConfig, cancelToken?: CancelToken): Promise<boolean> {
|
||||||
let url_ = this.baseUrl + "/api/OscilloscopeApi/Initialize";
|
let url_ = this.baseUrl + "/api/OscilloscopeApi/Initialize";
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
@@ -9090,22 +9090,14 @@ export interface INetworkInterfaceDto {
|
|||||||
macAddress: string;
|
macAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 示波器完整配置 */
|
export class OscilloscopeConfig implements IOscilloscopeConfig {
|
||||||
export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
|
|
||||||
/** 是否启动捕获 */
|
|
||||||
captureEnabled!: boolean;
|
captureEnabled!: boolean;
|
||||||
/** 触发电平(0-255) */
|
|
||||||
triggerLevel!: number;
|
triggerLevel!: number;
|
||||||
/** 触发边沿(true为上升沿,false为下降沿) */
|
|
||||||
triggerRisingEdge!: boolean;
|
triggerRisingEdge!: boolean;
|
||||||
/** 水平偏移量(0-1023) */
|
|
||||||
horizontalShift!: number;
|
horizontalShift!: number;
|
||||||
/** 抽样率(0-1023) */
|
|
||||||
decimationRate!: number;
|
decimationRate!: number;
|
||||||
/** 是否自动刷新RAM */
|
|
||||||
autoRefreshRAM!: boolean;
|
|
||||||
|
|
||||||
constructor(data?: IOscilloscopeFullConfig) {
|
constructor(data?: IOscilloscopeConfig) {
|
||||||
if (data) {
|
if (data) {
|
||||||
for (var property in data) {
|
for (var property in data) {
|
||||||
if (data.hasOwnProperty(property))
|
if (data.hasOwnProperty(property))
|
||||||
@@ -9121,13 +9113,12 @@ export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
|
|||||||
this.triggerRisingEdge = _data["triggerRisingEdge"];
|
this.triggerRisingEdge = _data["triggerRisingEdge"];
|
||||||
this.horizontalShift = _data["horizontalShift"];
|
this.horizontalShift = _data["horizontalShift"];
|
||||||
this.decimationRate = _data["decimationRate"];
|
this.decimationRate = _data["decimationRate"];
|
||||||
this.autoRefreshRAM = _data["autoRefreshRAM"];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJS(data: any): OscilloscopeFullConfig {
|
static fromJS(data: any): OscilloscopeConfig {
|
||||||
data = typeof data === 'object' ? data : {};
|
data = typeof data === 'object' ? data : {};
|
||||||
let result = new OscilloscopeFullConfig();
|
let result = new OscilloscopeConfig();
|
||||||
result.init(data);
|
result.init(data);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -9139,38 +9130,23 @@ export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
|
|||||||
data["triggerRisingEdge"] = this.triggerRisingEdge;
|
data["triggerRisingEdge"] = this.triggerRisingEdge;
|
||||||
data["horizontalShift"] = this.horizontalShift;
|
data["horizontalShift"] = this.horizontalShift;
|
||||||
data["decimationRate"] = this.decimationRate;
|
data["decimationRate"] = this.decimationRate;
|
||||||
data["autoRefreshRAM"] = this.autoRefreshRAM;
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 示波器完整配置 */
|
export interface IOscilloscopeConfig {
|
||||||
export interface IOscilloscopeFullConfig {
|
|
||||||
/** 是否启动捕获 */
|
|
||||||
captureEnabled: boolean;
|
captureEnabled: boolean;
|
||||||
/** 触发电平(0-255) */
|
|
||||||
triggerLevel: number;
|
triggerLevel: number;
|
||||||
/** 触发边沿(true为上升沿,false为下降沿) */
|
|
||||||
triggerRisingEdge: boolean;
|
triggerRisingEdge: boolean;
|
||||||
/** 水平偏移量(0-1023) */
|
|
||||||
horizontalShift: number;
|
horizontalShift: number;
|
||||||
/** 抽样率(0-1023) */
|
|
||||||
decimationRate: number;
|
decimationRate: number;
|
||||||
/** 是否自动刷新RAM */
|
|
||||||
autoRefreshRAM: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 示波器状态和数据 */
|
|
||||||
export class OscilloscopeDataResponse implements IOscilloscopeDataResponse {
|
export class OscilloscopeDataResponse implements IOscilloscopeDataResponse {
|
||||||
/** AD采样频率 */
|
|
||||||
adFrequency!: number;
|
adFrequency!: number;
|
||||||
/** AD采样幅度 */
|
|
||||||
adVpp!: number;
|
adVpp!: number;
|
||||||
/** AD采样最大值 */
|
|
||||||
adMax!: number;
|
adMax!: number;
|
||||||
/** AD采样最小值 */
|
|
||||||
adMin!: number;
|
adMin!: number;
|
||||||
/** 波形数据(Base64编码) */
|
|
||||||
waveformData!: string;
|
waveformData!: string;
|
||||||
|
|
||||||
constructor(data?: IOscilloscopeDataResponse) {
|
constructor(data?: IOscilloscopeDataResponse) {
|
||||||
@@ -9210,17 +9186,11 @@ export class OscilloscopeDataResponse implements IOscilloscopeDataResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 示波器状态和数据 */
|
|
||||||
export interface IOscilloscopeDataResponse {
|
export interface IOscilloscopeDataResponse {
|
||||||
/** AD采样频率 */
|
|
||||||
adFrequency: number;
|
adFrequency: number;
|
||||||
/** AD采样幅度 */
|
|
||||||
adVpp: number;
|
adVpp: number;
|
||||||
/** AD采样最大值 */
|
|
||||||
adMax: number;
|
adMax: number;
|
||||||
/** AD采样最小值 */
|
|
||||||
adMin: number;
|
adMin: number;
|
||||||
/** 波形数据(Base64编码) */
|
|
||||||
waveformData: string;
|
waveformData: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,35 @@
|
|||||||
import { autoResetRef, createInjectionState } from "@vueuse/core";
|
|
||||||
import { shallowRef, reactive, ref, computed } from "vue";
|
|
||||||
import { Mutex } from "async-mutex";
|
|
||||||
import {
|
import {
|
||||||
OscilloscopeFullConfig,
|
autoResetRef,
|
||||||
OscilloscopeDataResponse,
|
createInjectionState,
|
||||||
OscilloscopeApiClient,
|
watchDebounced,
|
||||||
} from "@/APIClient";
|
} from "@vueuse/core";
|
||||||
|
import {
|
||||||
|
shallowRef,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
computed,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
watchEffect,
|
||||||
|
} from "vue";
|
||||||
|
import { Mutex } from "async-mutex";
|
||||||
|
import { OscilloscopeApiClient } from "@/APIClient";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
|
import type { HubConnection } from "@microsoft/signalr";
|
||||||
|
import type {
|
||||||
|
IOscilloscopeHub,
|
||||||
|
IOscilloscopeReceiver,
|
||||||
|
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
|
||||||
|
import {
|
||||||
|
getHubProxyFactory,
|
||||||
|
getReceiverRegister,
|
||||||
|
} from "@/utils/signalR/TypedSignalR.Client";
|
||||||
|
import type {
|
||||||
|
OscilloscopeDataResponse,
|
||||||
|
OscilloscopeFullConfig,
|
||||||
|
} from "@/utils/signalR/server.Hubs";
|
||||||
|
|
||||||
export type OscilloscopeDataType = {
|
export type OscilloscopeDataType = {
|
||||||
x: number[];
|
x: number[];
|
||||||
@@ -22,41 +43,103 @@ export type OscilloscopeDataType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
|
const DEFAULT_CONFIG: OscilloscopeFullConfig = {
|
||||||
captureEnabled: false,
|
captureEnabled: false,
|
||||||
triggerLevel: 128,
|
triggerLevel: 128,
|
||||||
triggerRisingEdge: true,
|
triggerRisingEdge: true,
|
||||||
horizontalShift: 0,
|
horizontalShift: 0,
|
||||||
decimationRate: 50,
|
decimationRate: 50,
|
||||||
autoRefreshRAM: false,
|
captureFrequency: 100,
|
||||||
});
|
};
|
||||||
|
|
||||||
// 采样频率常量(后端返回)
|
// 采样频率常量(后端返回)
|
||||||
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
||||||
() => {
|
() => {
|
||||||
const oscData = shallowRef<OscilloscopeDataType>();
|
// Global Store
|
||||||
const alert = useRequiredInjection(useAlertStore);
|
const alert = useRequiredInjection(useAlertStore);
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const oscData = shallowRef<OscilloscopeDataType>();
|
||||||
|
const clearOscilloscopeData = () => {
|
||||||
|
oscData.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// SignalR Hub
|
||||||
|
const oscilloscopeHub = shallowRef<{
|
||||||
|
connection: HubConnection;
|
||||||
|
proxy: IOscilloscopeHub;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const oscilloscopeReceiver: IOscilloscopeReceiver = {
|
||||||
|
onDataReceived: async (data) => {
|
||||||
|
analyzeOscilloscopeData(data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initHub();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearHub();
|
||||||
|
});
|
||||||
|
|
||||||
|
function initHub() {
|
||||||
|
if (oscilloscopeHub.value) return;
|
||||||
|
|
||||||
|
const connection = AuthManager.createHubConnection("OscilloscopeHub");
|
||||||
|
|
||||||
|
const proxy =
|
||||||
|
getHubProxyFactory("IOscilloscopeHub").createHubProxy(connection);
|
||||||
|
|
||||||
|
getReceiverRegister("IOscilloscopeReceiver").register(
|
||||||
|
connection,
|
||||||
|
oscilloscopeReceiver,
|
||||||
|
);
|
||||||
|
connection.start();
|
||||||
|
oscilloscopeHub.value = { connection, proxy };
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHub() {
|
||||||
|
if (!oscilloscopeHub.value) return;
|
||||||
|
oscilloscopeHub.value.connection.stop();
|
||||||
|
oscilloscopeHub.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reinitializeHub() {
|
||||||
|
clearHub();
|
||||||
|
initHub();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHubProxy() {
|
||||||
|
if (!oscilloscopeHub.value) throw new Error("Hub not initialized");
|
||||||
|
return oscilloscopeHub.value.proxy;
|
||||||
|
}
|
||||||
|
|
||||||
// 互斥锁
|
// 互斥锁
|
||||||
const operationMutex = new Mutex();
|
const operationMutex = new Mutex();
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const isApplying = ref(false);
|
const isApplying = ref(false);
|
||||||
const isCapturing = ref(false);
|
const isCapturing = ref(false);
|
||||||
|
const isAutoApplying = ref(false);
|
||||||
|
|
||||||
// 配置
|
// 配置
|
||||||
const config = reactive<OscilloscopeFullConfig>(
|
const config = reactive<OscilloscopeFullConfig>({ ...DEFAULT_CONFIG });
|
||||||
new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }),
|
watchDebounced(
|
||||||
);
|
config,
|
||||||
|
() => {
|
||||||
|
if (!isAutoApplying.value) return;
|
||||||
|
|
||||||
// 采样点数(由后端数据决定)
|
if (
|
||||||
const sampleCount = ref(0);
|
!isApplying.value ||
|
||||||
|
!isCapturing.value ||
|
||||||
// 采样周期(ns),由adFrequency计算
|
!operationMutex.isLocked()
|
||||||
const samplePeriodNs = computed(() =>
|
) {
|
||||||
oscData.value?.adFrequency
|
applyConfiguration();
|
||||||
? 1_000_000_000 / oscData.value.adFrequency
|
}
|
||||||
: 200,
|
},
|
||||||
|
{ debounce: 200, maxWait: 1000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// 应用配置
|
// 应用配置
|
||||||
@@ -68,14 +151,18 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||||||
const release = await operationMutex.acquire();
|
const release = await operationMutex.acquire();
|
||||||
isApplying.value = true;
|
isApplying.value = true;
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
const proxy = getHubProxy();
|
||||||
const success = await client.initialize({ ...config });
|
|
||||||
|
const success = await proxy.initialize(config);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
alert.success("示波器配置已应用", 2000);
|
alert.success("示波器配置已应用", 2000);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("应用失败");
|
throw new Error("应用失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === "Hub not initialized")
|
||||||
|
reinitializeHub();
|
||||||
alert.error("应用配置失败", 3000);
|
alert.error("应用配置失败", 3000);
|
||||||
} finally {
|
} finally {
|
||||||
isApplying.value = false;
|
isApplying.value = false;
|
||||||
@@ -89,16 +176,17 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||||||
alert.info("配置已重置", 2000);
|
alert.info("配置已重置", 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearOscilloscopeData = () => {
|
// 采样点数(由后端数据决定)
|
||||||
oscData.value = undefined;
|
const sampleCount = ref(0);
|
||||||
};
|
|
||||||
|
|
||||||
// 获取数据
|
// 采样周期(ns),由adFrequency计算
|
||||||
const getOscilloscopeData = async () => {
|
const samplePeriodNs = computed(() =>
|
||||||
try {
|
oscData.value?.adFrequency
|
||||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
? 1_000_000_000 / oscData.value.adFrequency
|
||||||
const resp: OscilloscopeDataResponse = await client.getData();
|
: 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
const analyzeOscilloscopeData = (resp: OscilloscopeDataResponse) => {
|
||||||
// 解析波形数据
|
// 解析波形数据
|
||||||
const binaryString = atob(resp.waveformData);
|
const binaryString = atob(resp.waveformData);
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
@@ -119,38 +207,24 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||||||
y,
|
y,
|
||||||
xUnit: "us",
|
xUnit: "us",
|
||||||
yUnit: "V",
|
yUnit: "V",
|
||||||
adFrequency: resp.adFrequency,
|
adFrequency: resp.aDFrequency,
|
||||||
adVpp: resp.adVpp,
|
adVpp: resp.aDVpp,
|
||||||
adMax: resp.adMax,
|
adMax: resp.aDMax,
|
||||||
adMin: resp.adMin,
|
adMin: resp.aDMin,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const getOscilloscopeData = async () => {
|
||||||
|
try {
|
||||||
|
const proxy = getHubProxy();
|
||||||
|
const resp = await proxy.getData();
|
||||||
|
analyzeOscilloscopeData(resp);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert.error("获取示波器数据失败", 3000);
|
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 () => {
|
const startCapture = async () => {
|
||||||
if (operationMutex.isLocked()) {
|
if (operationMutex.isLocked()) {
|
||||||
@@ -160,17 +234,13 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||||||
isCapturing.value = true;
|
isCapturing.value = true;
|
||||||
const release = await operationMutex.acquire();
|
const release = await operationMutex.acquire();
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
const proxy = getHubProxy();
|
||||||
const started = await client.startCapture();
|
const started = await proxy.startCapture();
|
||||||
if (!started) throw new Error("无法启动捕获");
|
if (!started) throw new Error("无法启动捕获");
|
||||||
alert.info("开始捕获...", 2000);
|
alert.info("开始捕获...", 2000);
|
||||||
|
|
||||||
// 启动定时刷新
|
|
||||||
startAutoRefresh();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert.error("捕获失败", 3000);
|
alert.error("捕获失败", 3000);
|
||||||
isCapturing.value = false;
|
isCapturing.value = false;
|
||||||
stopAutoRefresh();
|
|
||||||
} finally {
|
} finally {
|
||||||
release();
|
release();
|
||||||
}
|
}
|
||||||
@@ -183,11 +253,10 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isCapturing.value = false;
|
isCapturing.value = false;
|
||||||
stopAutoRefresh();
|
|
||||||
const release = await operationMutex.acquire();
|
const release = await operationMutex.acquire();
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
const proxy = getHubProxy();
|
||||||
const stopped = await client.stopCapture();
|
const stopped = await proxy.stopCapture();
|
||||||
if (!stopped) throw new Error("无法停止捕获");
|
if (!stopped) throw new Error("无法停止捕获");
|
||||||
alert.info("捕获已停止", 2000);
|
alert.info("捕获已停止", 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -197,6 +266,14 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleCapture = async () => {
|
||||||
|
if (isCapturing.value) {
|
||||||
|
await stopCapture();
|
||||||
|
} else {
|
||||||
|
await startCapture();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 更新触发参数
|
// 更新触发参数
|
||||||
const updateTrigger = async (level: number, risingEdge: boolean) => {
|
const updateTrigger = async (level: number, risingEdge: boolean) => {
|
||||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||||
@@ -279,9 +356,9 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||||||
config,
|
config,
|
||||||
isApplying,
|
isApplying,
|
||||||
isCapturing,
|
isCapturing,
|
||||||
|
isAutoApplying,
|
||||||
sampleCount,
|
sampleCount,
|
||||||
samplePeriodNs,
|
samplePeriodNs,
|
||||||
refreshIntervalMs,
|
|
||||||
|
|
||||||
applyConfiguration,
|
applyConfiguration,
|
||||||
resetConfiguration,
|
resetConfiguration,
|
||||||
@@ -289,6 +366,7 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||||||
getOscilloscopeData,
|
getOscilloscopeData,
|
||||||
startCapture,
|
startCapture,
|
||||||
stopCapture,
|
stopCapture,
|
||||||
|
toggleCapture,
|
||||||
updateTrigger,
|
updateTrigger,
|
||||||
updateSampling,
|
updateSampling,
|
||||||
refreshRAM,
|
refreshRAM,
|
||||||
|
|||||||
@@ -1,36 +1,93 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-100 flex flex-col">
|
<div
|
||||||
<!-- 原有内容 -->
|
class="waveform-container w-full h-full relative overflow-hidden rounded-lg"
|
||||||
<v-chart v-if="hasData" class="w-full h-full" :option="option" autoresize />
|
>
|
||||||
<div v-else class="w-full h-full flex flex-col gap-4 items-center justify-center text-gray-500">
|
<!-- 波形图表 -->
|
||||||
<span> 暂无数据 </span>
|
<v-chart
|
||||||
<!-- 采集控制按钮 -->
|
v-if="hasData"
|
||||||
<div class="flex justify-center items-center mb-2">
|
class="w-full h-full transition-all duration-500 ease-in-out"
|
||||||
|
:option="option"
|
||||||
|
autoresize
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 无数据状态 -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-full h-full flex flex-col items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-slate-800"
|
||||||
|
>
|
||||||
|
<!-- 动画图标 -->
|
||||||
|
<div class="relative mb-6">
|
||||||
|
<div
|
||||||
|
class="w-24 h-24 rounded-full border-4 border-blue-200 dark:border-blue-800 animate-pulse"
|
||||||
|
></div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<Activity class="w-12 h-12 text-blue-500 animate-bounce" />
|
||||||
|
</div>
|
||||||
|
<!-- 扫描线效果 -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 rounded-full border-2 border-transparent border-t-blue-500 animate-spin"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态文本 -->
|
||||||
|
<div class="text-center space-y-2 mb-8">
|
||||||
|
<h3 class="text-xl font-semibold text-slate-700 dark:text-slate-300">
|
||||||
|
等待信号输入
|
||||||
|
</h3>
|
||||||
|
<p class="text-slate-500 dark:text-slate-400">
|
||||||
|
请启动数据采集以显示波形
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速启动按钮 -->
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
<button
|
<button
|
||||||
class="group relative px-8 py-3 bg-gradient-to-r text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 active:scale-95"
|
class="group relative px-8 py-4 bg-gradient-to-r text-white font-semibold rounded-xl shadow-xl hover:shadow-2xl transform hover:scale-110 transition-all duration-300 ease-out focus:outline-none focus:ring-4 active:scale-95 overflow-hidden"
|
||||||
:class="{
|
:class="{
|
||||||
'from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 focus:ring-blue-300':
|
'from-emerald-500 via-blue-500 to-purple-600 hover:from-emerald-600 hover:via-blue-600 hover:to-purple-700 focus:ring-blue-300':
|
||||||
!oscManager.isCapturing.value,
|
!oscManager.isCapturing.value,
|
||||||
'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
|
'from-red-500 via-pink-500 to-red-600 hover:from-red-600 hover:via-pink-600 hover:to-red-700 focus:ring-red-300':
|
||||||
oscManager.isCapturing.value,
|
oscManager.isCapturing.value,
|
||||||
}" @click="
|
}"
|
||||||
|
@click="
|
||||||
oscManager.isCapturing.value
|
oscManager.isCapturing.value
|
||||||
? oscManager.stopCapture()
|
? oscManager.stopCapture()
|
||||||
: oscManager.startCapture()
|
: oscManager.startCapture()
|
||||||
">
|
"
|
||||||
<span class="flex items-center gap-2">
|
>
|
||||||
|
<!-- 背景动画效果 -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-300"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 按钮内容 -->
|
||||||
|
<span class="relative flex items-center gap-3">
|
||||||
<template v-if="oscManager.isCapturing.value">
|
<template v-if="oscManager.isCapturing.value">
|
||||||
<Square class="w-5 h-5" />
|
<Square class="w-6 h-6 animate-pulse" />
|
||||||
停止采集
|
停止采集
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Play class="w-5 h-5" />
|
<Play class="w-6 h-6 group-hover:animate-pulse" />
|
||||||
开始采集
|
开始采集
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- 光晕效果 -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 rounded-xl bg-gradient-to-r from-transparent via-white to-transparent opacity-0 group-hover:opacity-30 transform -skew-x-12 translate-x-full group-hover:translate-x-[-200%] transition-transform duration-700"
|
||||||
|
></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据采集状态指示器 -->
|
||||||
|
<div
|
||||||
|
v-if="hasData && oscManager.isCapturing.value"
|
||||||
|
class="absolute top-4 right-4 flex items-center gap-2 bg-red-500/90 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<div class="w-2 h-2 bg-white rounded-full animate-pulse"></div>
|
||||||
|
采集中
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -61,7 +118,7 @@ import type {
|
|||||||
GridComponentOption,
|
GridComponentOption,
|
||||||
} from "echarts/components";
|
} from "echarts/components";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
import { Play, Square } from "lucide-vue-next";
|
import { Play, Square, Activity } from "lucide-vue-next";
|
||||||
|
|
||||||
use([
|
use([
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
@@ -113,12 +170,23 @@ const option = computed((): EChartsOption => {
|
|||||||
? (oscData.value.y as number[][])
|
? (oscData.value.y as number[][])
|
||||||
: [oscData.value.y as number[]];
|
: [oscData.value.y as number[]];
|
||||||
|
|
||||||
|
// 预定义的通道颜色
|
||||||
|
const channelColors = [
|
||||||
|
"#3B82F6", // blue-500
|
||||||
|
"#EF4444", // red-500
|
||||||
|
"#10B981", // emerald-500
|
||||||
|
"#F59E0B", // amber-500
|
||||||
|
"#8B5CF6", // violet-500
|
||||||
|
"#06B6D4", // cyan-500
|
||||||
|
];
|
||||||
|
|
||||||
forEach(yChannels, (yData, index) => {
|
forEach(yChannels, (yData, index) => {
|
||||||
if (!oscData.value || !yData) return;
|
if (!oscData.value || !yData) return;
|
||||||
const seriesData = oscData.value.x.map((xValue, i) => [
|
const seriesData = oscData.value.x.map((xValue, i) => [
|
||||||
xValue,
|
xValue,
|
||||||
yData && yData[i] !== undefined ? yData[i] : 0,
|
yData && yData[i] !== undefined ? yData[i] : 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
series.push({
|
series.push({
|
||||||
type: "line",
|
type: "line",
|
||||||
name: `通道 ${index + 1}`,
|
name: `通道 ${index + 1}`,
|
||||||
@@ -126,41 +194,82 @@ const option = computed((): EChartsOption => {
|
|||||||
smooth: false,
|
smooth: false,
|
||||||
symbol: "none",
|
symbol: "none",
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 2,
|
width: 2.5,
|
||||||
|
color: channelColors[index % channelColors.length],
|
||||||
|
shadowColor: channelColors[index % channelColors.length],
|
||||||
|
shadowBlur: isCapturing ? 0 : 4,
|
||||||
|
shadowOffsetY: 2,
|
||||||
},
|
},
|
||||||
// 关闭系列动画
|
itemStyle: {
|
||||||
|
color: channelColors[index % channelColors.length],
|
||||||
|
},
|
||||||
|
// 动画配置
|
||||||
animation: !isCapturing,
|
animation: !isCapturing,
|
||||||
animationDuration: isCapturing ? 0 : 1000,
|
animationDuration: isCapturing ? 0 : 1200,
|
||||||
animationEasing: isCapturing ? "linear" : "cubicOut",
|
animationEasing: isCapturing ? "linear" : "cubicOut",
|
||||||
|
animationDelay: index * 100, // 错开动画时间
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
backgroundColor: "transparent",
|
||||||
grid: {
|
grid: {
|
||||||
left: "10%",
|
left: "8%",
|
||||||
right: "10%",
|
right: "5%",
|
||||||
top: "15%",
|
top: "12%",
|
||||||
bottom: "25%",
|
bottom: "20%",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#E2E8F0",
|
||||||
|
backgroundColor: "rgba(248, 250, 252, 0.8)",
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
||||||
|
borderColor: "#E2E8F0",
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: "#334155",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
formatter: (params: any) => {
|
formatter: (params: any) => {
|
||||||
if (!oscData.value) return "";
|
if (!oscData.value) return "";
|
||||||
let result = `时间: ${params[0].data[0].toFixed(2)} ${oscData.value.xUnit}<br/>`;
|
let result = `<div style="font-weight: 600; margin-bottom: 4px;">时间: ${params[0].data[0].toFixed(2)} ${oscData.value.xUnit}</div>`;
|
||||||
params.forEach((param: any) => {
|
params.forEach((param: any) => {
|
||||||
result += `${param.seriesName}: ${param.data[1].toFixed(3)} ${oscData.value?.yUnit ?? ""}<br/>`;
|
result += `<div style="color: ${param.color};">● ${param.seriesName}: ${param.data[1].toFixed(3)} ${oscData.value?.yUnit ?? ""}</div>`;
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
top: "5%",
|
top: "2%",
|
||||||
|
left: "center",
|
||||||
|
textStyle: {
|
||||||
|
color: "#64748B",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
itemGap: 20,
|
||||||
data: series.map((s) => s.name) as string[],
|
data: series.map((s) => s.name) as string[],
|
||||||
},
|
},
|
||||||
toolbox: {
|
toolbox: {
|
||||||
|
right: "2%",
|
||||||
|
top: "2%",
|
||||||
feature: {
|
feature: {
|
||||||
restore: {},
|
restore: {
|
||||||
saveAsImage: {},
|
title: "重置缩放",
|
||||||
|
},
|
||||||
|
saveAsImage: {
|
||||||
|
title: "保存图片",
|
||||||
|
name: `oscilloscope_${new Date().toISOString().slice(0, 19)}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iconStyle: {
|
||||||
|
borderColor: "#64748B",
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
iconStyle: {
|
||||||
|
borderColor: "#3B82F6",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dataZoom: [
|
dataZoom: [
|
||||||
@@ -168,47 +277,275 @@ const option = computed((): EChartsOption => {
|
|||||||
type: "inside",
|
type: "inside",
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 100,
|
end: 100,
|
||||||
|
filterMode: "weakFilter",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 100,
|
end: 100,
|
||||||
|
height: 25,
|
||||||
|
bottom: "8%",
|
||||||
|
borderColor: "#E2E8F0",
|
||||||
|
fillerColor: "rgba(59, 130, 246, 0.1)",
|
||||||
|
handleStyle: {
|
||||||
|
color: "#3B82F6",
|
||||||
|
borderColor: "#1E40AF",
|
||||||
|
},
|
||||||
|
textStyle: {
|
||||||
|
color: "#64748B",
|
||||||
|
fontSize: 11,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
name: oscData.value ? `时间 (${oscData.value.xUnit})` : "时间",
|
name: oscData.value ? `时间 (${oscData.value.xUnit})` : "时间",
|
||||||
nameLocation: "middle",
|
nameLocation: "middle",
|
||||||
nameGap: 30,
|
nameGap: 35,
|
||||||
|
nameTextStyle: {
|
||||||
|
color: "#64748B",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
show: true,
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#CBD5E1",
|
||||||
|
width: 1.5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: true,
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#E2E8F0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: "#64748B",
|
||||||
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: false,
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#F1F5F9",
|
||||||
|
type: "dashed",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
name: oscData.value ? `电压 (${oscData.value.yUnit})` : "电压",
|
name: oscData.value ? `电压 (${oscData.value.yUnit})` : "电压",
|
||||||
nameLocation: "middle",
|
nameLocation: "middle",
|
||||||
nameGap: 40,
|
nameGap: 50,
|
||||||
|
nameTextStyle: {
|
||||||
|
color: "#64748B",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
show: true,
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#CBD5E1",
|
||||||
|
width: 1.5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: true,
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#E2E8F0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: "#64748B",
|
||||||
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: false,
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#F1F5F9",
|
||||||
|
type: "dashed",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 全局动画开关
|
// 全局动画开关
|
||||||
animation: !isCapturing,
|
animation: !isCapturing,
|
||||||
animationDuration: isCapturing ? 0 : 1000,
|
animationDuration: isCapturing ? 0 : 1200,
|
||||||
animationEasing: isCapturing ? "linear" : "cubicOut",
|
animationEasing: isCapturing ? "linear" : "cubicOut",
|
||||||
series: series,
|
series: series,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="postcss">
|
||||||
|
@import "@/assets/main.css";
|
||||||
|
/* 波形容器样式 */
|
||||||
|
.waveform-container {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(248, 250, 252, 0.8) 0%,
|
||||||
|
rgba(241, 245, 249, 0.8) 100%
|
||||||
|
);
|
||||||
|
border: 1px solid rgba(226, 232, 240, 0.5);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveform-container::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
transparent 48%,
|
||||||
|
rgba(59, 130, 246, 0.05) 50%,
|
||||||
|
transparent 52%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无数据状态的背景动画 */
|
||||||
|
.waveform-container:not(:has(canvas)) {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(248, 250, 252, 1) 0%,
|
||||||
|
rgba(239, 246, 255, 1) 25%,
|
||||||
|
rgba(219, 234, 254, 1) 50%,
|
||||||
|
rgba(239, 246, 255, 1) 75%,
|
||||||
|
rgba(248, 250, 252, 1) 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: gradient-shift 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient-shift {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色模式支持 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.waveform-container {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(15, 23, 42, 0.8) 0%,
|
||||||
|
rgba(30, 41, 59, 0.8) 100%
|
||||||
|
);
|
||||||
|
border-color: rgba(71, 85, 105, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveform-container:not(:has(canvas)) {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(15, 23, 42, 1) 0%,
|
||||||
|
rgba(30, 41, 59, 1) 25%,
|
||||||
|
rgba(51, 65, 85, 1) 50%,
|
||||||
|
rgba(30, 41, 59, 1) 75%,
|
||||||
|
rgba(15, 23, 42, 1) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮光晕效果增强 */
|
||||||
|
button {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition:
|
||||||
|
width 0.6s,
|
||||||
|
height 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active::after {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扫描线动画优化 */
|
||||||
|
@keyframes scan-line {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg) scale(1.1);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: scan-line 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态指示器增强 */
|
||||||
|
.absolute.top-4.right-4 {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.25);
|
||||||
|
animation: float 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表容器增强 */
|
||||||
|
.w-full.h-full.transition-all {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.waveform-container {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute.top-4.right-4 {
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平滑过渡效果 */
|
||||||
|
* {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 焦点样式 */
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid rgba(59, 130, 246, 0.5);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ export class AuthManager {
|
|||||||
|
|
||||||
// SignalR连接 - 简单明了
|
// SignalR连接 - 简单明了
|
||||||
static createHubConnection(
|
static createHubConnection(
|
||||||
hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub" | "RotaryEncoderHub",
|
hubPath:
|
||||||
|
| "ProgressHub"
|
||||||
|
| "JtagHub"
|
||||||
|
| "DigitalTubesHub"
|
||||||
|
| "RotaryEncoderHub"
|
||||||
|
| "OscilloscopeHub",
|
||||||
) {
|
) {
|
||||||
return new HubConnectionBuilder()
|
return new HubConnectionBuilder()
|
||||||
.withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, {
|
.withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
|
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
|
||||||
import type { IDigitalTubesHub, IJtagHub, IProgressHub, IRotaryEncoderHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver, IRotaryEncoderReceiver } from './server.Hubs';
|
import type { IDigitalTubesHub, IJtagHub, IOscilloscopeHub, IProgressHub, IRotaryEncoderHub, IDigitalTubesReceiver, IJtagReceiver, IOscilloscopeReceiver, IProgressReceiver, IRotaryEncoderReceiver } from './server.Hubs';
|
||||||
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
|
import type { DigitalTubeTaskStatus, OscilloscopeFullConfig, OscilloscopeDataResponse, ProgressInfo } from '../server.Hubs';
|
||||||
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
|
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
|
||||||
|
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ class ReceiverMethodSubscription implements Disposable {
|
|||||||
export type HubProxyFactoryProvider = {
|
export type HubProxyFactoryProvider = {
|
||||||
(hubType: "IDigitalTubesHub"): HubProxyFactory<IDigitalTubesHub>;
|
(hubType: "IDigitalTubesHub"): HubProxyFactory<IDigitalTubesHub>;
|
||||||
(hubType: "IJtagHub"): HubProxyFactory<IJtagHub>;
|
(hubType: "IJtagHub"): HubProxyFactory<IJtagHub>;
|
||||||
|
(hubType: "IOscilloscopeHub"): HubProxyFactory<IOscilloscopeHub>;
|
||||||
(hubType: "IProgressHub"): HubProxyFactory<IProgressHub>;
|
(hubType: "IProgressHub"): HubProxyFactory<IProgressHub>;
|
||||||
(hubType: "IRotaryEncoderHub"): HubProxyFactory<IRotaryEncoderHub>;
|
(hubType: "IRotaryEncoderHub"): HubProxyFactory<IRotaryEncoderHub>;
|
||||||
}
|
}
|
||||||
@@ -57,6 +58,9 @@ export const getHubProxyFactory = ((hubType: string) => {
|
|||||||
if(hubType === "IJtagHub") {
|
if(hubType === "IJtagHub") {
|
||||||
return IJtagHub_HubProxyFactory.Instance;
|
return IJtagHub_HubProxyFactory.Instance;
|
||||||
}
|
}
|
||||||
|
if(hubType === "IOscilloscopeHub") {
|
||||||
|
return IOscilloscopeHub_HubProxyFactory.Instance;
|
||||||
|
}
|
||||||
if(hubType === "IProgressHub") {
|
if(hubType === "IProgressHub") {
|
||||||
return IProgressHub_HubProxyFactory.Instance;
|
return IProgressHub_HubProxyFactory.Instance;
|
||||||
}
|
}
|
||||||
@@ -68,6 +72,7 @@ export const getHubProxyFactory = ((hubType: string) => {
|
|||||||
export type ReceiverRegisterProvider = {
|
export type ReceiverRegisterProvider = {
|
||||||
(receiverType: "IDigitalTubesReceiver"): ReceiverRegister<IDigitalTubesReceiver>;
|
(receiverType: "IDigitalTubesReceiver"): ReceiverRegister<IDigitalTubesReceiver>;
|
||||||
(receiverType: "IJtagReceiver"): ReceiverRegister<IJtagReceiver>;
|
(receiverType: "IJtagReceiver"): ReceiverRegister<IJtagReceiver>;
|
||||||
|
(receiverType: "IOscilloscopeReceiver"): ReceiverRegister<IOscilloscopeReceiver>;
|
||||||
(receiverType: "IProgressReceiver"): ReceiverRegister<IProgressReceiver>;
|
(receiverType: "IProgressReceiver"): ReceiverRegister<IProgressReceiver>;
|
||||||
(receiverType: "IRotaryEncoderReceiver"): ReceiverRegister<IRotaryEncoderReceiver>;
|
(receiverType: "IRotaryEncoderReceiver"): ReceiverRegister<IRotaryEncoderReceiver>;
|
||||||
}
|
}
|
||||||
@@ -79,6 +84,9 @@ export const getReceiverRegister = ((receiverType: string) => {
|
|||||||
if(receiverType === "IJtagReceiver") {
|
if(receiverType === "IJtagReceiver") {
|
||||||
return IJtagReceiver_Binder.Instance;
|
return IJtagReceiver_Binder.Instance;
|
||||||
}
|
}
|
||||||
|
if(receiverType === "IOscilloscopeReceiver") {
|
||||||
|
return IOscilloscopeReceiver_Binder.Instance;
|
||||||
|
}
|
||||||
if(receiverType === "IProgressReceiver") {
|
if(receiverType === "IProgressReceiver") {
|
||||||
return IProgressReceiver_Binder.Instance;
|
return IProgressReceiver_Binder.Instance;
|
||||||
}
|
}
|
||||||
@@ -151,6 +159,55 @@ class IJtagHub_HubProxy implements IJtagHub {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IOscilloscopeHub_HubProxyFactory implements HubProxyFactory<IOscilloscopeHub> {
|
||||||
|
public static Instance = new IOscilloscopeHub_HubProxyFactory();
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly createHubProxy = (connection: HubConnection): IOscilloscopeHub => {
|
||||||
|
return new IOscilloscopeHub_HubProxy(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IOscilloscopeHub_HubProxy implements IOscilloscopeHub {
|
||||||
|
|
||||||
|
public constructor(private connection: HubConnection) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly initialize = async (config: OscilloscopeFullConfig): Promise<boolean> => {
|
||||||
|
return await this.connection.invoke("Initialize", config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly startCapture = async (): Promise<boolean> => {
|
||||||
|
return await this.connection.invoke("StartCapture");
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly stopCapture = async (): Promise<boolean> => {
|
||||||
|
return await this.connection.invoke("StopCapture");
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly getData = async (): Promise<OscilloscopeDataResponse> => {
|
||||||
|
return await this.connection.invoke("GetData");
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly setTrigger = async (level: number): Promise<boolean> => {
|
||||||
|
return await this.connection.invoke("SetTrigger", level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly setRisingEdge = async (risingEdge: boolean): Promise<boolean> => {
|
||||||
|
return await this.connection.invoke("SetRisingEdge", risingEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly setSampling = async (decimationRate: number): Promise<boolean> => {
|
||||||
|
return await this.connection.invoke("SetSampling", decimationRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly setFrequency = async (frequency: number): Promise<boolean> => {
|
||||||
|
return await this.connection.invoke("SetFrequency", frequency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class IProgressHub_HubProxyFactory implements HubProxyFactory<IProgressHub> {
|
class IProgressHub_HubProxyFactory implements HubProxyFactory<IProgressHub> {
|
||||||
public static Instance = new IProgressHub_HubProxyFactory();
|
public static Instance = new IProgressHub_HubProxyFactory();
|
||||||
|
|
||||||
@@ -258,6 +315,27 @@ class IJtagReceiver_Binder implements ReceiverRegister<IJtagReceiver> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IOscilloscopeReceiver_Binder implements ReceiverRegister<IOscilloscopeReceiver> {
|
||||||
|
|
||||||
|
public static Instance = new IOscilloscopeReceiver_Binder();
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly register = (connection: HubConnection, receiver: IOscilloscopeReceiver): Disposable => {
|
||||||
|
|
||||||
|
const __onDataReceived = (...args: [OscilloscopeDataResponse]) => receiver.onDataReceived(...args);
|
||||||
|
|
||||||
|
connection.on("OnDataReceived", __onDataReceived);
|
||||||
|
|
||||||
|
const methodList: ReceiverMethod[] = [
|
||||||
|
{ methodName: "OnDataReceived", method: __onDataReceived }
|
||||||
|
]
|
||||||
|
|
||||||
|
return new ReceiverMethodSubscription(connection, methodList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class IProgressReceiver_Binder implements ReceiverRegister<IProgressReceiver> {
|
class IProgressReceiver_Binder implements ReceiverRegister<IProgressReceiver> {
|
||||||
|
|
||||||
public static Instance = new IProgressReceiver_Binder();
|
public static Instance = new IProgressReceiver_Binder();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import type { IStreamResult, Subject } from '@microsoft/signalr';
|
import type { IStreamResult, Subject } from '@microsoft/signalr';
|
||||||
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
|
import type { DigitalTubeTaskStatus, OscilloscopeFullConfig, OscilloscopeDataResponse, ProgressInfo } from '../server.Hubs';
|
||||||
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
|
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
|
||||||
|
|
||||||
export type IDigitalTubesHub = {
|
export type IDigitalTubesHub = {
|
||||||
@@ -43,6 +43,46 @@ export type IJtagHub = {
|
|||||||
stopBoundaryScan(): Promise<boolean>;
|
stopBoundaryScan(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IOscilloscopeHub = {
|
||||||
|
/**
|
||||||
|
* @param config Transpiled from server.Hubs.OscilloscopeFullConfig
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
||||||
|
*/
|
||||||
|
initialize(config: OscilloscopeFullConfig): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
||||||
|
*/
|
||||||
|
startCapture(): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
||||||
|
*/
|
||||||
|
stopCapture(): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<server.Hubs.OscilloscopeDataResponse?>
|
||||||
|
*/
|
||||||
|
getData(): Promise<OscilloscopeDataResponse>;
|
||||||
|
/**
|
||||||
|
* @param level Transpiled from byte
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
||||||
|
*/
|
||||||
|
setTrigger(level: number): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @param risingEdge Transpiled from bool
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
||||||
|
*/
|
||||||
|
setRisingEdge(risingEdge: boolean): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @param decimationRate Transpiled from ushort
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
||||||
|
*/
|
||||||
|
setSampling(decimationRate: number): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @param frequency Transpiled from int
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
||||||
|
*/
|
||||||
|
setFrequency(frequency: number): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
export type IProgressHub = {
|
export type IProgressHub = {
|
||||||
/**
|
/**
|
||||||
* @param taskId Transpiled from string
|
* @param taskId Transpiled from string
|
||||||
@@ -102,6 +142,14 @@ export type IJtagReceiver = {
|
|||||||
onReceiveBoundaryScanData(msg: Partial<Record<string, boolean>>): Promise<void>;
|
onReceiveBoundaryScanData(msg: Partial<Record<string, boolean>>): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IOscilloscopeReceiver = {
|
||||||
|
/**
|
||||||
|
* @param data Transpiled from server.Hubs.OscilloscopeDataResponse
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task
|
||||||
|
*/
|
||||||
|
onDataReceived(data: OscilloscopeDataResponse): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export type IProgressReceiver = {
|
export type IProgressReceiver = {
|
||||||
/**
|
/**
|
||||||
* @param message Transpiled from server.Hubs.ProgressInfo
|
* @param message Transpiled from server.Hubs.ProgressInfo
|
||||||
|
|||||||
@@ -10,6 +10,36 @@ export type DigitalTubeTaskStatus = {
|
|||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Transpiled from server.Hubs.OscilloscopeDataResponse */
|
||||||
|
export type OscilloscopeDataResponse = {
|
||||||
|
/** Transpiled from uint */
|
||||||
|
aDFrequency: number;
|
||||||
|
/** Transpiled from byte */
|
||||||
|
aDVpp: number;
|
||||||
|
/** Transpiled from byte */
|
||||||
|
aDMax: number;
|
||||||
|
/** Transpiled from byte */
|
||||||
|
aDMin: number;
|
||||||
|
/** Transpiled from string */
|
||||||
|
waveformData: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transpiled from server.Hubs.OscilloscopeFullConfig */
|
||||||
|
export type OscilloscopeFullConfig = {
|
||||||
|
/** Transpiled from bool */
|
||||||
|
captureEnabled: boolean;
|
||||||
|
/** Transpiled from byte */
|
||||||
|
triggerLevel: number;
|
||||||
|
/** Transpiled from bool */
|
||||||
|
triggerRisingEdge: boolean;
|
||||||
|
/** Transpiled from ushort */
|
||||||
|
horizontalShift: number;
|
||||||
|
/** Transpiled from ushort */
|
||||||
|
decimationRate: number;
|
||||||
|
/** Transpiled from int */
|
||||||
|
captureFrequency: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** Transpiled from server.Hubs.ProgressStatus */
|
/** Transpiled from server.Hubs.ProgressStatus */
|
||||||
export enum ProgressStatus {
|
export enum ProgressStatus {
|
||||||
Running = 0,
|
Running = 0,
|
||||||
|
|||||||
@@ -1,109 +1,336 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-base-100 flex flex-col gap-4">
|
<div
|
||||||
<!-- 波形展示 -->
|
class="oscilloscope-container min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-slate-800 p-4"
|
||||||
<div class="card bg-base-200 shadow-xl mx-5">
|
>
|
||||||
<div class="card-body">
|
<!-- 顶部状态栏 -->
|
||||||
<h2 class="card-title flex flex-row justify-between">
|
<div class="status-bar mb-6">
|
||||||
<div class="flex items-center gap-2">
|
<div
|
||||||
<Activity class="w-5 h-5" />
|
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20"
|
||||||
波形显示
|
>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="status-indicator flex items-center gap-2">
|
||||||
|
<div class="relative">
|
||||||
|
<Activity class="w-6 h-6 text-blue-600" />
|
||||||
|
<div
|
||||||
|
v-if="osc.isCapturing.value"
|
||||||
|
class="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div>
|
||||||
<button class="btn btn-sm btn-warning" @click="osc.stopCapture" :disabled="!osc.isCapturing.value">
|
<h1
|
||||||
停止捕获
|
class="text-xl font-bold text-slate-800 dark:text-slate-200"
|
||||||
</button>
|
>
|
||||||
<div class="flex items-center gap-2">
|
数字示波器
|
||||||
<button class="btn btn-sm btn-error" @click="osc.clearOscilloscopeData">
|
</h1>
|
||||||
清空
|
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||||
</button>
|
{{ osc.isCapturing.value ? "正在采集数据..." : "待机状态" }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
|
||||||
<OscilloscopeWaveformDisplay />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 示波器配置 -->
|
<div class="control-buttons flex items-center gap-3">
|
||||||
<div class="card bg-base-200 shadow-xl mx-5">
|
<button
|
||||||
<div class="card-body">
|
class="btn-gradient"
|
||||||
<h2 class="card-title">示波器配置</h2>
|
:class="osc.isCapturing.value ? 'btn-stop' : 'btn-start'"
|
||||||
<form class="flex flex-col gap-2" @submit.prevent="applyConfiguration">
|
@click="toggleCapture"
|
||||||
<div class="flex flex-row items-center justify-between gap-4">
|
>
|
||||||
<label>
|
<component
|
||||||
边沿触发:
|
:is="osc.isCapturing.value ? Square : Play"
|
||||||
<select v-model="osc.config.triggerRisingEdge" class="select select-bordered w-24">
|
class="w-5 h-5"
|
||||||
<option :value="true">上升沿</option>
|
/>
|
||||||
<option :value="false">下降沿</option>
|
{{ osc.isCapturing.value ? "停止采集" : "开始采集" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn-clear"
|
||||||
|
@click="osc.clearOscilloscopeData"
|
||||||
|
:disabled="osc.isCapturing.value"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
清空数据
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<div class="main-content grid grid-cols-1 xl:grid-cols-4 gap-6">
|
||||||
|
<!-- 波形显示区域 - 占据大部分空间 -->
|
||||||
|
<div class="waveform-section xl:col-span-3">
|
||||||
|
<div
|
||||||
|
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20 h-full"
|
||||||
|
>
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="waveform-header flex items-center justify-between mb-4">
|
||||||
|
<h2
|
||||||
|
class="text-lg font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Zap class="w-5 h-5 text-yellow-500" />
|
||||||
|
波形显示
|
||||||
|
</h2>
|
||||||
|
<div class="waveform-controls flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="refresh-indicator flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 bg-green-500 rounded-full animate-pulse"
|
||||||
|
></div>
|
||||||
|
{{ refreshCycle }}ms 刷新
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="waveform-display h-96 lg:h-[500px] relative overflow-hidden rounded-lg border border-slate-200 dark:border-slate-700"
|
||||||
|
>
|
||||||
|
<OscilloscopeWaveformDisplay class="w-full h-full" />
|
||||||
|
|
||||||
|
<!-- 数据覆盖层 -->
|
||||||
|
<div
|
||||||
|
v-if="osc.isCapturing.value && !hasWaveformData"
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-slate-50/50 dark:bg-slate-900/50 backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 mx-auto text-slate-400">
|
||||||
|
<Activity class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-600 dark:text-slate-400">
|
||||||
|
等待波形数据...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制面板 -->
|
||||||
|
<div class="control-panel xl:col-span-1">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- 触发设置 -->
|
||||||
|
<div
|
||||||
|
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20"
|
||||||
|
>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3
|
||||||
|
class="text-lg font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2 mb-4"
|
||||||
|
>
|
||||||
|
<Target class="w-5 h-5 text-red-500" />
|
||||||
|
触发设置
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">触发边沿</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
v-model="osc.config.triggerRisingEdge"
|
||||||
|
class="select select-bordered w-full focus:border-blue-500 transition-colors"
|
||||||
|
>
|
||||||
|
<option :value="true">上升沿 ↗</option>
|
||||||
|
<option :value="false">下降沿 ↘</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
触发电平:
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input type="range" min="0" max="255" step="1" v-model="osc.config.triggerLevel"
|
|
||||||
class="range range-sm w-50" />
|
|
||||||
<input type="number" v-model="osc.config.triggerLevel" min="0" max="255"
|
|
||||||
class="input input-bordered w-24" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">触发电平</span>
|
||||||
|
<span class="label-text-alt">{{
|
||||||
|
osc.config.triggerLevel
|
||||||
|
}}</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<input
|
||||||
水平偏移:
|
type="range"
|
||||||
<div class="flex items-center gap-2">
|
min="0"
|
||||||
<input type="range" min="0" max="1000" step="1" v-model="osc.config.horizontalShift"
|
max="255"
|
||||||
class="range range-sm w-50" />
|
step="1"
|
||||||
<input type="number" v-model="osc.config.horizontalShift" min="0" max="1000"
|
v-model="osc.config.triggerLevel"
|
||||||
class="input input-bordered w-24" />
|
class="range range-primary [--range-bg:#2b7fff]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="range-labels flex justify-between text-xs text-slate-500 mt-1"
|
||||||
|
>
|
||||||
|
<span>0</span>
|
||||||
|
<span>128</span>
|
||||||
|
<span>255</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时基设置 -->
|
||||||
|
<div
|
||||||
|
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20"
|
||||||
|
>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3
|
||||||
|
class="text-lg font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2 mb-4"
|
||||||
|
>
|
||||||
|
<Clock class="w-5 h-5 text-blue-500" />
|
||||||
|
时基控制
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">水平偏移</span>
|
||||||
|
<span class="label-text-alt">{{
|
||||||
|
osc.config.horizontalShift
|
||||||
|
}}</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<input
|
||||||
抽取率:
|
type="range"
|
||||||
<div class="flex items-center gap-2">
|
min="0"
|
||||||
<input type="range" min="0" max="100" step="1" v-model="osc.config.decimationRate"
|
max="1000"
|
||||||
class="range range-sm w-50" />
|
step="1"
|
||||||
<input type="number" v-model="osc.config.decimationRate" min="0" max="100"
|
v-model="osc.config.horizontalShift"
|
||||||
class="input input-bordered w-24" />
|
class="range range-secondary [--range-bg:#c27aff]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">抽取率</span>
|
||||||
|
<span class="label-text-alt"
|
||||||
|
>{{ osc.config.decimationRate }}%</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
v-model="osc.config.decimationRate"
|
||||||
|
class="range range-accent [--range-bg:#fb64b6]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">刷新间隔</span>
|
||||||
|
<span class="label-text-alt">{{ refreshCycle }}ms</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="1000"
|
||||||
|
step="1"
|
||||||
|
v-model="refreshCycle"
|
||||||
|
class="range range-info [--range-bg:#51a2ff]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统控制 -->
|
||||||
|
<div
|
||||||
|
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20"
|
||||||
|
>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div
|
||||||
|
class="card-title flex flex-row justify-between items-center mb-4"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="text-lg font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Settings class="w-5 h-5 text-purple-500" />
|
||||||
|
系统控制
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- 自动应用开关 -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label cursor-pointer">
|
||||||
|
<span class="label-text text-sm font-medium"
|
||||||
|
>自动应用设置</span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-primary"
|
||||||
|
v-model="osc.isAutoApplying"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-2 mt-2">
|
|
||||||
<label>
|
<div class="space-y-4">
|
||||||
刷新间隔(ms):
|
<!-- 控制按钮组 -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="space-y-2">
|
||||||
<input type="range" min="100" max="3000" step="100" v-model="osc.refreshIntervalMs.value"
|
<button
|
||||||
class="range range-sm w-50" />
|
class="btn-primary-full"
|
||||||
<input type="number" min="100" max="3000" step="100" v-model="osc.refreshIntervalMs.value"
|
@click="applyConfiguration"
|
||||||
class="input input-bordered w-24" />
|
:disabled="osc.isApplying.value || osc.isCapturing.value"
|
||||||
</div>
|
>
|
||||||
</label>
|
<CheckCircle class="w-4 h-4" />
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button class="btn btn-primary" type="submit" :disabled="osc.isApplying.value || osc.isCapturing.value">
|
|
||||||
应用配置
|
应用配置
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" type="button" @click="osc.resetConfiguration"
|
|
||||||
:disabled="osc.isApplying.value || osc.isCapturing.value">
|
<button
|
||||||
重置
|
class="btn-secondary-full"
|
||||||
|
@click="resetConfiguration"
|
||||||
|
:disabled="osc.isApplying.value || osc.isCapturing.value"
|
||||||
|
>
|
||||||
|
<RotateCcw class="w-4 h-4" />
|
||||||
|
重置配置
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline" @click="osc.refreshRAM" :disabled="osc.isApplying.value || osc.isCapturing.value">
|
|
||||||
|
<button
|
||||||
|
class="btn-outline-full"
|
||||||
|
@click="osc.refreshRAM"
|
||||||
|
:disabled="osc.isApplying.value || osc.isCapturing.value"
|
||||||
|
>
|
||||||
|
<RefreshCw class="w-4 h-4" />
|
||||||
刷新RAM
|
刷新RAM
|
||||||
</button>
|
</button>
|
||||||
<!-- <button class="btn btn-accent" @click="osc.generateTestData" :disabled="osc.isOperationInProgress.value">
|
|
||||||
生成测试数据
|
|
||||||
</button> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态提示 -->
|
||||||
|
<div
|
||||||
|
v-if="osc.isApplying.value"
|
||||||
|
class="fixed bottom-4 right-4 alert alert-info shadow-lg max-w-sm animate-slide-in-right"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"
|
||||||
|
></div>
|
||||||
|
<span>正在应用配置...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Activity } from "lucide-vue-next";
|
import {
|
||||||
|
Activity,
|
||||||
|
Settings,
|
||||||
|
Play,
|
||||||
|
Square,
|
||||||
|
Trash2,
|
||||||
|
Zap,
|
||||||
|
Target,
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
RotateCcw,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-vue-next";
|
||||||
import { OscilloscopeWaveformDisplay } from "@/components/Oscilloscope";
|
import { OscilloscopeWaveformDisplay } from "@/components/Oscilloscope";
|
||||||
import { useEquipments } from "@/stores/equipments";
|
import { useEquipments } from "@/stores/equipments";
|
||||||
import { useOscilloscopeState } from "@/components/Oscilloscope/OscilloscopeManager";
|
import { useOscilloscopeState } from "@/components/Oscilloscope/OscilloscopeManager";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
// 使用全局设备配置
|
// 使用全局设备配置
|
||||||
const equipments = useEquipments();
|
const equipments = useEquipments();
|
||||||
@@ -111,6 +338,178 @@ const equipments = useEquipments();
|
|||||||
// 获取示波器状态和操作
|
// 获取示波器状态和操作
|
||||||
const osc = useRequiredInjection(useOscilloscopeState);
|
const osc = useRequiredInjection(useOscilloscopeState);
|
||||||
|
|
||||||
|
const refreshCycle = ref(10);
|
||||||
|
|
||||||
|
// 计算是否有波形数据
|
||||||
|
const hasWaveformData = computed(() => {
|
||||||
|
const data = osc.oscData.value;
|
||||||
|
return data && data.x && data.y && data.x.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
// 应用配置
|
// 应用配置
|
||||||
const applyConfiguration = () => osc.applyConfiguration();
|
function applyConfiguration() {
|
||||||
|
osc.applyConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCapture() {
|
||||||
|
osc.toggleCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConfiguration() {
|
||||||
|
osc.resetConfiguration();
|
||||||
|
refreshCycle.value = 1000 / osc.config.captureFrequency;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="postcss">
|
||||||
|
@import "@/assets/main.css";
|
||||||
|
/* 渐变按钮样式 */
|
||||||
|
.btn-gradient {
|
||||||
|
@apply px-6 py-3 rounded-lg font-medium transition-all duration-300 transform hover:scale-105 active:scale-95 shadow-lg flex items-center gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-start {
|
||||||
|
@apply bg-gradient-to-r from-green-500 to-blue-600 hover:from-green-600 hover:to-blue-700 text-white shadow-green-200 hover:shadow-green-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop {
|
||||||
|
@apply bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-600 hover:to-pink-700 text-white shadow-red-200 hover:shadow-red-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
@apply px-4 py-3 bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white rounded-lg font-medium transition-all duration-300 transform hover:scale-105 active:scale-95 shadow-lg flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全宽按钮样式 */
|
||||||
|
.btn-primary-full {
|
||||||
|
@apply w-full px-4 py-3 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white rounded-lg font-medium transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-lg flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary-full {
|
||||||
|
@apply w-full px-4 py-3 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-700 text-white rounded-lg font-medium transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-lg flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-full {
|
||||||
|
@apply w-full px-4 py-3 border-2 border-slate-300 dark:border-slate-600 hover:border-blue-500 dark:hover:border-blue-400 text-slate-700 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 rounded-lg font-medium transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-sm hover:shadow-md flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑块样式美化 */
|
||||||
|
.range {
|
||||||
|
@apply rounded-lg appearance-none cursor-pointer;
|
||||||
|
--range-fill: 0;
|
||||||
|
--range-thumb: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range::-webkit-slider-thumb {
|
||||||
|
@apply appearance-none bg-white border-2 border-current rounded-full cursor-pointer shadow-lg hover:shadow-xl transition-shadow duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range::-moz-range-thumb {
|
||||||
|
@apply bg-white border-2 border-current rounded-full cursor-pointer shadow-lg hover:shadow-xl transition-shadow duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 范围标签 */
|
||||||
|
.range-labels {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片悬停效果 */
|
||||||
|
.card {
|
||||||
|
@apply transition-all duration-300 hover:shadow-2xl hover:scale-[1.01];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义动画 */
|
||||||
|
@keyframes slide-in-right {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-right {
|
||||||
|
animation: slide-in-right 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 玻璃态效果增强 */
|
||||||
|
.backdrop-blur-lg {
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态指示器脉动效果 */
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 5px currentColor;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 20px currentColor,
|
||||||
|
0 0 30px currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator .animate-pulse {
|
||||||
|
animation: pulse-glow 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.main-content {
|
||||||
|
@apply grid-cols-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
@apply order-first;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel .space-y-6 {
|
||||||
|
@apply grid grid-cols-1 md:grid-cols-3 gap-4 space-y-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.control-panel .space-y-6 {
|
||||||
|
@apply grid-cols-1 space-y-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
@apply flex-col gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar .card-body {
|
||||||
|
@apply flex-col items-start gap-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条美化 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
@apply w-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
@apply bg-slate-100 dark:bg-slate-800 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-slate-300 dark:bg-slate-600 rounded-full hover:bg-slate-400 dark:hover:bg-slate-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入焦点效果 */
|
||||||
|
.select:focus,
|
||||||
|
.input:focus {
|
||||||
|
@apply ring-2 ring-blue-500 opacity-50 border-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 切换开关样式 */
|
||||||
|
.toggle {
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle:checked {
|
||||||
|
@apply shadow-lg;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user