988 lines
46 KiB
Plaintext
988 lines
46 KiB
Plaintext
@page "/motion/navigation-monitor"
|
|
@rendermode InteractiveWebAssemblyNoPrerender
|
|
@implements IAsyncDisposable
|
|
|
|
@using RobotNet10.RobotApp.Client.Clients
|
|
@using RobotNet10.RobotApp.Shared.NavigationMonitor
|
|
@using Microsoft.AspNetCore.SignalR.Client
|
|
@using MudBlazor
|
|
@using ApexCharts
|
|
@using Color = MudBlazor.Color
|
|
@using Size = MudBlazor.Size
|
|
|
|
<PageTitle>Navigation Monitor</PageTitle>
|
|
<div style="height: 100%; width:100%; display: flex; flex-direction: column; overflow-x: hidden; overflow: auto" class="p-2">
|
|
@* Connection status *@
|
|
@if (!hubClient.IsConnected)
|
|
{
|
|
<MudAlert Severity="Severity.Warning" Dense="true" Class="mb-2">
|
|
Disconnected from Navigation Monitor hub. Reconnecting...
|
|
</MudAlert>
|
|
}
|
|
|
|
@* Controls *@
|
|
<MudPaper Class="pa-3 mb-2" Elevation="1">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="4">
|
|
<MudSwitch Value="telemetryEnabled" Color="Color.Primary"
|
|
Label="Enable Telemetry" Disabled="@(!hubClient.IsConnected)"
|
|
ValueChanged="@((bool v) => OnTelemetryToggle(v))" T="bool" />
|
|
<MudSwitch Value="safetyStopEnabled" Color="Color.Error"
|
|
Label="Enable Safety Stop" Disabled="@(!hubClient.IsConnected)"
|
|
ValueChanged="@((bool v) => OnSafetyStopToggle(v))" T="bool" />
|
|
<MudSpacer />
|
|
@if (telemetryEnabled)
|
|
{
|
|
<MudChip T="string" Color="Color.Success" Size="Size.Small">@($"{updateFrequencyHz:F0} Hz")</MudChip>
|
|
}
|
|
else
|
|
{
|
|
<MudChip T="string" Color="Color.Default" Size="Size.Small">Disabled</MudChip>
|
|
}
|
|
</MudStack>
|
|
</MudPaper>
|
|
|
|
@* Safety Stop Latch Banner *@
|
|
@if (safetyStopLatched)
|
|
{
|
|
<MudAlert Severity="Severity.Error" Dense="false" Class="mb-2" NoIcon="false">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center">
|
|
<MudText Typo="Typo.body1">
|
|
<strong>SAFETY STOP ACTIVE</strong> — @safetyStopReason
|
|
</MudText>
|
|
<MudSpacer />
|
|
<MudButton Variant="Variant.Filled" Color="Color.Warning"
|
|
OnClick="OnReleaseSafetyStop"
|
|
Disabled="@(!hubClient.IsConnected)">
|
|
Release Safety Stop
|
|
</MudButton>
|
|
</MudStack>
|
|
</MudAlert>
|
|
}
|
|
|
|
@* DockTo Telemetry - special panel when docking *@
|
|
@if (telemetry.DockTo is not null)
|
|
{
|
|
<MudPaper Class="pa-3 mb-3" Elevation="2" Style="border-left: 4px solid var(--mud-palette-info);">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
|
|
<MudIcon Icon="@Icons.Material.Filled.GpsFixed" Color="Color.Info" />
|
|
<MudText Typo="Typo.h6" Color="Color.Info">Docking Telemetry</MudText>
|
|
<MudSpacer />
|
|
<MudChip T="string" Color="@GetDockPhaseColor()" Size="Size.Small">@telemetry.DockTo.Phase</MudChip>
|
|
<MudChip T="string" Color="Color.Default" Size="Size.Small" Variant="Variant.Outlined">@telemetry.DockTo.Direction</MudChip>
|
|
</MudStack>
|
|
@{
|
|
var dock = telemetry.DockTo!;
|
|
var svgBounds = GetDockSvgBounds(dock, telemetry.X, telemetry.Y);
|
|
var vb = svgBounds;
|
|
}
|
|
<MudGrid>
|
|
@* SVG Visualization - left side *@
|
|
<MudItem xs="12" md="7">
|
|
<MudText Typo="Typo.subtitle2" Class="mb-1">Docking Path</MudText>
|
|
<div style="background: #1e1e2e; border-radius: 4px; padding: 8px; height: 100%;">
|
|
<svg viewBox="@FormatViewBox(vb)"
|
|
width="100%" height="280" preserveAspectRatio="xMidYMid meet"
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
|
|
@* Grid reference lines *@
|
|
<line x1="@vb.MinX.ToString("F3")" y1="0" x2="@((vb.MinX + vb.Width).ToString("F3"))" y2="0"
|
|
stroke="#444" stroke-width="@((vb.Width * 0.003).ToString("F4"))" stroke-dasharray="@((vb.Width * 0.01).ToString("F4"))" />
|
|
<line x1="0" y1="@vb.MinY.ToString("F3")" x2="0" y2="@((vb.MinY + vb.Height).ToString("F3"))"
|
|
stroke="#444" stroke-width="@((vb.Width * 0.003).ToString("F4"))" stroke-dasharray="@((vb.Width * 0.01).ToString("F4"))" />
|
|
|
|
@* Waypoints path *@
|
|
@if (dock.Waypoints.Count >= 2)
|
|
{
|
|
var pathPoints = string.Join(" ", dock.Waypoints.Select(w => $"{w.X.ToString("F3")},{FlipY(w.Y).ToString("F3")}"));
|
|
<polyline points="@pathPoints"
|
|
fill="none" stroke="#5b9bd5" stroke-width="@((vb.Width * 0.008).ToString("F4"))"
|
|
stroke-dasharray="@((vb.Width * 0.015).ToString("F4"))" stroke-linecap="round" opacity="0.8" />
|
|
}
|
|
|
|
@* Distance to goal line *@
|
|
<line x1="@telemetry.X.ToString("F3")" y1="@FlipY(telemetry.Y).ToString("F3")"
|
|
x2="@dock.GoalX.ToString("F3")" y2="@FlipY(dock.GoalY).ToString("F3")"
|
|
stroke="#888" stroke-width="@((vb.Width * 0.003).ToString("F4"))"
|
|
stroke-dasharray="@((vb.Width * 0.008).ToString("F4"))" opacity="0.5" />
|
|
|
|
@* Start Node *@
|
|
<circle cx="@dock.StartX.ToString("F3")" cy="@FlipY(dock.StartY).ToString("F3")"
|
|
r="@((vb.Width * 0.025).ToString("F4"))" fill="#4caf50" stroke="#fff" stroke-width="@((vb.Width * 0.005).ToString("F4"))" />
|
|
<text x="@dock.StartX.ToString("F3")" y="@((FlipY(dock.StartY) - vb.Width * 0.04).ToString("F3"))"
|
|
text-anchor="middle" fill="#4caf50" font-size="@((vb.Width * 0.04).ToString("F4"))" font-weight="bold">Start</text>
|
|
|
|
@* Goal Node with direction arrow *@
|
|
@{
|
|
var goalSvgY = FlipY(dock.GoalY);
|
|
var arrowLen = vb.Width * 0.06;
|
|
var goalArrowX = dock.GoalX + arrowLen * Math.Cos(dock.GoalTheta);
|
|
var goalArrowY = goalSvgY - arrowLen * Math.Sin(dock.GoalTheta);
|
|
}
|
|
<circle cx="@dock.GoalX.ToString("F3")" cy="@goalSvgY.ToString("F3")"
|
|
r="@((vb.Width * 0.025).ToString("F4"))" fill="#f44336" stroke="#fff" stroke-width="@((vb.Width * 0.005).ToString("F4"))" />
|
|
<line x1="@dock.GoalX.ToString("F3")" y1="@goalSvgY.ToString("F3")"
|
|
x2="@goalArrowX.ToString("F3")" y2="@goalArrowY.ToString("F3")"
|
|
stroke="#f44336" stroke-width="@((vb.Width * 0.008).ToString("F4"))" marker-end="url(#arrowGoal)" />
|
|
<text x="@dock.GoalX.ToString("F3")" y="@((goalSvgY - vb.Width * 0.04).ToString("F3"))"
|
|
text-anchor="middle" fill="#f44336" font-size="@((vb.Width * 0.04).ToString("F4"))" font-weight="bold">Goal</text>
|
|
|
|
@* Robot current position (triangle pointing in Theta direction) *@
|
|
@{
|
|
var rSvgY = FlipY(telemetry.Y);
|
|
var rSize = vb.Width * 0.03;
|
|
var rTheta = telemetry.Theta;
|
|
// Triangle vertices: tip forward, two rear corners
|
|
var tipX = telemetry.X + rSize * 1.5 * Math.Cos(rTheta);
|
|
var tipY = rSvgY - rSize * 1.5 * Math.Sin(rTheta);
|
|
var leftX = telemetry.X + rSize * Math.Cos(rTheta + 2.5);
|
|
var leftY = rSvgY - rSize * Math.Sin(rTheta + 2.5);
|
|
var rightX = telemetry.X + rSize * Math.Cos(rTheta - 2.5);
|
|
var rightY = rSvgY - rSize * Math.Sin(rTheta - 2.5);
|
|
}
|
|
<polygon points="@($"{tipX.ToString("F3")},{tipY.ToString("F3")} {leftX.ToString("F3")},{leftY.ToString("F3")} {rightX.ToString("F3")},{rightY.ToString("F3")}")"
|
|
fill="#2196f3" stroke="#fff" stroke-width="@((vb.Width * 0.004).ToString("F4"))" />
|
|
<text x="@telemetry.X.ToString("F3")" y="@((rSvgY - vb.Width * 0.045).ToString("F3"))"
|
|
text-anchor="middle" fill="#2196f3" font-size="@((vb.Width * 0.035).ToString("F4"))">Robot</text>
|
|
|
|
@* Arrow marker definition *@
|
|
<defs>
|
|
<marker id="arrowGoal" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
|
<polygon points="0 0, 10 3.5, 0 7" fill="#f44336" />
|
|
</marker>
|
|
</defs>
|
|
</svg>
|
|
</div>
|
|
</MudItem>
|
|
|
|
@* Docking Info - right side *@
|
|
<MudItem xs="12" md="5">
|
|
<MudSimpleTable Dense="true" Hover="true" Class="mb-3">
|
|
<tbody>
|
|
<tr>
|
|
<td>Phase</td>
|
|
<td class="text-right">
|
|
<MudChip T="string" Color="@GetDockPhaseColor()" Size="Size.Small">@telemetry.DockTo.Phase</MudChip>
|
|
</td>
|
|
</tr>
|
|
<tr><td>Direction</td><td class="text-right"><strong>@telemetry.DockTo.Direction</strong></td></tr>
|
|
<tr>
|
|
<td>Fine Positioning Retries</td>
|
|
<td class="text-right">
|
|
<MudText Color="@(telemetry.DockTo.RetryCount > 0 ? Color.Warning : Color.Default)">
|
|
<strong>@telemetry.DockTo.RetryCount / @telemetry.DockTo.MaxRetries</strong>
|
|
</MudText>
|
|
</td>
|
|
</tr>
|
|
<tr><td>Waypoints</td><td class="text-right"><strong>@telemetry.DockTo.TotalWaypoints</strong></td></tr>
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
<MudSimpleTable Dense="true" Hover="true">
|
|
<tbody>
|
|
<tr><td>Goal X</td><td class="text-right"><strong>@telemetry.DockTo.GoalX.ToString("F3") m</strong></td></tr>
|
|
<tr><td>Goal Y</td><td class="text-right"><strong>@telemetry.DockTo.GoalY.ToString("F3") m</strong></td></tr>
|
|
<tr><td>Goal Theta</td><td class="text-right"><strong>@((telemetry.DockTo.GoalTheta * 180 / Math.PI).ToString("F2"))°</strong></td></tr>
|
|
<tr>
|
|
<td>Distance to Goal</td>
|
|
<td class="text-right">
|
|
<MudText Color="@(telemetry.DistanceToGoal < 0.1 ? Color.Success : Color.Info)">
|
|
<strong>@telemetry.DistanceToGoal.ToString("F3") m</strong>
|
|
</MudText>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudPaper>
|
|
}
|
|
|
|
@* Telemetry Data Cards *@
|
|
<MudGrid>
|
|
@* Position *@
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Elevation="1" Style="height:180px">
|
|
<MudCardHeader Class="pb-0">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">Position</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudSimpleTable Dense="true" Hover="true">
|
|
<tbody>
|
|
<tr><td>X</td><td class="text-right"><strong>@telemetry.X.ToString("F3") m</strong></td></tr>
|
|
<tr><td>Y</td><td class="text-right"><strong>@telemetry.Y.ToString("F3") m</strong></td></tr>
|
|
<tr><td>Theta</td><td class="text-right"><strong>@((telemetry.Theta * 180 / Math.PI).ToString("F2"))°</strong></td></tr>
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Velocity *@
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Elevation="1" Style="height:180px">
|
|
<MudCardHeader Class="pb-0">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">Velocity</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudSimpleTable Dense="true" Hover="true">
|
|
<tbody>
|
|
<tr><td>Linear</td><td class="text-right"><strong>@telemetry.LinearVelocity.ToString("F3") m/s</strong></td></tr>
|
|
<tr><td>Angular</td><td class="text-right"><strong>@telemetry.AngularVelocity.ToString("F3") rad/s</strong></td></tr>
|
|
<tr><td>Confidence</td><td class="text-right"><strong>@telemetry.ModelConfidence.ToString("F2")</strong></td></tr>
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Navigation State *@
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Elevation="1" Style="height:180px">
|
|
<MudCardHeader Class="pb-0">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">Navigation</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudSimpleTable Dense="true" Hover="true">
|
|
<tbody>
|
|
<tr>
|
|
<td>State</td>
|
|
<td class="text-right">
|
|
<MudChip T="string" Color="@GetNavStateColor()" Size="Size.Small">@telemetry.NavigationState</MudChip>
|
|
</td>
|
|
</tr>
|
|
<tr><td>Driving</td><td class="text-right"><strong>@(telemetry.Driving ? "Yes" : "No")</strong></td></tr>
|
|
<tr><td>To Goal</td><td class="text-right"><strong>@telemetry.DistanceToGoal.ToString("F3") m</strong></td></tr>
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Tracking (CTE) *@
|
|
<MudItem xs="12" md="6">
|
|
<MudCard Elevation="1">
|
|
<MudCardHeader Class="pb-0">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">Tracking</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudSimpleTable Dense="true" Hover="true">
|
|
<tbody>
|
|
<tr>
|
|
<td>Cross-Track Error</td>
|
|
<td class="text-right">
|
|
<MudText Color="@(telemetry.CrossTrackError > safetyConfig.MaxCrossTrackError ? Color.Error : Color.Default)">
|
|
<strong>@telemetry.CrossTrackError.ToString("F3") m</strong>
|
|
</MudText>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Heading Error</td>
|
|
<td class="text-right">
|
|
<MudText Color="@(Math.Abs(telemetry.HeadingError) * 180 / Math.PI > safetyConfig.MaxHeadingError ? Color.Error : Color.Default)">
|
|
<strong>@((telemetry.HeadingError * 180 / Math.PI).ToString("F2"))°</strong>
|
|
</MudText>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Acceleration *@
|
|
<MudItem xs="12" md="6">
|
|
<MudCard Elevation="1">
|
|
<MudCardHeader Class="pb-0">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">Acceleration</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudSimpleTable Dense="true" Hover="true">
|
|
<tbody>
|
|
<tr>
|
|
<td>Linear</td>
|
|
<td class="text-right">
|
|
<MudText Color="@(Math.Abs(telemetry.LinearAcceleration) > safetyConfig.MaxLinearAcceleration ? Color.Warning : Color.Default)">
|
|
<strong>@telemetry.LinearAcceleration.ToString("F2") m/s²</strong>
|
|
</MudText>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Angular</td>
|
|
<td class="text-right">
|
|
<MudText Color="@(Math.Abs(telemetry.AngularAcceleration) > safetyConfig.MaxAngularVelocity ? Color.Warning : Color.Default)">
|
|
<strong>@telemetry.AngularAcceleration.ToString("F2") m/s²</strong>
|
|
</MudText>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
</MudGrid>
|
|
|
|
@* Telemetry Charts *@
|
|
<MudText Typo="Typo.h6" Class="mt-2 mb-2">Telemetry Charts</MudText>
|
|
<MudGrid>
|
|
<MudItem xs="12" md="4">
|
|
<MudText Typo="Typo.subtitle2" Class="mb-1">Cross-Track Error</MudText>
|
|
<ApexChart @ref="_cteChart" TItem="TelemetryPoint" Options="_cteOptions" Height="220">
|
|
<ApexPointSeries TItem="TelemetryPoint" Items="chartDataPoints"
|
|
SeriesType="SeriesType.Line" Name="CTE (m)"
|
|
XValue="@(p => (decimal)p.TimeSec)"
|
|
YValue="@(p => (decimal)p.Cte)"
|
|
OrderBy="p => p.X" />
|
|
</ApexChart>
|
|
</MudItem>
|
|
<MudItem xs="12" md="4">
|
|
<MudText Typo="Typo.subtitle2" Class="mb-1">Velocity</MudText>
|
|
<ApexChart @ref="_velocityChart" TItem="TelemetryPoint" Options="_velocityOptions" Height="220">
|
|
<ApexPointSeries TItem="TelemetryPoint" Items="chartDataPoints"
|
|
SeriesType="SeriesType.Line" Name="Linear (m/s)"
|
|
XValue="@(p => (decimal)p.TimeSec)"
|
|
YValue="@(p => (decimal)p.LinearVel)"
|
|
OrderBy="p => p.X" />
|
|
<ApexPointSeries TItem="TelemetryPoint" Items="chartDataPoints"
|
|
SeriesType="SeriesType.Line" Name="Angular (rad/s)"
|
|
XValue="@(p => (decimal)p.TimeSec)"
|
|
YValue="@(p => (decimal)p.AngularVel)"
|
|
OrderBy="p => p.X" />
|
|
</ApexChart>
|
|
</MudItem>
|
|
<MudItem xs="12" md="4">
|
|
<MudText Typo="Typo.subtitle2" Class="mb-1">Heading Error</MudText>
|
|
<ApexChart @ref="_headingChart" TItem="TelemetryPoint" Options="_headingOptions" Height="220">
|
|
<ApexPointSeries TItem="TelemetryPoint" Items="chartDataPoints"
|
|
SeriesType="SeriesType.Line" Name="Heading (deg)"
|
|
XValue="@(p => (decimal)p.TimeSec)"
|
|
YValue="@(p => (decimal)p.HeadingErrorDeg)"
|
|
OrderBy="p => p.X" />
|
|
</ApexChart>
|
|
</MudItem>
|
|
</MudGrid>
|
|
|
|
@* Navigation Path Visualization *@
|
|
@if (_cachedWaypoints is { Count: >= 2 })
|
|
{
|
|
<MudPaper Class="pa-3 mb-3 mt-2" Elevation="2" Style="border-left: 4px solid var(--mud-palette-primary);">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
|
|
<MudIcon Icon="@Icons.Material.Filled.Route" Color="Color.Primary" />
|
|
<MudText Typo="Typo.h6" Color="Color.Primary">Navigation Path</MudText>
|
|
<MudSpacer />
|
|
<MudChip T="string" Color="Color.Info" Size="Size.Small">@_cachedWaypoints.Count waypoints</MudChip>
|
|
<MudChip T="string" Color="Color.Default" Size="Size.Small" Variant="Variant.Outlined">@telemetry.NavigationState</MudChip>
|
|
</MudStack>
|
|
@{
|
|
var wp = _cachedWaypoints;
|
|
var pvb = GetPathSvgBounds(wp, _robotTrail);
|
|
var pStroke = (pvb.Width * 0.005).ToString("F4");
|
|
var pThin = (pvb.Width * 0.003).ToString("F4");
|
|
var pDash = (pvb.Width * 0.01).ToString("F4");
|
|
}
|
|
<div style="background: #1e1e2e; border-radius: 4px; padding: 8px;">
|
|
<svg viewBox="@FormatViewBox(pvb)"
|
|
width="100%" height="350" preserveAspectRatio="xMidYMid meet"
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
|
|
@* Grid reference lines *@
|
|
<line x1="@pvb.MinX.ToString("F3")" y1="0" x2="@((pvb.MinX + pvb.Width).ToString("F3"))" y2="0"
|
|
stroke="#444" stroke-width="@pThin" stroke-dasharray="@pDash" />
|
|
<line x1="0" y1="@pvb.MinY.ToString("F3")" x2="0" y2="@((pvb.MinY + pvb.Height).ToString("F3"))"
|
|
stroke="#444" stroke-width="@pThin" stroke-dasharray="@pDash" />
|
|
|
|
@* Waypoints path line *@
|
|
@{
|
|
var pathLine = string.Join(" ", wp.Select(w => $"{w.X.ToString("F3")},{FlipY(w.Y).ToString("F3")}"));
|
|
}
|
|
<polyline points="@pathLine"
|
|
fill="none" stroke="#5b9bd5" stroke-width="@pStroke"
|
|
stroke-linecap="round" stroke-linejoin="round" opacity="0.7" />
|
|
|
|
@* Waypoint dots *@
|
|
@for (int wi = 0; wi < wp.Count; wi++)
|
|
{
|
|
var wpt = wp[wi];
|
|
var wColor = wpt.Direction == "BACKWARD" ? "#ff9800" : "#5b9bd5";
|
|
var wRadius = (pvb.Width * 0.008).ToString("F4");
|
|
<circle cx="@wpt.X.ToString("F3")" cy="@FlipY(wpt.Y).ToString("F3")"
|
|
r="@wRadius" fill="@wColor" opacity="0.8" />
|
|
}
|
|
|
|
@* Start waypoint *@
|
|
@{
|
|
var wpStart = wp[0];
|
|
var startR = (pvb.Width * 0.02).ToString("F4");
|
|
}
|
|
<circle cx="@wpStart.X.ToString("F3")" cy="@FlipY(wpStart.Y).ToString("F3")"
|
|
r="@startR" fill="#4caf50" stroke="#fff"
|
|
stroke-width="@((pvb.Width * 0.004).ToString("F4"))" />
|
|
<text x="@wpStart.X.ToString("F3")"
|
|
y="@((FlipY(wpStart.Y) - pvb.Width * 0.035).ToString("F3"))"
|
|
text-anchor="middle" fill="#4caf50"
|
|
font-size="@((pvb.Width * 0.035).ToString("F4"))" font-weight="bold">Start</text>
|
|
|
|
@* Goal waypoint *@
|
|
@{
|
|
var wpGoal = wp[^1];
|
|
var goalR = (pvb.Width * 0.02).ToString("F4");
|
|
}
|
|
<circle cx="@wpGoal.X.ToString("F3")" cy="@FlipY(wpGoal.Y).ToString("F3")"
|
|
r="@goalR" fill="#f44336" stroke="#fff"
|
|
stroke-width="@((pvb.Width * 0.004).ToString("F4"))" />
|
|
<text x="@wpGoal.X.ToString("F3")"
|
|
y="@((FlipY(wpGoal.Y) - pvb.Width * 0.035).ToString("F3"))"
|
|
text-anchor="middle" fill="#f44336"
|
|
font-size="@((pvb.Width * 0.035).ToString("F4"))" font-weight="bold">Goal</text>
|
|
|
|
@* Robot trail (breadcrumb) *@
|
|
@if (_robotTrail.Count >= 2)
|
|
{
|
|
var trailLine = string.Join(" ", _robotTrail.Select(t => $"{t.X.ToString("F3")},{FlipY(t.Y).ToString("F3")}"));
|
|
<polyline points="@trailLine"
|
|
fill="none" stroke="#66bb6a" stroke-width="@pThin"
|
|
stroke-linecap="round" stroke-linejoin="round" opacity="0.6"
|
|
stroke-dasharray="@((pvb.Width * 0.008).ToString("F4"))" />
|
|
}
|
|
|
|
@* Legend *@
|
|
@{
|
|
var legendX = pvb.MinX + pvb.Width * 0.02;
|
|
var legendY = pvb.MinY + pvb.Height * 0.06;
|
|
var legendFs = (pvb.Width * 0.025).ToString("F4");
|
|
var legendR = (pvb.Width * 0.008).ToString("F4");
|
|
var legendStep = pvb.Height * 0.05;
|
|
}
|
|
<circle cx="@legendX.ToString("F3")" cy="@legendY.ToString("F3")" r="@legendR" fill="#5b9bd5" />
|
|
<text x="@((legendX + pvb.Width * 0.02).ToString("F3"))" y="@((legendY + pvb.Height * 0.012).ToString("F3"))"
|
|
fill="#aaa" font-size="@legendFs">Forward</text>
|
|
<circle cx="@legendX.ToString("F3")" cy="@((legendY + legendStep).ToString("F3"))" r="@legendR" fill="#ff9800" />
|
|
<text x="@((legendX + pvb.Width * 0.02).ToString("F3"))" y="@((legendY + legendStep + pvb.Height * 0.012).ToString("F3"))"
|
|
fill="#aaa" font-size="@legendFs">Backward</text>
|
|
<line x1="@legendX.ToString("F3")" y1="@((legendY + 2 * legendStep).ToString("F3"))"
|
|
x2="@((legendX + pvb.Width * 0.03).ToString("F3"))" y2="@((legendY + 2 * legendStep).ToString("F3"))"
|
|
stroke="#66bb6a" stroke-width="@pThin" stroke-dasharray="@((pvb.Width * 0.008).ToString("F4"))" />
|
|
<text x="@((legendX + pvb.Width * 0.04).ToString("F3"))" y="@((legendY + 2 * legendStep + pvb.Height * 0.012).ToString("F3"))"
|
|
fill="#aaa" font-size="@legendFs">Robot Trail</text>
|
|
</svg>
|
|
</div>
|
|
</MudPaper>
|
|
}
|
|
|
|
@* Safety Configuration *@
|
|
<MudExpansionPanels Class="mt-3">
|
|
<MudExpansionPanel>
|
|
<TitleContent>
|
|
<div class="d-flex flex-row">
|
|
<MudText>Safety Configuration</MudText>
|
|
<MudButton Class="ms-4" Variant="Variant.Text" Color="Color.Primary" OnClick="ApplySafetyConfig"
|
|
Disabled="@(!hubClient.IsConnected)" Size="Size.Small">Apply</MudButton>
|
|
</div>
|
|
</TitleContent>
|
|
<ChildContent>
|
|
<MudGrid>
|
|
<MudItem xs="12" sm="6" md="4">
|
|
<MudNumericField @bind-Value="safetyConfig.MaxLinearVelocity" Label="Max Linear Velocity (m/s)"
|
|
Step="0.1" Min="0.1" Max="5.0" Format="F2" Variant="Variant.Outlined" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" md="4">
|
|
<MudNumericField @bind-Value="safetyConfig.MaxAngularVelocity" Label="Max Angular Velocity (rad/s)"
|
|
Step="0.5" Min="0.5" Max="15.0" Format="F2" Variant="Variant.Outlined" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" md="4">
|
|
<MudNumericField @bind-Value="safetyConfig.MaxLinearAcceleration" Label="Max Linear Accel (m/s2)"
|
|
Step="0.5" Min="0.5" Max="10.0" Format="F2" Variant="Variant.Outlined" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" md="4">
|
|
<MudNumericField @bind-Value="safetyConfig.MaxCrossTrackError" Label="Max CTE (m)"
|
|
Step="0.05" Min="0.05" Max="2.0" Format="F2" Variant="Variant.Outlined" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" md="4">
|
|
<MudNumericField @bind-Value="safetyConfig.MaxHeadingError" Label="Max Heading Error (deg)"
|
|
Step="5.0" Min="5.0" Max="180.0" Format="F1" Variant="Variant.Outlined" />
|
|
</MudItem>
|
|
</MudGrid>
|
|
</ChildContent>
|
|
</MudExpansionPanel>
|
|
</MudExpansionPanels>
|
|
|
|
@* Safety Violations *@
|
|
<MudPaper Class="pa-3 mt-3" Elevation="1">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
|
|
<MudText Typo="Typo.h6">Safety Violations</MudText>
|
|
<MudSpacer />
|
|
<MudButton Variant="Variant.Text" Size="Size.Small" OnClick="ClearViolations">Clear</MudButton>
|
|
</MudStack>
|
|
|
|
@if (violations.Count == 0)
|
|
{
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">No violations recorded.</MudText>
|
|
}
|
|
else
|
|
{
|
|
<div style="max-height: 300px; overflow-y: auto;">
|
|
@foreach (var v in violations)
|
|
{
|
|
<MudAlert Severity="@(v.Severity == SafetyViolationSeverity.Critical ? Severity.Error : Severity.Warning)"
|
|
Dense="true" Class="mb-1" NoIcon="false">
|
|
<MudText Typo="Typo.caption">
|
|
@DateTimeOffset.FromUnixTimeMilliseconds(v.TimestampMs).ToLocalTime().ToString("HH:mm:ss.fff")
|
|
— @v.Message
|
|
</MudText>
|
|
</MudAlert>
|
|
}
|
|
</div>
|
|
}
|
|
</MudPaper>
|
|
|
|
</div>
|
|
|
|
<MudThemeProvider IsDarkMode />
|
|
<MudPopoverProvider />
|
|
<MudDialogProvider />
|
|
<MudSnackbarProvider />
|
|
|
|
@code {
|
|
[Inject] private NavigationMonitorHubClient hubClient { get; set; } = null!;
|
|
[Inject] private ISnackbar Snackbar { get; set; } = null!;
|
|
|
|
private NavigationTelemetryDto telemetry = new();
|
|
private NavigationSafetyConfigDto safetyConfig = new();
|
|
private List<NavigationSafetyViolationDto> violations = new();
|
|
private bool telemetryEnabled;
|
|
private bool safetyStopEnabled;
|
|
private bool safetyStopLatched;
|
|
private string safetyStopReason = "";
|
|
private double updateFrequencyHz = 10;
|
|
private const int MaxViolations = 100;
|
|
|
|
// UI render throttle: render at 2Hz
|
|
private const int UiRefreshIntervalMs = 500;
|
|
private System.Threading.Timer? _uiRefreshTimer;
|
|
private volatile bool _telemetryDirty;
|
|
|
|
// Chart data & ApexCharts
|
|
private List<TelemetryPoint> chartDataPoints = new();
|
|
private const int MaxChartPoints = 120;
|
|
private long chartStartTimeMs;
|
|
private volatile bool _chartDirty;
|
|
|
|
// Path visualization: cached waypoints persist after navigation ends
|
|
private List<WaypointDto> _cachedWaypoints = new();
|
|
private List<(double X, double Y)> _robotTrail = new();
|
|
private const int MaxTrailPoints = 120;
|
|
|
|
private ApexChart<TelemetryPoint>? _cteChart;
|
|
private ApexChart<TelemetryPoint>? _velocityChart;
|
|
private ApexChart<TelemetryPoint>? _headingChart;
|
|
|
|
private ApexChartOptions<TelemetryPoint> _cteOptions = new();
|
|
private ApexChartOptions<TelemetryPoint> _velocityOptions = new();
|
|
private ApexChartOptions<TelemetryPoint> _headingOptions = new();
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
ConfigureChartOptions();
|
|
|
|
hubClient.TelemetryReceived += OnTelemetryReceived;
|
|
hubClient.SafetyViolationReceived += OnSafetyViolationReceived;
|
|
hubClient.MonitorStateChanged += OnMonitorStateChanged;
|
|
hubClient.ConnectionStateChanged += OnConnectionStateChanged;
|
|
|
|
_uiRefreshTimer = new System.Threading.Timer(OnUiRefreshTick, null, UiRefreshIntervalMs, UiRefreshIntervalMs);
|
|
|
|
try
|
|
{
|
|
await hubClient.StartAsync();
|
|
var state = await hubClient.GetStateAsync();
|
|
ApplyMonitorState(state);
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to connect: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnTelemetryReceived(NavigationTelemetryDto dto)
|
|
{
|
|
telemetry = dto;
|
|
|
|
// Cache waypoints: update when new path arrives, persist after navigation ends
|
|
if (dto.Waypoints is { Count: >= 2 })
|
|
{
|
|
// New waypoints = new navigation task → reset trail
|
|
if (!WaypointsMatch(_cachedWaypoints, dto.Waypoints))
|
|
{
|
|
_cachedWaypoints = dto.Waypoints;
|
|
_robotTrail.Clear();
|
|
}
|
|
}
|
|
|
|
// Track robot trail (keep trail after navigation ends for visualization)
|
|
if (dto.Driving)
|
|
{
|
|
_robotTrail.Add((dto.X, dto.Y));
|
|
if (_robotTrail.Count > MaxTrailPoints)
|
|
_robotTrail.RemoveAt(0);
|
|
}
|
|
|
|
// Add chart data point every telemetry tick (now 2Hz from server)
|
|
if (chartStartTimeMs == 0) chartStartTimeMs = dto.TimestampMs;
|
|
chartDataPoints.Add(new TelemetryPoint
|
|
{
|
|
TimeSec = (dto.TimestampMs - chartStartTimeMs) / 1000.0,
|
|
Cte = dto.CrossTrackError,
|
|
LinearVel = dto.LinearVelocity,
|
|
AngularVel = dto.AngularVelocity,
|
|
HeadingErrorDeg = dto.HeadingError * 180 / Math.PI
|
|
});
|
|
if (chartDataPoints.Count > MaxChartPoints)
|
|
chartDataPoints.RemoveRange(0, chartDataPoints.Count - MaxChartPoints);
|
|
|
|
_telemetryDirty = true;
|
|
_chartDirty = true;
|
|
}
|
|
|
|
private void OnUiRefreshTick(object? state)
|
|
{
|
|
if (!_telemetryDirty) return;
|
|
_telemetryDirty = false;
|
|
|
|
InvokeAsync(async () =>
|
|
{
|
|
if (_chartDirty)
|
|
{
|
|
_chartDirty = false;
|
|
try
|
|
{
|
|
if (_cteChart is not null) await _cteChart.UpdateSeriesAsync(true);
|
|
if (_velocityChart is not null) await _velocityChart.UpdateSeriesAsync(true);
|
|
if (_headingChart is not null) await _headingChart.UpdateSeriesAsync(true);
|
|
}
|
|
catch (ObjectDisposedException) { }
|
|
}
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
|
|
private void OnSafetyViolationReceived(NavigationSafetyViolationDto dto)
|
|
{
|
|
violations.Insert(0, dto);
|
|
if (violations.Count > MaxViolations)
|
|
violations.RemoveRange(MaxViolations, violations.Count - MaxViolations);
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private void OnMonitorStateChanged(NavigationMonitorStateDto state)
|
|
{
|
|
ApplyMonitorState(state);
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private void OnConnectionStateChanged(HubConnectionState state)
|
|
{
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private void ApplyMonitorState(NavigationMonitorStateDto state)
|
|
{
|
|
telemetryEnabled = state.TelemetryEnabled;
|
|
safetyStopEnabled = state.SafetyStopEnabled;
|
|
safetyStopLatched = state.SafetyStopLatched;
|
|
safetyStopReason = state.SafetyStopReason;
|
|
updateFrequencyHz = state.UpdateFrequencyHz;
|
|
safetyConfig = state.SafetyConfig;
|
|
}
|
|
|
|
private async Task OnTelemetryToggle(bool enabled)
|
|
{
|
|
try
|
|
{
|
|
telemetryEnabled = enabled;
|
|
if (enabled)
|
|
{
|
|
// Reset chart history when re-enabling
|
|
chartDataPoints.Clear();
|
|
chartStartTimeMs = 0;
|
|
_cachedWaypoints.Clear();
|
|
_robotTrail.Clear();
|
|
}
|
|
await hubClient.SetTelemetryEnabledAsync(enabled);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task OnSafetyStopToggle(bool enabled)
|
|
{
|
|
try
|
|
{
|
|
safetyStopEnabled = enabled;
|
|
await hubClient.SetSafetyStopEnabledAsync(enabled);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task OnReleaseSafetyStop()
|
|
{
|
|
try
|
|
{
|
|
await hubClient.ReleaseSafetyStopAsync();
|
|
Snackbar.Add("Safety stop released", Severity.Info);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task ApplySafetyConfig()
|
|
{
|
|
try
|
|
{
|
|
await hubClient.UpdateSafetyConfigAsync(safetyConfig);
|
|
Snackbar.Add("Safety config updated", Severity.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private void ClearViolations()
|
|
{
|
|
violations.Clear();
|
|
}
|
|
|
|
private MudBlazor.Color GetNavStateColor() => telemetry.NavigationState switch
|
|
{
|
|
"Moving" or "Rotating" => MudBlazor.Color.Info,
|
|
"Docking" or "FinePositioning" => MudBlazor.Color.Tertiary,
|
|
"Completed" => MudBlazor.Color.Success,
|
|
"Error" or "Canceled" or "SafetyStop" => MudBlazor.Color.Error,
|
|
"Paused" => MudBlazor.Color.Warning,
|
|
_ => MudBlazor.Color.Default
|
|
};
|
|
|
|
private MudBlazor.Color GetDockPhaseColor() => telemetry.DockTo?.Phase switch
|
|
{
|
|
"Approaching" => MudBlazor.Color.Info,
|
|
"Aligning" => MudBlazor.Color.Warning,
|
|
"Advancing" => MudBlazor.Color.Success,
|
|
_ => MudBlazor.Color.Default
|
|
};
|
|
|
|
// SVG helpers for docking visualization
|
|
private record struct SvgBounds(double MinX, double MinY, double Width, double Height);
|
|
|
|
private SvgBounds GetDockSvgBounds(DockToTelemetryDto dock, double robotX, double robotY)
|
|
{
|
|
var xs = new List<double> { dock.StartX, dock.GoalX, robotX };
|
|
var ys = new List<double> { dock.StartY, dock.GoalY, robotY };
|
|
foreach (var w in dock.Waypoints) { xs.Add(w.X); ys.Add(w.Y); }
|
|
|
|
double minX = xs.Min(), maxX = xs.Max();
|
|
double minY = ys.Min(), maxY = ys.Max();
|
|
|
|
// Ensure minimum size and add padding
|
|
double rangeX = maxX - minX;
|
|
double rangeY = maxY - minY;
|
|
if (rangeX < 0.5) { minX -= 0.25; rangeX = 0.5; maxX = minX + rangeX; }
|
|
if (rangeY < 0.5) { minY -= 0.25; rangeY = 0.5; maxY = minY + rangeY; }
|
|
|
|
double pad = Math.Max(rangeX, rangeY) * 0.15;
|
|
// Flip Y: SVG Y-down, world Y-up. We flip individual Y coords with FlipY(),
|
|
// so viewBox uses flipped min/max
|
|
double svgMinX = minX - pad;
|
|
double svgMinY = -(maxY + pad); // flipped
|
|
double svgW = rangeX + 2 * pad;
|
|
double svgH = rangeY + 2 * pad;
|
|
|
|
return new SvgBounds(svgMinX, svgMinY, svgW, svgH);
|
|
}
|
|
|
|
private static string FormatViewBox(SvgBounds vb)
|
|
=> $"{vb.MinX.ToString("F3")} {vb.MinY.ToString("F3")} {vb.Width.ToString("F3")} {vb.Height.ToString("F3")}";
|
|
|
|
private static double FlipY(double worldY) => -worldY;
|
|
|
|
// SVG helpers for navigation path visualization
|
|
private static SvgBounds GetPathSvgBounds(List<WaypointDto> waypoints, List<(double X, double Y)> trail)
|
|
{
|
|
var xs = waypoints.Select(w => w.X).ToList();
|
|
var ys = waypoints.Select(w => w.Y).ToList();
|
|
foreach (var t in trail) { xs.Add(t.X); ys.Add(t.Y); }
|
|
|
|
double minX = xs.Min(), maxX = xs.Max();
|
|
double minY = ys.Min(), maxY = ys.Max();
|
|
|
|
double rangeX = maxX - minX;
|
|
double rangeY = maxY - minY;
|
|
if (rangeX < 0.5) { minX -= 0.25; rangeX = 0.5; maxX = minX + rangeX; }
|
|
if (rangeY < 0.5) { minY -= 0.25; rangeY = 0.5; maxY = minY + rangeY; }
|
|
|
|
double pad = Math.Max(rangeX, rangeY) * 0.15;
|
|
double svgMinX = minX - pad;
|
|
double svgMinY = -(maxY + pad);
|
|
double svgW = rangeX + 2 * pad;
|
|
double svgH = rangeY + 2 * pad;
|
|
|
|
return new SvgBounds(svgMinX, svgMinY, svgW, svgH);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Quick check if waypoints are the same path (compare first/last + count)
|
|
/// </summary>
|
|
private static bool WaypointsMatch(List<WaypointDto> a, List<WaypointDto> b)
|
|
{
|
|
if (a.Count != b.Count || a.Count == 0) return false;
|
|
return Math.Abs(a[0].X - b[0].X) < 0.001
|
|
&& Math.Abs(a[0].Y - b[0].Y) < 0.001
|
|
&& Math.Abs(a[^1].X - b[^1].X) < 0.001
|
|
&& Math.Abs(a[^1].Y - b[^1].Y) < 0.001;
|
|
}
|
|
|
|
// ApexCharts configuration (following TelemetryChartPanel pattern)
|
|
private void ConfigureChartOptions()
|
|
{
|
|
var axisColor = "#009933";
|
|
|
|
var baseGrid = () => new Grid
|
|
{
|
|
BorderColor = "#00993330",
|
|
StrokeDashArray = 4,
|
|
Xaxis = new GridXAxis { Lines = new Lines { Show = false } },
|
|
Yaxis = new GridYAxis { Lines = new Lines { Show = true } }
|
|
};
|
|
|
|
var baseChart = () => new Chart
|
|
{
|
|
ForeColor = axisColor,
|
|
Animations = new Animations { Enabled = false },
|
|
Toolbar = new Toolbar { Show = false },
|
|
Zoom = new Zoom { Enabled = false }
|
|
};
|
|
|
|
var baseXAxis = () => new XAxis
|
|
{
|
|
Title = new AxisTitle { Text = "Time (s)", Style = new AxisTitleStyle { Color = axisColor } },
|
|
Labels = new XAxisLabels
|
|
{
|
|
Formatter = @"function(val) { return parseFloat(val).toFixed(0); }",
|
|
Style = new AxisLabelStyle { Colors = axisColor }
|
|
}
|
|
};
|
|
|
|
var baseTooltip = () => new Tooltip
|
|
{
|
|
Shared = true,
|
|
X = new TooltipX { Format = "0.1f" },
|
|
Y = new TooltipY { Formatter = @"function(val) { return val.toFixed(4); }" }
|
|
};
|
|
|
|
var yAxisStyle = new AxisTitleStyle { Color = axisColor };
|
|
var yLabelStyle = new AxisLabelStyle { Colors = axisColor };
|
|
|
|
// CTE chart — blue
|
|
_cteOptions.Chart = baseChart();
|
|
_cteOptions.Grid = baseGrid();
|
|
_cteOptions.Stroke = new Stroke { Curve = Curve.Smooth, Width = 3 };
|
|
_cteOptions.Xaxis = baseXAxis();
|
|
_cteOptions.Colors = new List<string> { "#0D47A1" };
|
|
_cteOptions.Tooltip = baseTooltip();
|
|
_cteOptions.Yaxis = new List<YAxis>
|
|
{
|
|
new YAxis
|
|
{
|
|
Title = new AxisTitle { Text = "CTE (m)", Style = yAxisStyle },
|
|
Labels = new YAxisLabels
|
|
{
|
|
Formatter = @"function(val) { return val.toFixed(3); }",
|
|
Style = yLabelStyle
|
|
},
|
|
DecimalsInFloat = 3,
|
|
Min = 0
|
|
}
|
|
};
|
|
|
|
// Velocity chart — green (linear) + purple (angular)
|
|
_velocityOptions.Chart = baseChart();
|
|
_velocityOptions.Grid = baseGrid();
|
|
_velocityOptions.Stroke = new Stroke { Curve = Curve.Smooth, Width = 3 };
|
|
_velocityOptions.Xaxis = baseXAxis();
|
|
_velocityOptions.Colors = new List<string> { "#1B5E20", "#7B1FA2" };
|
|
_velocityOptions.Tooltip = baseTooltip();
|
|
_velocityOptions.Yaxis = new List<YAxis>
|
|
{
|
|
new YAxis
|
|
{
|
|
Title = new AxisTitle { Text = "Velocity", Style = yAxisStyle },
|
|
Labels = new YAxisLabels
|
|
{
|
|
Formatter = @"function(val) { return val.toFixed(3); }",
|
|
Style = yLabelStyle
|
|
},
|
|
DecimalsInFloat = 3
|
|
}
|
|
};
|
|
|
|
// Heading error chart — orange
|
|
_headingOptions.Chart = baseChart();
|
|
_headingOptions.Grid = baseGrid();
|
|
_headingOptions.Stroke = new Stroke { Curve = Curve.Smooth, Width = 3 };
|
|
_headingOptions.Xaxis = baseXAxis();
|
|
_headingOptions.Colors = new List<string> { "#E65100" };
|
|
_headingOptions.Tooltip = baseTooltip();
|
|
_headingOptions.Yaxis = new List<YAxis>
|
|
{
|
|
new YAxis
|
|
{
|
|
Title = new AxisTitle { Text = "Heading (deg)", Style = yAxisStyle },
|
|
Labels = new YAxisLabels
|
|
{
|
|
Formatter = @"function(val) { return val.toFixed(2); }",
|
|
Style = yLabelStyle
|
|
},
|
|
DecimalsInFloat = 2
|
|
}
|
|
};
|
|
}
|
|
|
|
private class TelemetryPoint
|
|
{
|
|
public double TimeSec { get; set; }
|
|
public double Cte { get; set; }
|
|
public double LinearVel { get; set; }
|
|
public double AngularVel { get; set; }
|
|
public double HeadingErrorDeg { get; set; }
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_uiRefreshTimer is not null)
|
|
await _uiRefreshTimer.DisposeAsync();
|
|
hubClient.TelemetryReceived -= OnTelemetryReceived;
|
|
hubClient.SafetyViolationReceived -= OnSafetyViolationReceived;
|
|
hubClient.MonitorStateChanged -= OnMonitorStateChanged;
|
|
hubClient.ConnectionStateChanged -= OnConnectionStateChanged;
|
|
await hubClient.DisposeAsync();
|
|
}
|
|
}
|