508 lines
26 KiB
Plaintext
508 lines
26 KiB
Plaintext
@using MudBlazor
|
|
@using Microsoft.AspNetCore.SignalR.Client
|
|
@using RobotNet10.RobotApp.Client.Clients
|
|
@using RobotNet10.RobotApp.Client.Shared.Devices
|
|
@using RobotNet10.Shared.Sensor
|
|
@using RobotNet10.Shared.Geometry
|
|
@using RobotNet10.Shared.Numbers
|
|
@using QuaternionGeometry = RobotNet10.Shared.Geometry.Quaternion
|
|
@using QuaternionNumbers = RobotNet10.Shared.Numbers.Quaternion
|
|
@implements IAsyncDisposable
|
|
@inject InertialMeasurementUnitHubClient ImuHubClient
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudCard>
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h5">@DeviceName</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">@DeviceId</MudText>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
<MudTooltip Text="Reload IMU data">
|
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
|
Color="Color.Primary"
|
|
Size="Size.Small"
|
|
OnClick="ReloadImuDataAsync"
|
|
Disabled="@(!ImuHubClient.IsConnected || IsReloading)">
|
|
</MudIconButton>
|
|
</MudTooltip>
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
<MudCardContent Style="position: relative;">
|
|
<MudGrid>
|
|
<!-- Left Column: IMU SVG với 3D visualization -->
|
|
<MudItem xs="12" md="4">
|
|
<div class="d-flex flex-column align-center justify-center" style="height: 100%; width: 100%; position: relative;">
|
|
<svg width="100%" height="100%" viewBox="0 0 300 300" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" style="max-height: 100%;">
|
|
<defs>
|
|
<!-- Gradients cho các trục -->
|
|
<linearGradient id="@($"xAxisGradient_{DeviceId}")" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" style="stop-color:#f44336;stop-opacity:0.8" />
|
|
<stop offset="100%" style="stop-color:#f44336;stop-opacity:0.4" />
|
|
</linearGradient>
|
|
<linearGradient id="@($"yAxisGradient_{DeviceId}")" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#4caf50;stop-opacity:0.8" />
|
|
<stop offset="100%" style="stop-color:#4caf50;stop-opacity:0.4" />
|
|
</linearGradient>
|
|
<linearGradient id="@($"zAxisGradient_{DeviceId}")" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#2196f3;stop-opacity:0.8" />
|
|
<stop offset="100%" style="stop-color:#2196f3;stop-opacity:0.4" />
|
|
</linearGradient>
|
|
<!-- Filter cho glow effect -->
|
|
<filter id="@($"glow_{DeviceId}")">
|
|
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
|
<feMerge>
|
|
<feMergeNode in="coloredBlur"/>
|
|
<feMergeNode in="SourceGraphic"/>
|
|
</feMerge>
|
|
</filter>
|
|
<!-- Arrow markers -->
|
|
<marker id="@($"arrowhead-red-{DeviceId}")" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
|
<polygon points="0 0, 10 3, 0 6" fill="#f44336" />
|
|
</marker>
|
|
<marker id="@($"arrowhead-green-{DeviceId}")" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
|
<polygon points="0 0, 10 3, 0 6" fill="#4caf50" />
|
|
</marker>
|
|
<marker id="@($"arrowhead-blue-{DeviceId}")" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
|
<polygon points="0 0, 10 3, 0 6" fill="#2196f3" />
|
|
</marker>
|
|
</defs>
|
|
|
|
<!-- Background circle -->
|
|
<circle cx="150" cy="150" r="120" fill="none" stroke="currentColor" stroke-width="2"
|
|
opacity="0.1" style="color: var(--mud-palette-text-primary);" />
|
|
|
|
<!-- IMU Device (3D box representation) -->
|
|
<g transform="translate(150, 150)">
|
|
<!-- Device box với perspective -->
|
|
<g transform="rotate(@(QuaternionToEuler(ImuData.Orientation).yaw * 180 / Math.PI), 0, 0)">
|
|
<!-- Top face -->
|
|
<polygon points="-30,-20 -10,-30 10,-30 30,-20 30,20 10,30 -10,30 -30,20"
|
|
fill="var(--mud-palette-surface)"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
opacity="0.9"
|
|
style="color: var(--mud-palette-text-primary);" />
|
|
<!-- Front face -->
|
|
<polygon points="-30,-20 30,-20 30,20 -30,20"
|
|
fill="var(--mud-palette-surface)"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
opacity="0.7"
|
|
style="color: var(--mud-palette-text-primary);" />
|
|
<!-- Side face -->
|
|
<polygon points="30,-20 10,-30 -10,-30 -30,-20 -30,20 -10,30 10,30 30,20"
|
|
fill="var(--mud-palette-surface)"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
opacity="0.5"
|
|
style="color: var(--mud-palette-text-primary);" />
|
|
</g>
|
|
|
|
<!-- X Axis (Red) -->
|
|
@{
|
|
var accelX = Math.Max(-50, Math.Min(50, ImuData.LinearAcceleration.X * 5));
|
|
var accelXEnd = 80 + accelX;
|
|
}
|
|
<line x1="0" y1="0" x2="@accelXEnd" y2="0"
|
|
stroke="#f44336"
|
|
stroke-width="4"
|
|
marker-end="@($"url(#arrowhead-red-{DeviceId})")"
|
|
filter="@($"url(#glow_{DeviceId})")"
|
|
opacity="0.8">
|
|
<animate attributeName="x2"
|
|
values="@(accelXEnd - 5);@(accelXEnd + 5);@(accelXEnd - 5)"
|
|
dur="1s"
|
|
repeatCount="indefinite" />
|
|
</line>
|
|
<text x="@(accelXEnd + 10)" y="5" font-size="12" fill="#f44336" font-weight="bold">X</text>
|
|
|
|
<!-- Y Axis (Green) -->
|
|
@{
|
|
var accelY = Math.Max(-50, Math.Min(50, ImuData.LinearAcceleration.Y * 5));
|
|
var accelYEnd = 80 + accelY;
|
|
}
|
|
<line x1="0" y1="0" x2="0" y2="@(-accelYEnd)"
|
|
stroke="#4caf50"
|
|
stroke-width="4"
|
|
marker-end="@($"url(#arrowhead-green-{DeviceId})")"
|
|
filter="@($"url(#glow_{DeviceId})")"
|
|
opacity="0.8">
|
|
<animate attributeName="y2"
|
|
values="@(-accelYEnd - 5);@(-accelYEnd + 5);@(-accelYEnd - 5)"
|
|
dur="1s"
|
|
repeatCount="indefinite" />
|
|
</line>
|
|
<text x="5" y="@(-accelYEnd - 10)" font-size="12" fill="#4caf50" font-weight="bold">Y</text>
|
|
|
|
<!-- Z Axis (Blue) - represented as depth -->
|
|
@{
|
|
var accelZ = Math.Max(-50, Math.Min(50, ImuData.LinearAcceleration.Z * 5));
|
|
var zOffset = accelZ * 0.5;
|
|
}
|
|
<line x1="0" y1="0" x2="@zOffset" y2="@(80 + zOffset)"
|
|
stroke="#2196f3"
|
|
stroke-width="4"
|
|
stroke-dasharray="5,5"
|
|
marker-end="@($"url(#arrowhead-blue-{DeviceId})")"
|
|
filter="@($"url(#glow_{DeviceId})")"
|
|
opacity="0.8">
|
|
<animate attributeName="x2"
|
|
values="@(zOffset - 3);@(zOffset + 3);@(zOffset - 3)"
|
|
dur="1s"
|
|
repeatCount="indefinite" />
|
|
<animate attributeName="y2"
|
|
values="@(80 + zOffset - 3);@(80 + zOffset + 3);@(80 + zOffset - 3)"
|
|
dur="1s"
|
|
repeatCount="indefinite" />
|
|
</line>
|
|
<text x="@(zOffset + 5)" y="@(80 + zOffset + 15)" font-size="12" fill="#2196f3" font-weight="bold">Z</text>
|
|
|
|
<!-- Angular velocity indicators (circular arrows) -->
|
|
@{
|
|
var angularVel = Math.Sqrt(ImuData.AngularVelocity.X * ImuData.AngularVelocity.X +
|
|
ImuData.AngularVelocity.Y * ImuData.AngularVelocity.Y +
|
|
ImuData.AngularVelocity.Z * ImuData.AngularVelocity.Z);
|
|
var angularVelNormalized = Math.Max(0, Math.Min(1, angularVel / 5.0));
|
|
var rotationSpeed = angularVelNormalized * 360;
|
|
}
|
|
<circle cx="0" cy="0" r="60"
|
|
fill="none"
|
|
stroke="#ff9800"
|
|
stroke-width="3"
|
|
stroke-dasharray="10,5"
|
|
opacity="@(0.3 + angularVelNormalized * 0.5)"
|
|
transform="rotate(@rotationSpeed)">
|
|
<animateTransform attributeName="transform"
|
|
type="rotate"
|
|
values="0;360"
|
|
dur="@(Math.Max(0.5, 5 - angularVelNormalized * 4.5))s"
|
|
repeatCount="indefinite" />
|
|
</circle>
|
|
|
|
<!-- Center point -->
|
|
<circle cx="0" cy="0" r="5" fill="currentColor"
|
|
style="color: var(--mud-palette-text-primary);" />
|
|
</g>
|
|
|
|
<!-- Orientation text -->
|
|
<text x="150" y="280"
|
|
text-anchor="middle"
|
|
font-size="14"
|
|
font-weight="bold"
|
|
fill="currentColor"
|
|
style="color: var(--mud-palette-text-primary);">
|
|
Orientation
|
|
</text>
|
|
<text x="150" y="295"
|
|
text-anchor="middle"
|
|
font-size="12"
|
|
fill="currentColor"
|
|
style="color: var(--mud-palette-text-secondary);">
|
|
R:@(QuaternionToEuler(ImuData.Orientation).roll.ToString("F2")) P:@(QuaternionToEuler(ImuData.Orientation).pitch.ToString("F2")) Y:@(QuaternionToEuler(ImuData.Orientation).yaw.ToString("F2"))
|
|
</text>
|
|
</svg>
|
|
</div>
|
|
</MudItem>
|
|
|
|
<!-- Right Column: Data parameters -->
|
|
<MudItem xs="12" md="8">
|
|
<MudGrid>
|
|
<!-- Status Row -->
|
|
<MudItem xs="12">
|
|
<MudGrid>
|
|
<!-- Status fields removed - not available in Imu struct -->
|
|
</MudGrid>
|
|
</MudItem>
|
|
|
|
<!-- Acceleration -->
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h6" Color="Color.Primary">Acceleration</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">m/s²</MudText>
|
|
<MudText Typo="Typo.body1">X: <strong>@ImuData.LinearAcceleration.X.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">Y: <strong>@ImuData.LinearAcceleration.Y.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">Z: <strong>@ImuData.LinearAcceleration.Z.ToString("F3")</strong></MudText>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- Angular Velocity -->
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h6" Color="Color.Info">Angular Velocity</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">rad/s</MudText>
|
|
<MudText Typo="Typo.body1">Roll (X): <strong>@ImuData.AngularVelocity.X.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">Pitch (Y): <strong>@ImuData.AngularVelocity.Y.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">Yaw (Z): <strong>@ImuData.AngularVelocity.Z.ToString("F3")</strong></MudText>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- Orientation -->
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h6" Color="Color.Success">Orientation</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">rad (Euler)</MudText>
|
|
<MudText Typo="Typo.body1">Roll: <strong>@QuaternionToEuler(ImuData.Orientation).roll.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">Pitch: <strong>@QuaternionToEuler(ImuData.Orientation).pitch.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">Yaw: <strong>@QuaternionToEuler(ImuData.Orientation).yaw.ToString("F3")</strong></MudText>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- Quaternion -->
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h6" Color="Color.Secondary">Quaternion</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">Unit quaternion</MudText>
|
|
<MudText Typo="Typo.body1">W: <strong>@ImuData.Orientation.W.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">X: <strong>@ImuData.Orientation.X.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">Y: <strong>@ImuData.Orientation.Y.ToString("F3")</strong></MudText>
|
|
<MudText Typo="Typo.body1">Z: <strong>@ImuData.Orientation.Z.ToString("F3")</strong></MudText>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- Last Update Time -->
|
|
<MudItem xs="12">
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
|
Last Update: @ImuData.Header.Stamp.ToString("yyyy-MM-dd HH:mm:ss")
|
|
</MudText>
|
|
</MudItem>
|
|
|
|
<!-- Device Properties -->
|
|
@if (DeviceProperties.Any() && PropertyDescriptions.Any())
|
|
{
|
|
<MudItem xs="12">
|
|
<MudExpansionPanels Dense="true" Class="mt-2">
|
|
<MudExpansionPanel Text="@($"Device Properties ({DeviceProperties.Count})")"
|
|
Icon="@Icons.Material.Filled.Info"
|
|
Style="font-size: 0.875rem;">
|
|
<MudSimpleTable Dense="true" Hover="true" Striped="true">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 8px; width: 30%;">Property</th>
|
|
<th style="padding: 8px; width: 70%;">Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var prop in GetDisplayedProperties())
|
|
{
|
|
<tr>
|
|
<td style="padding: 6px 8px;">
|
|
<MudText Typo="Typo.body2" Style="font-weight: 500;">
|
|
@prop.DisplayName
|
|
</MudText>
|
|
@if (!string.IsNullOrWhiteSpace(prop.Description))
|
|
{
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary" Style="font-size: 0.7rem;">
|
|
@prop.Description
|
|
</MudText>
|
|
}
|
|
</td>
|
|
<td style="padding: 6px 8px;">
|
|
@if (DeviceProperties.TryGetValue(prop.Key, out var value))
|
|
{
|
|
<MudText Typo="Typo.body2">
|
|
@FormatPropertyValue(value, prop)
|
|
</MudText>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">-</MudText>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
</MudExpansionPanel>
|
|
</MudExpansionPanels>
|
|
</MudItem>
|
|
}
|
|
</MudGrid>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
|
|
<MudOverlay Visible="@IsLoading" Absolute="true">
|
|
<MudProgressCircular Indeterminate="true" Size="Size.Large" />
|
|
</MudOverlay>
|
|
|
|
@code {
|
|
[Parameter, EditorRequired]
|
|
public string DeviceId { get; set; } = string.Empty;
|
|
|
|
private string DeviceName { get; set; } = string.Empty;
|
|
private Imu ImuData = new();
|
|
private Dictionary<string, string> DeviceProperties = new();
|
|
private List<PropertyDescription> PropertyDescriptions = new();
|
|
private bool IsLoading => !ImuHubClient.IsConnected;
|
|
private bool IsReloading = false;
|
|
|
|
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);
|
|
}
|
|
|
|
private async Task ConnectAsync()
|
|
{
|
|
try
|
|
{
|
|
await ImuHubClient.StartAsync();
|
|
|
|
// Lấy DeviceName từ ImuHub
|
|
var deviceInfo = await ImuHubClient.GetDeviceInfoAsync(DeviceId);
|
|
if (deviceInfo != null)
|
|
{
|
|
DeviceName = deviceInfo.DeviceName;
|
|
}
|
|
else
|
|
{
|
|
DeviceName = DeviceId; // Fallback to DeviceId if not found
|
|
}
|
|
|
|
ImuData = await ImuHubClient.GetImuDataAsync(DeviceId);
|
|
|
|
// Lấy device properties và property descriptions
|
|
var properties = await ImuHubClient.GetDevicePropertiesAsync(DeviceId);
|
|
if (properties != null)
|
|
{
|
|
DeviceProperties = properties;
|
|
}
|
|
|
|
var propDescriptions = await ImuHubClient.GetDevicePropertyDescriptionsAsync(DeviceId);
|
|
if (propDescriptions != null)
|
|
{
|
|
PropertyDescriptions = propDescriptions;
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to connect: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task DisconnectAsync()
|
|
{
|
|
try
|
|
{
|
|
await ImuHubClient.StopAsync();
|
|
ImuData = new Imu(); // Reset về giá trị mặc định
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to disconnect: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task ReloadImuDataAsync()
|
|
{
|
|
if (!ImuHubClient.IsConnected || IsReloading)
|
|
return;
|
|
|
|
try
|
|
{
|
|
IsReloading = true;
|
|
StateHasChanged();
|
|
|
|
ImuData = await ImuHubClient.ReadAllDataAsync(DeviceId);
|
|
|
|
// Reload properties
|
|
var properties = await ImuHubClient.GetDevicePropertiesAsync(DeviceId);
|
|
if (properties != null)
|
|
{
|
|
DeviceProperties = properties;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to reload IMU data: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
IsReloading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await DisconnectAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert Quaternion sang Euler angles (Roll, Pitch, Yaw)
|
|
/// </summary>
|
|
private (double roll, double pitch, double yaw) QuaternionToEuler(QuaternionGeometry q)
|
|
{
|
|
// Roll (x-axis rotation)
|
|
var sinr_cosp = 2 * (q.W * q.X + q.Y * q.Z);
|
|
var cosr_cosp = 1 - 2 * (q.X * q.X + q.Y * q.Y);
|
|
var roll = Math.Atan2(sinr_cosp, cosr_cosp);
|
|
|
|
// Pitch (y-axis rotation)
|
|
var sinp = 2 * (q.W * q.Y - q.Z * q.X);
|
|
double pitch;
|
|
if (Math.Abs(sinp) >= 1)
|
|
pitch = Math.CopySign(Math.PI / 2, sinp); // use 90 degrees if out of range
|
|
else
|
|
pitch = Math.Asin(sinp);
|
|
|
|
// Yaw (z-axis rotation)
|
|
var siny_cosp = 2 * (q.W * q.Z + q.X * q.Y);
|
|
var cosy_cosp = 1 - 2 * (q.Y * q.Y + q.Z * q.Z);
|
|
var yaw = Math.Atan2(siny_cosp, cosy_cosp);
|
|
|
|
return (roll, pitch, yaw);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lấy danh sách properties đã sắp xếp để hiển thị
|
|
/// </summary>
|
|
private IEnumerable<PropertyDescription> GetDisplayedProperties()
|
|
{
|
|
return PropertyDescriptions
|
|
.OrderBy(p => p.DisplayOrder)
|
|
.ThenBy(p => p.Category ?? "")
|
|
.ThenBy(p => p.DisplayName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format giá trị property theo DataType và Format
|
|
/// </summary>
|
|
private string FormatPropertyValue(string value, PropertyDescription prop)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
return "-";
|
|
|
|
if (prop.DataType == "number" && double.TryParse(value, out var numValue))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(prop.Format))
|
|
{
|
|
try
|
|
{
|
|
return string.Format(prop.Format, numValue) + (prop.Unit != null ? $" {prop.Unit}" : "");
|
|
}
|
|
catch
|
|
{
|
|
return value + (prop.Unit != null ? $" {prop.Unit}" : "");
|
|
}
|
|
}
|
|
return value + (prop.Unit != null ? $" {prop.Unit}" : "");
|
|
}
|
|
|
|
return value + (prop.Unit != null ? $" {prop.Unit}" : "");
|
|
}
|
|
}
|