feat: 使用SignalR实时发送示波器数据,并美化示波器界面

This commit is contained in:
SikongJueluo 2025-08-19 12:55:18 +08:00
parent 1b5b0e28e3
commit 7e53b805ae
No known key found for this signature in database
13 changed files with 1664 additions and 347 deletions

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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
{

View 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;
}
}
}

View File

@ -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最多等待50ms5次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++)

View File

@ -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;
}

View File

@ -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,

View File

@ -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>

View File

@ -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}`, {

View File

@ -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();

View File

@ -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

View File

@ -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,

View File

@ -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>