feat: 使用SignalR实时发送示波器数据,并美化示波器界面
This commit is contained in:
parent
1b5b0e28e3
commit
7e53b805ae
|
@ -88,7 +88,8 @@ try
|
|||
path.StartsWithSegments("/hubs/JtagHub") ||
|
||||
path.StartsWithSegments("/hubs/ProgressHub") ||
|
||||
path.StartsWithSegments("/hubs/DigitalTubesHub") ||
|
||||
path.StartsWithSegments("/hubs/RotaryEncoderHub")
|
||||
path.StartsWithSegments("/hubs/RotaryEncoderHub") ||
|
||||
path.StartsWithSegments("/hubs/OscilloscopeHub")
|
||||
))
|
||||
{
|
||||
// Read the token out of the query string
|
||||
|
@ -256,6 +257,7 @@ try
|
|||
app.MapHub<server.Hubs.ProgressHub>("/hubs/ProgressHub");
|
||||
app.MapHub<server.Hubs.DigitalTubesHub>("/hubs/DigitalTubesHub");
|
||||
app.MapHub<server.Hubs.RotaryEncoderHub>("/hubs/RotaryEncoderHub");
|
||||
app.MapHub<server.Hubs.OscilloscopeHub>("/hubs/OscilloscopeHub");
|
||||
|
||||
// Setup Program
|
||||
MsgBus.Init();
|
||||
|
|
|
@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Peripherals.OscilloscopeClient;
|
||||
using server.Hubs;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
|
@ -9,6 +10,7 @@ namespace server.Controllers;
|
|||
/// 示波器API控制器 - 普通用户权限
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[EnableCors("Development")]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class OscilloscopeApiController : ControllerBase
|
||||
|
@ -20,7 +22,7 @@ public class OscilloscopeApiController : ControllerBase
|
|||
/// <summary>
|
||||
/// 获取示波器实例
|
||||
/// </summary>
|
||||
private Oscilloscope? GetOscilloscope()
|
||||
private OscilloscopeCtrl? GetOscilloscope()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -41,7 +43,7 @@ public class OscilloscopeApiController : ControllerBase
|
|||
return null;
|
||||
|
||||
var board = boardRet.Value.Value;
|
||||
return new Oscilloscope(board.IpAddr, board.Port);
|
||||
return new OscilloscopeCtrl(board.IpAddr, board.Port);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -56,12 +58,11 @@ public class OscilloscopeApiController : ControllerBase
|
|||
/// <param name="config">示波器配置</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("Initialize")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> Initialize([FromBody] OscilloscopeFullConfig config)
|
||||
public async Task<IActionResult> Initialize([FromBody] OscilloscopeConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -151,7 +152,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("StartCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
|
@ -185,7 +185,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("StopCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
|
@ -219,7 +218,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||
/// </summary>
|
||||
/// <returns>示波器数据和状态信息</returns>
|
||||
[HttpGet("GetData")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(OscilloscopeDataResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
|
@ -293,7 +291,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||
/// <param name="risingEdge">触发边沿(true为上升沿,false为下降沿)</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("UpdateTrigger")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
|
@ -338,7 +335,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||
/// <param name="decimationRate">抽样率(0-1023)</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("UpdateSampling")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
|
@ -387,7 +383,6 @@ public class OscilloscopeApiController : ControllerBase
|
|||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("RefreshRAM")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
|
@ -415,72 +410,4 @@ public class OscilloscopeApiController : ControllerBase
|
|||
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 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 ClientID { get; set; }
|
||||
|
@ -50,13 +44,22 @@ public class ScanTaskInfo
|
|||
public int Frequency { get; set; } = 100;
|
||||
public bool IsRunning { get; set; } = false;
|
||||
|
||||
public ScanTaskInfo(
|
||||
public DigitalTubesScanTaskInfo(
|
||||
string boardID, string clientID, SevenDigitalTubesCtrl client)
|
||||
{
|
||||
BoardID = boardID;
|
||||
ClientID = clientID;
|
||||
TubeClient = client;
|
||||
}
|
||||
|
||||
public DigitalTubeTaskStatus ToDigitalTubeTaskStatus()
|
||||
{
|
||||
return new DigitalTubeTaskStatus
|
||||
{
|
||||
Frequency = Frequency,
|
||||
IsRunning = IsRunning
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
|
@ -67,7 +70,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
|
|||
private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
|
||||
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)
|
||||
{
|
||||
|
@ -100,7 +103,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
|
|||
return boardRet.Value.Value;
|
||||
}
|
||||
|
||||
private Task ScanAllTubes(ScanTaskInfo scanInfo)
|
||||
private Task ScanAllTubes(DigitalTubesScanTaskInfo scanInfo)
|
||||
{
|
||||
var token = scanInfo.CTS.Token;
|
||||
return Task.Run(async () =>
|
||||
|
@ -163,7 +166,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
|
|||
return true;
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var scanTaskInfo = new ScanTaskInfo(
|
||||
var scanTaskInfo = new DigitalTubesScanTaskInfo(
|
||||
board.ID.ToString(), Context.ConnectionId,
|
||||
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 0)
|
||||
);
|
||||
|
@ -240,7 +243,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
|
|||
|
||||
if (_scanTasks.TryGetValue(key, out var scanInfo))
|
||||
{
|
||||
return new DigitalTubeTaskStatus(scanInfo);
|
||||
return scanInfo.ToDigitalTubeTaskStatus();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -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 DotNext;
|
||||
using WebProtocol;
|
||||
using Tapper;
|
||||
|
||||
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
|
||||
{
|
||||
const UInt32 BASE = 0x8000_0000;
|
||||
|
@ -71,7 +82,7 @@ static class OscilloscopeAddr
|
|||
public const UInt32 RD_DATA_LENGTH = 0x0000_0400;
|
||||
}
|
||||
|
||||
class Oscilloscope
|
||||
class OscilloscopeCtrl
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
|
@ -88,7 +99,7 @@ class Oscilloscope
|
|||
/// <param name="address">示波器设备IP地址</param>
|
||||
/// <param name="port">示波器设备端口</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)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
|
@ -98,6 +109,49 @@ class Oscilloscope
|
|||
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>
|
||||
|
@ -309,13 +363,13 @@ class Oscilloscope
|
|||
// 等待WAVE_READY[0]位为1,最多等待50ms(5次x10ms间隔)
|
||||
var readyResult = await UDPClientPool.ReadAddrWithWait(
|
||||
this.ep, this.taskID, OscilloscopeAddr.WAVE_READY, 0b00, 0x01, 10, 50);
|
||||
|
||||
|
||||
if (!readyResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to wait for wave ready: {readyResult.Error}");
|
||||
return new(readyResult.Error);
|
||||
}
|
||||
|
||||
|
||||
// 无论准备好与否,都继续读取数据(readyResult.Value表示是否在超时前准备好)
|
||||
if (!readyResult.Value)
|
||||
{
|
||||
|
@ -365,14 +419,14 @@ class Oscilloscope
|
|||
logger.Error("ReadAddr returned invalid data for trigger position");
|
||||
return new(new Exception("Failed to read trigger position"));
|
||||
}
|
||||
|
||||
|
||||
UInt32 trigAddr = Number.BytesToUInt32(trigPosResult.Value.Options.Data).Value;
|
||||
|
||||
|
||||
// 根据触发地址对数据进行偏移,使触发点位于数据中间
|
||||
int targetPos = sampleCount / 2; // 目标位置:数据中间
|
||||
int actualTrigPos = (int)(trigAddr % (UInt32)sampleCount); // 实际触发位置
|
||||
int shiftAmount = targetPos - actualTrigPos;
|
||||
|
||||
|
||||
// 创建偏移后的数据数组
|
||||
byte[] offsetData = new byte[sampleCount];
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
|
|
|
@ -299,7 +299,7 @@ export class VideoStreamClient {
|
|||
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?";
|
||||
if (enable === 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;
|
||||
let _headers: any = {};
|
||||
if (response.headers && typeof response.headers === "object") {
|
||||
|
@ -343,7 +343,7 @@ export class VideoStreamClient {
|
|||
let resultData200 = _responseText;
|
||||
result200 = resultData200 !== undefined ? resultData200 : <any>null;
|
||||
|
||||
return Promise.resolve<any>(result200);
|
||||
return Promise.resolve<string>(result200);
|
||||
|
||||
} else if (status === 500) {
|
||||
const _responseText = response.data;
|
||||
|
@ -357,7 +357,7 @@ export class VideoStreamClient {
|
|||
const _responseText = response.data;
|
||||
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 示波器配置
|
||||
* @return 操作结果
|
||||
*/
|
||||
initialize(config: OscilloscopeFullConfig, cancelToken?: CancelToken): Promise<boolean> {
|
||||
initialize(config: OscilloscopeConfig, cancelToken?: CancelToken): Promise<boolean> {
|
||||
let url_ = this.baseUrl + "/api/OscilloscopeApi/Initialize";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
|
@ -9090,22 +9090,14 @@ export interface INetworkInterfaceDto {
|
|||
macAddress: string;
|
||||
}
|
||||
|
||||
/** 示波器完整配置 */
|
||||
export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
|
||||
/** 是否启动捕获 */
|
||||
export class OscilloscopeConfig implements IOscilloscopeConfig {
|
||||
captureEnabled!: boolean;
|
||||
/** 触发电平(0-255) */
|
||||
triggerLevel!: number;
|
||||
/** 触发边沿(true为上升沿,false为下降沿) */
|
||||
triggerRisingEdge!: boolean;
|
||||
/** 水平偏移量(0-1023) */
|
||||
horizontalShift!: number;
|
||||
/** 抽样率(0-1023) */
|
||||
decimationRate!: number;
|
||||
/** 是否自动刷新RAM */
|
||||
autoRefreshRAM!: boolean;
|
||||
|
||||
constructor(data?: IOscilloscopeFullConfig) {
|
||||
constructor(data?: IOscilloscopeConfig) {
|
||||
if (data) {
|
||||
for (var property in data) {
|
||||
if (data.hasOwnProperty(property))
|
||||
|
@ -9121,13 +9113,12 @@ export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
|
|||
this.triggerRisingEdge = _data["triggerRisingEdge"];
|
||||
this.horizontalShift = _data["horizontalShift"];
|
||||
this.decimationRate = _data["decimationRate"];
|
||||
this.autoRefreshRAM = _data["autoRefreshRAM"];
|
||||
}
|
||||
}
|
||||
|
||||
static fromJS(data: any): OscilloscopeFullConfig {
|
||||
static fromJS(data: any): OscilloscopeConfig {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
let result = new OscilloscopeFullConfig();
|
||||
let result = new OscilloscopeConfig();
|
||||
result.init(data);
|
||||
return result;
|
||||
}
|
||||
|
@ -9139,38 +9130,23 @@ export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
|
|||
data["triggerRisingEdge"] = this.triggerRisingEdge;
|
||||
data["horizontalShift"] = this.horizontalShift;
|
||||
data["decimationRate"] = this.decimationRate;
|
||||
data["autoRefreshRAM"] = this.autoRefreshRAM;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/** 示波器完整配置 */
|
||||
export interface IOscilloscopeFullConfig {
|
||||
/** 是否启动捕获 */
|
||||
export interface IOscilloscopeConfig {
|
||||
captureEnabled: boolean;
|
||||
/** 触发电平(0-255) */
|
||||
triggerLevel: number;
|
||||
/** 触发边沿(true为上升沿,false为下降沿) */
|
||||
triggerRisingEdge: boolean;
|
||||
/** 水平偏移量(0-1023) */
|
||||
horizontalShift: number;
|
||||
/** 抽样率(0-1023) */
|
||||
decimationRate: number;
|
||||
/** 是否自动刷新RAM */
|
||||
autoRefreshRAM: boolean;
|
||||
}
|
||||
|
||||
/** 示波器状态和数据 */
|
||||
export class OscilloscopeDataResponse implements IOscilloscopeDataResponse {
|
||||
/** AD采样频率 */
|
||||
adFrequency!: number;
|
||||
/** AD采样幅度 */
|
||||
adVpp!: number;
|
||||
/** AD采样最大值 */
|
||||
adMax!: number;
|
||||
/** AD采样最小值 */
|
||||
adMin!: number;
|
||||
/** 波形数据(Base64编码) */
|
||||
waveformData!: string;
|
||||
|
||||
constructor(data?: IOscilloscopeDataResponse) {
|
||||
|
@ -9210,17 +9186,11 @@ export class OscilloscopeDataResponse implements IOscilloscopeDataResponse {
|
|||
}
|
||||
}
|
||||
|
||||
/** 示波器状态和数据 */
|
||||
export interface IOscilloscopeDataResponse {
|
||||
/** AD采样频率 */
|
||||
adFrequency: number;
|
||||
/** AD采样幅度 */
|
||||
adVpp: number;
|
||||
/** AD采样最大值 */
|
||||
adMax: number;
|
||||
/** AD采样最小值 */
|
||||
adMin: number;
|
||||
/** 波形数据(Base64编码) */
|
||||
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 {
|
||||
OscilloscopeFullConfig,
|
||||
OscilloscopeDataResponse,
|
||||
OscilloscopeApiClient,
|
||||
} from "@/APIClient";
|
||||
autoResetRef,
|
||||
createInjectionState,
|
||||
watchDebounced,
|
||||
} 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 { useAlertStore } from "@/components/Alert";
|
||||
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 = {
|
||||
x: number[];
|
||||
|
@ -22,41 +43,103 @@ export type OscilloscopeDataType = {
|
|||
};
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
|
||||
const DEFAULT_CONFIG: OscilloscopeFullConfig = {
|
||||
captureEnabled: false,
|
||||
triggerLevel: 128,
|
||||
triggerRisingEdge: true,
|
||||
horizontalShift: 0,
|
||||
decimationRate: 50,
|
||||
autoRefreshRAM: false,
|
||||
});
|
||||
captureFrequency: 100,
|
||||
};
|
||||
|
||||
// 采样频率常量(后端返回)
|
||||
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
||||
() => {
|
||||
const oscData = shallowRef<OscilloscopeDataType>();
|
||||
// Global Store
|
||||
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 isApplying = ref(false);
|
||||
const isCapturing = ref(false);
|
||||
const isAutoApplying = ref(false);
|
||||
|
||||
// 配置
|
||||
const config = reactive<OscilloscopeFullConfig>(
|
||||
new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }),
|
||||
);
|
||||
const config = reactive<OscilloscopeFullConfig>({ ...DEFAULT_CONFIG });
|
||||
watchDebounced(
|
||||
config,
|
||||
() => {
|
||||
if (!isAutoApplying.value) return;
|
||||
|
||||
// 采样点数(由后端数据决定)
|
||||
const sampleCount = ref(0);
|
||||
|
||||
// 采样周期(ns),由adFrequency计算
|
||||
const samplePeriodNs = computed(() =>
|
||||
oscData.value?.adFrequency
|
||||
? 1_000_000_000 / oscData.value.adFrequency
|
||||
: 200,
|
||||
if (
|
||||
!isApplying.value ||
|
||||
!isCapturing.value ||
|
||||
!operationMutex.isLocked()
|
||||
) {
|
||||
applyConfiguration();
|
||||
}
|
||||
},
|
||||
{ debounce: 200, maxWait: 1000 },
|
||||
);
|
||||
|
||||
// 应用配置
|
||||
|
@ -68,14 +151,18 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||
const release = await operationMutex.acquire();
|
||||
isApplying.value = true;
|
||||
try {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
const success = await client.initialize({ ...config });
|
||||
const proxy = getHubProxy();
|
||||
|
||||
const success = await proxy.initialize(config);
|
||||
|
||||
if (success) {
|
||||
alert.success("示波器配置已应用", 2000);
|
||||
} else {
|
||||
throw new Error("应用失败");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "Hub not initialized")
|
||||
reinitializeHub();
|
||||
alert.error("应用配置失败", 3000);
|
||||
} finally {
|
||||
isApplying.value = false;
|
||||
|
@ -89,68 +176,55 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||
alert.info("配置已重置", 2000);
|
||||
};
|
||||
|
||||
const clearOscilloscopeData = () => {
|
||||
oscData.value = undefined;
|
||||
// 采样点数(由后端数据决定)
|
||||
const sampleCount = ref(0);
|
||||
|
||||
// 采样周期(ns),由adFrequency计算
|
||||
const samplePeriodNs = computed(() =>
|
||||
oscData.value?.adFrequency
|
||||
? 1_000_000_000 / oscData.value.adFrequency
|
||||
: 200,
|
||||
);
|
||||
|
||||
const analyzeOscilloscopeData = (resp: OscilloscopeDataResponse) => {
|
||||
// 解析波形数据
|
||||
const binaryString = atob(resp.waveformData);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
sampleCount.value = bytes.length;
|
||||
|
||||
// 构建时间轴
|
||||
const x = Array.from(
|
||||
{ length: bytes.length },
|
||||
(_, i) => (i * samplePeriodNs.value) / 1000, // us
|
||||
);
|
||||
const y = Array.from(bytes);
|
||||
|
||||
oscData.value = {
|
||||
x,
|
||||
y,
|
||||
xUnit: "us",
|
||||
yUnit: "V",
|
||||
adFrequency: resp.aDFrequency,
|
||||
adVpp: resp.aDVpp,
|
||||
adMax: resp.aDMax,
|
||||
adMin: resp.aDMin,
|
||||
};
|
||||
};
|
||||
|
||||
// 获取数据
|
||||
const getOscilloscopeData = async () => {
|
||||
try {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
const resp: OscilloscopeDataResponse = await client.getData();
|
||||
|
||||
// 解析波形数据
|
||||
const binaryString = atob(resp.waveformData);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
sampleCount.value = bytes.length;
|
||||
|
||||
// 构建时间轴
|
||||
const x = Array.from(
|
||||
{ length: bytes.length },
|
||||
(_, i) => (i * samplePeriodNs.value) / 1000, // us
|
||||
);
|
||||
const y = Array.from(bytes);
|
||||
|
||||
oscData.value = {
|
||||
x,
|
||||
y,
|
||||
xUnit: "us",
|
||||
yUnit: "V",
|
||||
adFrequency: resp.adFrequency,
|
||||
adVpp: resp.adVpp,
|
||||
adMax: resp.adMax,
|
||||
adMin: resp.adMin,
|
||||
};
|
||||
const proxy = getHubProxy();
|
||||
const resp = await proxy.getData();
|
||||
analyzeOscilloscopeData(resp);
|
||||
} catch (error) {
|
||||
alert.error("获取示波器数据失败", 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 定时器引用
|
||||
let refreshIntervalId: number | undefined;
|
||||
// 刷新间隔(毫秒),可根据需要调整
|
||||
const refreshIntervalMs = ref(1000);
|
||||
|
||||
// 定时刷新函数
|
||||
const startAutoRefresh = () => {
|
||||
if (refreshIntervalId !== undefined) return;
|
||||
refreshIntervalId = window.setInterval(async () => {
|
||||
await refreshRAM();
|
||||
await getOscilloscopeData();
|
||||
}, refreshIntervalMs.value);
|
||||
};
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshIntervalId !== undefined) {
|
||||
clearInterval(refreshIntervalId);
|
||||
refreshIntervalId = undefined;
|
||||
isCapturing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 启动捕获
|
||||
const startCapture = async () => {
|
||||
if (operationMutex.isLocked()) {
|
||||
|
@ -160,17 +234,13 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||
isCapturing.value = true;
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
const started = await client.startCapture();
|
||||
const proxy = getHubProxy();
|
||||
const started = await proxy.startCapture();
|
||||
if (!started) throw new Error("无法启动捕获");
|
||||
alert.info("开始捕获...", 2000);
|
||||
|
||||
// 启动定时刷新
|
||||
startAutoRefresh();
|
||||
} catch (error) {
|
||||
alert.error("捕获失败", 3000);
|
||||
isCapturing.value = false;
|
||||
stopAutoRefresh();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
|
@ -183,11 +253,10 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||
return;
|
||||
}
|
||||
isCapturing.value = false;
|
||||
stopAutoRefresh();
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
const stopped = await client.stopCapture();
|
||||
const proxy = getHubProxy();
|
||||
const stopped = await proxy.stopCapture();
|
||||
if (!stopped) throw new Error("无法停止捕获");
|
||||
alert.info("捕获已停止", 2000);
|
||||
} 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 client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
|
@ -279,9 +356,9 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||
config,
|
||||
isApplying,
|
||||
isCapturing,
|
||||
isAutoApplying,
|
||||
sampleCount,
|
||||
samplePeriodNs,
|
||||
refreshIntervalMs,
|
||||
|
||||
applyConfiguration,
|
||||
resetConfiguration,
|
||||
|
@ -289,6 +366,7 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
|||
getOscilloscopeData,
|
||||
startCapture,
|
||||
stopCapture,
|
||||
toggleCapture,
|
||||
updateTrigger,
|
||||
updateSampling,
|
||||
refreshRAM,
|
||||
|
|
|
@ -1,36 +1,93 @@
|
|||
<template>
|
||||
<div class="w-full h-100 flex flex-col">
|
||||
<!-- 原有内容 -->
|
||||
<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>
|
||||
<!-- 采集控制按钮 -->
|
||||
<div class="flex justify-center items-center mb-2">
|
||||
<div
|
||||
class="waveform-container w-full h-full relative overflow-hidden rounded-lg"
|
||||
>
|
||||
<!-- 波形图表 -->
|
||||
<v-chart
|
||||
v-if="hasData"
|
||||
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
|
||||
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="{
|
||||
'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,
|
||||
'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,
|
||||
}" @click="
|
||||
}"
|
||||
@click="
|
||||
oscManager.isCapturing.value
|
||||
? oscManager.stopCapture()
|
||||
: 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">
|
||||
<Square class="w-5 h-5" />
|
||||
<Square class="w-6 h-6 animate-pulse" />
|
||||
停止采集
|
||||
</template>
|
||||
<template v-else>
|
||||
<Play class="w-5 h-5" />
|
||||
<Play class="w-6 h-6 group-hover:animate-pulse" />
|
||||
开始采集
|
||||
</template>
|
||||
</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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
@ -61,7 +118,7 @@ import type {
|
|||
GridComponentOption,
|
||||
} from "echarts/components";
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
import { Play, Square } from "lucide-vue-next";
|
||||
import { Play, Square, Activity } from "lucide-vue-next";
|
||||
|
||||
use([
|
||||
TooltipComponent,
|
||||
|
@ -113,12 +170,23 @@ const option = computed((): EChartsOption => {
|
|||
? (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) => {
|
||||
if (!oscData.value || !yData) return;
|
||||
const seriesData = oscData.value.x.map((xValue, i) => [
|
||||
xValue,
|
||||
yData && yData[i] !== undefined ? yData[i] : 0,
|
||||
]);
|
||||
|
||||
series.push({
|
||||
type: "line",
|
||||
name: `通道 ${index + 1}`,
|
||||
|
@ -126,41 +194,82 @@ const option = computed((): EChartsOption => {
|
|||
smooth: false,
|
||||
symbol: "none",
|
||||
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,
|
||||
animationDuration: isCapturing ? 0 : 1000,
|
||||
animationDuration: isCapturing ? 0 : 1200,
|
||||
animationEasing: isCapturing ? "linear" : "cubicOut",
|
||||
animationDelay: index * 100, // 错开动画时间
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
left: "10%",
|
||||
right: "10%",
|
||||
top: "15%",
|
||||
bottom: "25%",
|
||||
left: "8%",
|
||||
right: "5%",
|
||||
top: "12%",
|
||||
bottom: "20%",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E2E8F0",
|
||||
backgroundColor: "rgba(248, 250, 252, 0.8)",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
||||
borderColor: "#E2E8F0",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#334155",
|
||||
fontSize: 12,
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
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) => {
|
||||
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;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: "5%",
|
||||
top: "2%",
|
||||
left: "center",
|
||||
textStyle: {
|
||||
color: "#64748B",
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
itemGap: 20,
|
||||
data: series.map((s) => s.name) as string[],
|
||||
},
|
||||
toolbox: {
|
||||
right: "2%",
|
||||
top: "2%",
|
||||
feature: {
|
||||
restore: {},
|
||||
saveAsImage: {},
|
||||
restore: {
|
||||
title: "重置缩放",
|
||||
},
|
||||
saveAsImage: {
|
||||
title: "保存图片",
|
||||
name: `oscilloscope_${new Date().toISOString().slice(0, 19)}`,
|
||||
},
|
||||
},
|
||||
iconStyle: {
|
||||
borderColor: "#64748B",
|
||||
},
|
||||
emphasis: {
|
||||
iconStyle: {
|
||||
borderColor: "#3B82F6",
|
||||
},
|
||||
},
|
||||
},
|
||||
dataZoom: [
|
||||
|
@ -168,47 +277,275 @@ const option = computed((): EChartsOption => {
|
|||
type: "inside",
|
||||
start: 0,
|
||||
end: 100,
|
||||
filterMode: "weakFilter",
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
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: {
|
||||
type: "value",
|
||||
name: oscData.value ? `时间 (${oscData.value.xUnit})` : "时间",
|
||||
nameLocation: "middle",
|
||||
nameGap: 30,
|
||||
nameGap: 35,
|
||||
nameTextStyle: {
|
||||
color: "#64748B",
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#CBD5E1",
|
||||
width: 1.5,
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#E2E8F0",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#64748B",
|
||||
fontSize: 11,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#F1F5F9",
|
||||
type: "dashed",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: oscData.value ? `电压 (${oscData.value.yUnit})` : "电压",
|
||||
nameLocation: "middle",
|
||||
nameGap: 40,
|
||||
nameGap: 50,
|
||||
nameTextStyle: {
|
||||
color: "#64748B",
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#CBD5E1",
|
||||
width: 1.5,
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#E2E8F0",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#64748B",
|
||||
fontSize: 11,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#F1F5F9",
|
||||
type: "dashed",
|
||||
},
|
||||
},
|
||||
},
|
||||
// 全局动画开关
|
||||
animation: !isCapturing,
|
||||
animationDuration: isCapturing ? 0 : 1000,
|
||||
animationDuration: isCapturing ? 0 : 1200,
|
||||
animationEasing: isCapturing ? "linear" : "cubicOut",
|
||||
series: series,
|
||||
};
|
||||
});
|
||||
</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连接 - 简单明了
|
||||
static createHubConnection(
|
||||
hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub" | "RotaryEncoderHub",
|
||||
hubPath:
|
||||
| "ProgressHub"
|
||||
| "JtagHub"
|
||||
| "DigitalTubesHub"
|
||||
| "RotaryEncoderHub"
|
||||
| "OscilloscopeHub",
|
||||
) {
|
||||
return new HubConnectionBuilder()
|
||||
.withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, {
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
|
||||
import type { IDigitalTubesHub, IJtagHub, IProgressHub, IRotaryEncoderHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver, IRotaryEncoderReceiver } from './server.Hubs';
|
||||
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
|
||||
import type { IDigitalTubesHub, IJtagHub, IOscilloscopeHub, IProgressHub, IRotaryEncoderHub, IDigitalTubesReceiver, IJtagReceiver, IOscilloscopeReceiver, IProgressReceiver, IRotaryEncoderReceiver } from './server.Hubs';
|
||||
import type { DigitalTubeTaskStatus, OscilloscopeFullConfig, OscilloscopeDataResponse, ProgressInfo } from '../server.Hubs';
|
||||
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
|
||||
|
||||
|
||||
|
@ -46,6 +46,7 @@ class ReceiverMethodSubscription implements Disposable {
|
|||
export type HubProxyFactoryProvider = {
|
||||
(hubType: "IDigitalTubesHub"): HubProxyFactory<IDigitalTubesHub>;
|
||||
(hubType: "IJtagHub"): HubProxyFactory<IJtagHub>;
|
||||
(hubType: "IOscilloscopeHub"): HubProxyFactory<IOscilloscopeHub>;
|
||||
(hubType: "IProgressHub"): HubProxyFactory<IProgressHub>;
|
||||
(hubType: "IRotaryEncoderHub"): HubProxyFactory<IRotaryEncoderHub>;
|
||||
}
|
||||
|
@ -57,6 +58,9 @@ export const getHubProxyFactory = ((hubType: string) => {
|
|||
if(hubType === "IJtagHub") {
|
||||
return IJtagHub_HubProxyFactory.Instance;
|
||||
}
|
||||
if(hubType === "IOscilloscopeHub") {
|
||||
return IOscilloscopeHub_HubProxyFactory.Instance;
|
||||
}
|
||||
if(hubType === "IProgressHub") {
|
||||
return IProgressHub_HubProxyFactory.Instance;
|
||||
}
|
||||
|
@ -68,6 +72,7 @@ export const getHubProxyFactory = ((hubType: string) => {
|
|||
export type ReceiverRegisterProvider = {
|
||||
(receiverType: "IDigitalTubesReceiver"): ReceiverRegister<IDigitalTubesReceiver>;
|
||||
(receiverType: "IJtagReceiver"): ReceiverRegister<IJtagReceiver>;
|
||||
(receiverType: "IOscilloscopeReceiver"): ReceiverRegister<IOscilloscopeReceiver>;
|
||||
(receiverType: "IProgressReceiver"): ReceiverRegister<IProgressReceiver>;
|
||||
(receiverType: "IRotaryEncoderReceiver"): ReceiverRegister<IRotaryEncoderReceiver>;
|
||||
}
|
||||
|
@ -79,6 +84,9 @@ export const getReceiverRegister = ((receiverType: string) => {
|
|||
if(receiverType === "IJtagReceiver") {
|
||||
return IJtagReceiver_Binder.Instance;
|
||||
}
|
||||
if(receiverType === "IOscilloscopeReceiver") {
|
||||
return IOscilloscopeReceiver_Binder.Instance;
|
||||
}
|
||||
if(receiverType === "IProgressReceiver") {
|
||||
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> {
|
||||
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> {
|
||||
|
||||
public static Instance = new IProgressReceiver_Binder();
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
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';
|
||||
|
||||
export type IDigitalTubesHub = {
|
||||
|
@ -43,6 +43,46 @@ export type IJtagHub = {
|
|||
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 = {
|
||||
/**
|
||||
* @param taskId Transpiled from string
|
||||
|
@ -102,6 +142,14 @@ export type IJtagReceiver = {
|
|||
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 = {
|
||||
/**
|
||||
* @param message Transpiled from server.Hubs.ProgressInfo
|
||||
|
|
|
@ -10,6 +10,36 @@ export type DigitalTubeTaskStatus = {
|
|||
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 */
|
||||
export enum ProgressStatus {
|
||||
Running = 0,
|
||||
|
|
|
@ -1,109 +1,336 @@
|
|||
<template>
|
||||
<div class="bg-base-100 flex flex-col gap-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="flex items-center gap-2">
|
||||
<Activity class="w-5 h-5" />
|
||||
波形显示
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-sm btn-warning" @click="osc.stopCapture" :disabled="!osc.isCapturing.value">
|
||||
停止捕获
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-sm btn-error" @click="osc.clearOscilloscopeData">
|
||||
清空
|
||||
<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="status-bar mb-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">
|
||||
<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>
|
||||
<h1
|
||||
class="text-xl font-bold text-slate-800 dark:text-slate-200"
|
||||
>
|
||||
数字示波器
|
||||
</h1>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
{{ osc.isCapturing.value ? "正在采集数据..." : "待机状态" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-buttons flex items-center gap-3">
|
||||
<button
|
||||
class="btn-gradient"
|
||||
:class="osc.isCapturing.value ? 'btn-stop' : 'btn-start'"
|
||||
@click="toggleCapture"
|
||||
>
|
||||
<component
|
||||
:is="osc.isCapturing.value ? Square : Play"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
{{ osc.isCapturing.value ? "停止采集" : "开始采集" }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-clear"
|
||||
@click="osc.clearOscilloscopeData"
|
||||
:disabled="osc.isCapturing.value"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
清空数据
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<OscilloscopeWaveformDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示波器配置 -->
|
||||
<div class="card bg-base-200 shadow-xl mx-5">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">示波器配置</h2>
|
||||
<form class="flex flex-col gap-2" @submit.prevent="applyConfiguration">
|
||||
<div class="flex flex-row items-center justify-between gap-4">
|
||||
<label>
|
||||
边沿触发:
|
||||
<select v-model="osc.config.triggerRisingEdge" class="select select-bordered w-24">
|
||||
<option :value="true">上升沿</option>
|
||||
<option :value="false">下降沿</option>
|
||||
</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 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>
|
||||
</label>
|
||||
<label>
|
||||
水平偏移:
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="range" min="0" max="1000" step="1" v-model="osc.config.horizontalShift"
|
||||
class="range range-sm w-50" />
|
||||
<input type="number" v-model="osc.config.horizontalShift" min="0" max="1000"
|
||||
class="input input-bordered w-24" />
|
||||
</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>
|
||||
</label>
|
||||
<label>
|
||||
抽取率:
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="range" min="0" max="100" step="1" v-model="osc.config.decimationRate"
|
||||
class="range range-sm w-50" />
|
||||
<input type="number" v-model="osc.config.decimationRate" min="0" max="100"
|
||||
class="input input-bordered w-24" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 mt-2">
|
||||
<label>
|
||||
刷新间隔(ms):
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="range" min="100" max="3000" step="100" v-model="osc.refreshIntervalMs.value"
|
||||
class="range range-sm w-50" />
|
||||
<input type="number" min="100" max="3000" step="100" v-model="osc.refreshIntervalMs.value"
|
||||
class="input input-bordered w-24" />
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-primary" type="submit" :disabled="osc.isApplying.value || osc.isCapturing.value">
|
||||
应用配置
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" @click="osc.resetConfiguration"
|
||||
:disabled="osc.isApplying.value || osc.isCapturing.value">
|
||||
重置
|
||||
</button>
|
||||
<button class="btn btn-outline" @click="osc.refreshRAM" :disabled="osc.isApplying.value || osc.isCapturing.value">
|
||||
刷新RAM
|
||||
</button>
|
||||
<!-- <button class="btn btn-accent" @click="osc.generateTestData" :disabled="osc.isOperationInProgress.value">
|
||||
生成测试数据
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
</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>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="255"
|
||||
step="1"
|
||||
v-model="osc.config.triggerLevel"
|
||||
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
|
||||
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>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1000"
|
||||
step="1"
|
||||
v-model="osc.config.horizontalShift"
|
||||
class="range range-secondary [--range-bg:#c27aff]"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 控制按钮组 -->
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
class="btn-primary-full"
|
||||
@click="applyConfiguration"
|
||||
:disabled="osc.isApplying.value || osc.isCapturing.value"
|
||||
>
|
||||
<CheckCircle class="w-4 h-4" />
|
||||
应用配置
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-secondary-full"
|
||||
@click="resetConfiguration"
|
||||
:disabled="osc.isApplying.value || osc.isCapturing.value"
|
||||
>
|
||||
<RotateCcw class="w-4 h-4" />
|
||||
重置配置
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-outline-full"
|
||||
@click="osc.refreshRAM"
|
||||
:disabled="osc.isApplying.value || osc.isCapturing.value"
|
||||
>
|
||||
<RefreshCw class="w-4 h-4" />
|
||||
刷新RAM
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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 { useEquipments } from "@/stores/equipments";
|
||||
import { useOscilloscopeState } from "@/components/Oscilloscope/OscilloscopeManager";
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// 使用全局设备配置
|
||||
const equipments = useEquipments();
|
||||
|
@ -111,6 +338,178 @@ const equipments = useEquipments();
|
|||
// 获取示波器状态和操作
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
|
Loading…
Reference in New Issue