@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
@foreach (var radius in RangeIndicatorRadii) { } @foreach (var gridLine in GridLines) { } @if (ValidScanPoints.Count > 0) { var pathData = BuildScanPath(); }
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 }