315 lines
8.6 KiB
Plaintext
315 lines
8.6 KiB
Plaintext
@page "/sick"
|
||
@implements IDisposable
|
||
|
||
@inject IOptions<CanBusOptions> CanOptions
|
||
@inject ICanBusService CanService
|
||
@inject IJSRuntime JS
|
||
|
||
|
||
<div class="encoder-container">
|
||
<div class="encoder-card">
|
||
<!-- HEADER -->
|
||
<div class="encoder-header">
|
||
<div class="encoder-title">Sick Encoder Monitor</div>
|
||
|
||
<div class="encoder-actions">
|
||
<button class="btn btn-start @( _starting ? "loading" : "" )"
|
||
disabled="@(_starting || _applying)"
|
||
@onclick="OnStartClicked">
|
||
@(_starting ? "Starting..." : "Start")
|
||
</button>
|
||
|
||
<button class="btn btn-stop @( _stopping ? "loading" : "" )"
|
||
disabled="@(_stopping || _applying)"
|
||
@onclick="OnStopClicked">
|
||
@(_stopping ? "Stopping..." : "Stop")
|
||
</button>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- TABLE -->
|
||
<table class="encoder-table">
|
||
<thead>
|
||
<tr>
|
||
<th>CAN-ID</th>
|
||
<th>Length</th>
|
||
<th>Data</th>
|
||
<th>Position (mm)</th>
|
||
<th>Time</th>
|
||
</tr>
|
||
</thead>
|
||
|
||
<tbody>
|
||
@foreach (var f in _frames.Values.OrderBy(f => f.CobId))
|
||
{
|
||
<tr>
|
||
<td class="encoder-id">@($"0x{f.CobId:X3}")</td>
|
||
<td>@f.Length</td>
|
||
<td class="encoder-data">@f.DataHex</td>
|
||
<td class="encoder-pos">
|
||
@(f.PositionMm.HasValue? f.PositionMm.Value.ToString("F2") : "-")
|
||
</td>
|
||
<td class="encoder-time">@f.Timestamp.ToString("HH:mm:ss.fff")</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
|
||
|
||
</table>
|
||
|
||
<!-- STATUS -->
|
||
<div class="encoder-status @StateClass"> Node State: @_nodeState | Bitrate: @(CurrentBitrate / 1000) kbit/s | Node ID: @CurrentNodeId</div>
|
||
<div class="bitrate-group">
|
||
<label class="bitrate-label">Bitrate</label>
|
||
|
||
<select @bind="_selectedBitrate" class="bitrate-select">
|
||
<option value="1000000">1000 kbit/s</option>
|
||
<option value="800000">800 kbit/s</option>
|
||
<option value="500000">500 kbit/s</option>
|
||
<option value="250000">250 kbit/s</option>
|
||
<option value="125000">125 kbit/s</option>
|
||
<option value="100000">100 kbit/s</option>
|
||
<option value="50000">50 kbit/s</option>
|
||
<option value="20000">20 kbit/s</option>
|
||
</select>
|
||
|
||
<button class="btn btn-apply @( _applying ? "loading" : "" )"
|
||
disabled="@(_starting || _applying)"
|
||
@onclick="OnApplyClicked">
|
||
@(_applying ? "Applying..." : "Apply Bitrate")
|
||
</button>
|
||
<label class="nodeid-label">Node ID (1–127)</label>
|
||
|
||
<input type="number"
|
||
min="1"
|
||
max="127"
|
||
@bind="_inputNodeId"
|
||
placeholder="@CurrentNodeId"
|
||
class="nodeid-input" />
|
||
|
||
<button class="btn btn-apply"
|
||
disabled="@(!_inputNodeId.HasValue)"
|
||
@onclick="OnApplyNodeIdClicked">
|
||
Apply Node ID
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@code {
|
||
private PositionPdo? _current;
|
||
private CanNodeState _nodeState = CanNodeState.Unknown;
|
||
private int CurrentBitrate => CanService.CurrentBitrate;
|
||
private readonly Dictionary<uint, CanFrame> _frames = new();
|
||
private int _selectedBitrate;
|
||
private bool _bitrateApplied = false;
|
||
private bool _starting;
|
||
private bool _stopping;
|
||
private bool _applying;
|
||
private int? _inputNodeId;
|
||
private byte CurrentNodeId => CanService.CurrentNodeId;
|
||
|
||
protected override void OnInitialized()
|
||
{
|
||
CanService.PositionReceived += OnPositionReceived;
|
||
CanService.NodeStateChanged += OnNodeStateChanged;
|
||
CanService.FrameReceived += OnFrameReceived;
|
||
CanService.NodeIdChanged += OnNodeIdChanged;
|
||
|
||
_selectedBitrate = CanService.CurrentBitrate;
|
||
}
|
||
|
||
private async Task Start()
|
||
{
|
||
// ❗ LUÔN init lại để đảm bảo COB-ID & state sạch
|
||
await CanService.InitAsync();
|
||
|
||
// ❗ Reset node theo Node ID MỚI
|
||
CanService.SendNmtReset(CurrentNodeId);
|
||
await Task.Delay(800);
|
||
|
||
CanService.SendNmtStart(CurrentNodeId);
|
||
|
||
// ❗ Start read với Node ID MỚI
|
||
CanService.Start();
|
||
}
|
||
|
||
private Task Stop()
|
||
{
|
||
CanService.SendNmtStop(CurrentNodeId);
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private void OnPositionReceived(object? sender, PositionPdo e)
|
||
{
|
||
_ = InvokeAsync(() =>
|
||
{
|
||
_current = e;
|
||
StateHasChanged();
|
||
});
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
CanService.PositionReceived -= OnPositionReceived;
|
||
CanService.NodeStateChanged -= OnNodeStateChanged;
|
||
CanService.FrameReceived -= OnFrameReceived;
|
||
CanService.NodeIdChanged -= OnNodeIdChanged;
|
||
CanService.Stop();
|
||
}
|
||
|
||
private void OnNodeStateChanged(object? sender, CanNodeState state)
|
||
{
|
||
_ = InvokeAsync(() =>
|
||
{
|
||
_nodeState = state;
|
||
StateHasChanged();
|
||
});
|
||
}
|
||
|
||
private string StateClass => _nodeState switch
|
||
{
|
||
CanNodeState.Operational => "state-op",
|
||
CanNodeState.PreOperational => "state-preop",
|
||
CanNodeState.Stopped => "state-stop",
|
||
CanNodeState.Timeout => "state-timeout",
|
||
_ => "state-unknown"
|
||
};
|
||
|
||
private void OnFrameReceived(object? sender, CanFrame frame)
|
||
{
|
||
_ = InvokeAsync(() =>
|
||
{
|
||
// mỗi CAN-ID chỉ giữ frame mới nhất
|
||
_frames[frame.CobId] = frame;
|
||
StateHasChanged();
|
||
});
|
||
}
|
||
|
||
private async Task ApplyBitrate()
|
||
{
|
||
await CanService.ApplyBitrateAsync(
|
||
CurrentNodeId,
|
||
_selectedBitrate
|
||
);
|
||
|
||
// UI chỉ báo PreOp / Stopped
|
||
_nodeState = CanNodeState.PreOperational;
|
||
}
|
||
|
||
private async Task OnApplyNodeIdClicked()
|
||
{
|
||
if (!_inputNodeId.HasValue) return;
|
||
|
||
_applying = true; // Sử dụng flag để disable các nút khác
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
byte newNodeId = (byte)_inputNodeId.Value;
|
||
byte oldNodeId = CanService.CurrentNodeId;
|
||
|
||
// 1. Gửi lệnh đổi ID qua CAN bus
|
||
await CanService.ApplyNodeIdAsync(oldNodeId, newNodeId);
|
||
|
||
// 2. DỪNG SERVICE NGAY LẬP TỨC
|
||
// Điều này ngăn việc nhận frame từ ID cũ
|
||
CanService.Stop();
|
||
|
||
// 3. Reset UI state
|
||
_frames.Clear();
|
||
_nodeState = CanNodeState.PreOperational;
|
||
_inputNodeId = null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Log lỗi nếu cần
|
||
}
|
||
finally
|
||
{
|
||
_applying = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
private async Task OnStartClicked()
|
||
{
|
||
if (_starting) return;
|
||
|
||
_starting = true;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
// ✅ Nếu CHƯA Apply bitrate → dùng bitrate hiện tại
|
||
if (!_bitrateApplied)
|
||
{
|
||
_selectedBitrate = CanService.CurrentBitrate;
|
||
}
|
||
|
||
await Start();
|
||
}
|
||
finally
|
||
{
|
||
_starting = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
private async Task OnStopClicked()
|
||
{
|
||
if (_stopping) return;
|
||
|
||
_stopping = true;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
await Stop();
|
||
}
|
||
finally
|
||
{
|
||
_stopping = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
private async Task OnApplyClicked()
|
||
{
|
||
if (_applying) return;
|
||
|
||
_applying = true;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
await ApplyBitrate();
|
||
|
||
// ✅ Đánh dấu đã Apply
|
||
_bitrateApplied = true;
|
||
}
|
||
finally
|
||
{
|
||
_applying = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
private void OnNodeIdChanged(object? sender, byte newNodeId)
|
||
{
|
||
_ = InvokeAsync(() =>
|
||
{
|
||
// 🔥 XÓA TOÀN BỘ FRAME CŨ
|
||
_frames.Clear();
|
||
|
||
// reset input
|
||
_inputNodeId = null;
|
||
|
||
StateHasChanged();
|
||
});
|
||
}
|
||
|
||
|
||
} |