@using MudBlazor
@using Microsoft.AspNetCore.SignalR.Client
@using RobotNet10.RobotApp.Client.Clients
@using RobotNet10.Shared.Sensor
@implements IAsyncDisposable
@inject LidarHubClient LidarHubClient
@inject ISnackbar Snackbar
@DeviceName
@DeviceId
Status
Points: @LidarData.Ranges.Length
@if (LidarData.ScanTime > 0)
{
Frequency: @((1.0 / LidarData.ScanTime).ToString("F1")) Hz
}
@if (LidarData.AngleIncrement > 0)
{
Resolution: @FormatAngleDegreesPrecise(LidarData.AngleIncrement)°
}
Specifications
Angle Range: @FormatAngleDegrees(LidarData.AngleMin)° to @FormatAngleDegrees(LidarData.AngleMax)°
FOV: @FormatAngleDegrees(LidarData.AngleMax - LidarData.AngleMin)°
Range: @LidarData.RangeMin.ToString("F2") m to @LidarData.RangeMax.ToString("F2") m
Intensity: @(LidarData.Intensities != null && LidarData.Intensities.Length > 0 ? "Supported" : "Not Supported")
@if (Statistics != null)
{
Statistics
Avg Distance: @Statistics.AvgDistance.ToString("F2") m
Min Distance: @Statistics.MinDistance.ToString("F2") m
Max Distance: @Statistics.MaxDistance.ToString("F2") m
Avg Intensity: @Statistics.AvgIntensity.ToString("F1")
}
Last Update: @LidarData.Header.Stamp.ToString("yyyy-MM-dd HH:mm:ss.fff")
@code {
#region Constants
private const int SvgSize = 400;
private const double SvgCenter = 200.0;
private const double SvgMaxRadius = 150.0;
private const double DefaultMaxDistanceM = 10.0;
private const int GridLineStepDegrees = 30;
private static readonly int[] RangeIndicatorRadii = { 150, 100, 50 };
#endregion
#region Parameters
[Parameter, EditorRequired]
public string DeviceId { get; set; } = string.Empty;
#endregion
#region Private Fields
private string DeviceName { get; set; } = string.Empty;
private LaserScan LidarData = new();
private bool IsReloading = false;
private bool AutoReloadEnabled = false;
private System.Threading.PeriodicTimer? _autoReloadTimer;
private readonly System.Threading.CancellationTokenSource _cancellationTokenSource = new();
#endregion
#region Computed Properties
private bool IsLoading => !LidarHubClient.IsConnected;
///
/// Tính AngleIncrement thực tế dựa trên số lượng Ranges để hiển thị đúng
///
private double ActualAngleIncrement
{
get
{
if (LidarData.Ranges == null || LidarData.Ranges.Length == 0)
return LidarData.AngleIncrement;
var angleSpan = LidarData.AngleMax - LidarData.AngleMin;
if (angleSpan <= 0 || LidarData.Ranges.Length <= 1)
return LidarData.AngleIncrement;
// Tính AngleIncrement thực tế dựa trên số lượng điểm
return angleSpan / (LidarData.Ranges.Length - 1);
}
}
private IReadOnlyList ValidScanPoints
{
get
{
if (LidarData.Ranges == null || LidarData.Ranges.Length == 0)
return Array.Empty();
var points = new List();
// Sử dụng ActualAngleIncrement để tính góc đúng cho hiển thị
var angleIncrement = ActualAngleIncrement;
for (int i = 0; i < LidarData.Ranges.Length; i++)
{
var range = LidarData.Ranges[i];
var angle = LidarData.AngleMin + i * angleIncrement;
var isValid = !double.IsNaN(range) && !double.IsInfinity(range) &&
range >= LidarData.RangeMin && range <= LidarData.RangeMax;
var intensity = LidarData.Intensities != null && i < LidarData.Intensities.Length
? LidarData.Intensities[i] : 0.0;
if (isValid)
{
points.Add(new ScanPoint
{
AngleRad = angle,
AngleDeg = angle * 180.0 / Math.PI,
DistanceM = range,
Intensity = intensity
});
}
}
return points.OrderBy(p => p.AngleDeg).ToList();
}
}
private double MaxDistanceM
{
get
{
var validPoints = ValidScanPoints;
if (validPoints.Count == 0)
return DefaultMaxDistanceM;
var max = validPoints.Max(p => p.DistanceM);
return max > 0 ? max : DefaultMaxDistanceM;
}
}
private double Scale => SvgMaxRadius / MaxDistanceM;
private ScanStatistics? Statistics
{
get
{
var validPoints = ValidScanPoints;
if (validPoints.Count == 0)
return null;
return new ScanStatistics
{
AvgDistance = validPoints.Average(p => p.DistanceM),
MinDistance = validPoints.Min(p => p.DistanceM),
MaxDistance = validPoints.Max(p => p.DistanceM),
AvgIntensity = validPoints.Average(p => p.Intensity)
};
}
}
private List<(double X, double Y)> GridLines
{
get
{
var lines = new List<(double X, double Y)>();
for (int angle = 0; angle < 360; angle += GridLineStepDegrees)
{
var rad = angle * Math.PI / 180.0;
var x = SvgCenter + Math.Cos(rad) * SvgMaxRadius;
var y = SvgCenter + Math.Sin(rad) * SvgMaxRadius;
lines.Add((x, y));
}
return lines;
}
}
#endregion
#region Lifecycle Methods
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !string.IsNullOrEmpty(DeviceId))
{
await ConnectAsync();
}
await base.OnAfterRenderAsync(firstRender);
}
public async ValueTask DisposeAsync()
{
await StopAutoReloadAsync();
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
await DisconnectAsync();
}
#endregion
#region Connection Methods
private async Task ConnectAsync()
{
try
{
await LidarHubClient.StartAsync();
var deviceInfo = await LidarHubClient.GetDeviceInfoAsync(DeviceId);
DeviceName = deviceInfo?.DeviceName ?? DeviceId;
LidarData = await LidarHubClient.GetLidarDataAsync(DeviceId);
StateHasChanged();
}
catch (Exception ex)
{
Snackbar.Add($"Failed to connect xx: {ex.Message}", Severity.Error);
}
}
private async Task DisconnectAsync()
{
try
{
await LidarHubClient.StopAsync();
LidarData = new LaserScan();
StateHasChanged();
}
catch (Exception ex)
{
Snackbar.Add($"Failed to disconnect: {ex.Message}", Severity.Error);
}
}
private async Task ReloadLidarDataAsync()
{
if (!LidarHubClient.IsConnected || IsReloading)
return;
try
{
IsReloading = true;
StateHasChanged();
await InvokeAsync(async () =>
{
LidarData = await LidarHubClient.GetLidarDataAsync(DeviceId);
StateHasChanged();
IsReloading = false;
});
}
catch (Exception ex)
{
Snackbar.Add($"Failed to reload lidar data: {ex.Message}", Severity.Error);
IsReloading = false;
}
}
private async Task ToggleAutoReloadAsync()
{
if (AutoReloadEnabled)
{
await StopAutoReloadAsync();
}
else
{
await StartAutoReloadAsync();
}
}
private async Task StartAutoReloadAsync()
{
if (AutoReloadEnabled || !LidarHubClient.IsConnected)
return;
AutoReloadEnabled = true;
_autoReloadTimer = new System.Threading.PeriodicTimer(TimeSpan.FromSeconds(1));
_ = Task.Run(async () =>
{
try
{
while (await _autoReloadTimer.WaitForNextTickAsync(_cancellationTokenSource.Token))
{
if (!_cancellationTokenSource.Token.IsCancellationRequested && LidarHubClient.IsConnected)
{
await InvokeAsync(async () =>
{
await ReloadLidarDataAsync();
});
}
else
{
break;
}
}
}
catch (System.OperationCanceledException)
{
// Expected when cancelling
}
});
StateHasChanged();
}
private async Task StopAutoReloadAsync()
{
if (!AutoReloadEnabled)
return;
AutoReloadEnabled = false;
if (_autoReloadTimer != null)
{
_autoReloadTimer.Dispose();
_autoReloadTimer = null;
}
StateHasChanged();
}
#endregion
#region Helper Methods
private string BuildScanPath()
{
var validPoints = ValidScanPoints;
if (validPoints.Count == 0)
return string.Empty;
var pathBuilder = new System.Text.StringBuilder();
// Start from center
pathBuilder.Append($"M {SvgCenter:F2} {SvgCenter:F2}");
// Connect to all scan points
foreach (var point in validPoints)
{
var (x, y) = ConvertAngleToSvgCoordinates(point.AngleRad, point.DistanceM);
pathBuilder.Append($" L {x:F2} {y:F2}");
}
// Close path back to center
pathBuilder.Append($" L {SvgCenter:F2} {SvgCenter:F2}");
pathBuilder.Append(" Z");
return pathBuilder.ToString();
}
private (double x, double y) ConvertAngleToSvgCoordinates(double angleRad, double distanceM)
{
// Convert from SICK coordinate system to SVG coordinate system
// SICK: 0° = right, increases counter-clockwise
// SVG: 0° = up, increases clockwise
// Formula: x = center + cos(angle) * distance, y = center - sin(angle) * distance
var scaledDistance = distanceM * Scale;
var x = SvgCenter + Math.Cos(angleRad) * scaledDistance;
var y = SvgCenter - Math.Sin(angleRad) * scaledDistance;
return (x, y);
}
private string FormatAngleDegrees(double angleRad) => (angleRad * 180.0 / Math.PI).ToString("F1");
private string FormatAngleDegreesPrecise(double angleRad) => (angleRad * 180.0 / Math.PI).ToString("F3");
#endregion
#region Helper Classes
private class ScanPoint
{
public double AngleRad { get; set; }
public double AngleDeg { get; set; }
public double DistanceM { get; set; }
public double Intensity { get; set; }
}
#endregion
#region Helper Classes
private class ScanStatistics
{
public double AvgDistance { get; set; }
public double MinDistance { get; set; }
public double MaxDistance { get; set; }
public double AvgIntensity { get; set; }
}
#endregion
}