Blazor app with can interface
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
8.4 KiB

@page "/serial"
@rendermode InteractiveServer
@implements IDisposable
@using IOModuleTestBlazor.Services
@inject ISerialPortService SerialService
@inject IJSRuntime JS
<PageTitle>Serial Terminal</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1>Serial Terminal</h1>
@* ── Connection Bar ──────────────────────────────────────────────────── *@
<div class="card mb-3">
<div class="card-body py-2">
<div class="d-flex align-items-center gap-2 flex-wrap">
@if (SerialService.IsOpen)
{
<span class="badge bg-success">● Connected</span>
<span class="font-monospace fw-semibold">@SerialService.PortName</span>
<span class="text-muted">115200 baud</span>
}
else
{
<span class="badge bg-danger">● Disconnected</span>
}
<select class="form-select form-select-sm" style="width: auto;"
@bind="_selectedPort" disabled="@SerialService.IsOpen">
@if (!_availablePorts.Any())
{
<option value="">No ports found</option>
}
@foreach (var port in _availablePorts)
{
<option value="@port">@port</option>
}
</select>
<button class="btn btn-sm btn-outline-secondary" @onclick="RefreshPorts"
disabled="@SerialService.IsOpen">
Refresh
</button>
@if (SerialService.IsOpen)
{
<button class="btn btn-sm btn-danger" @onclick="Disconnect">Disconnect</button>
}
else
{
<button class="btn btn-sm btn-success" @onclick="Connect"
disabled="@string.IsNullOrEmpty(_selectedPort)">
Connect
</button>
}
@if (_connectError is not null)
{
<div class="w-100 mt-1">
<span class="badge bg-danger">Connect failed:</span>
<span class="text-danger small ms-1">@_connectError</span>
</div>
}
</div>
</div>
</div>
@* ── Terminal Output ─────────────────────────────────────────────────── *@
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Output</h5>
<button class="btn btn-sm btn-outline-secondary" @onclick="ClearTerminal">Clear</button>
</div>
<div class="card-body p-0">
<pre id="terminal-output"
class="mb-0 p-3"
style="height: 400px; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; font-size: 0.85rem;">@GetTerminalText()</pre>
</div>
</div>
@* ── Command Input ───────────────────────────────────────────────────── *@
<div class="card mb-3">
<div class="card-body">
<div class="d-flex gap-2">
<input class="form-control font-monospace"
placeholder="Enter command (e.g. r 0000 4)"
@bind="_commandInput"
@bind:event="oninput"
@onkeydown="OnKeyDown"
disabled="@(!SerialService.IsOpen)" />
<button class="btn btn-primary" @onclick="SendCommand"
disabled="@(!SerialService.IsOpen || string.IsNullOrWhiteSpace(_commandInput))">
Send
</button>
</div>
@* ── Quick Commands ──────────────────────────────────────────── *@
<div class="mt-2 d-flex gap-2 flex-wrap">
<span class="text-muted small align-self-center">Quick:</span>
@foreach (var (label, cmd) in _quickCommands)
{
<button class="btn btn-sm btn-outline-secondary font-monospace"
@onclick="() => SendQuickCommand(cmd)"
disabled="@(!SerialService.IsOpen)">
@label
</button>
}
</div>
</div>
</div>
</div>
</div>
</div>
@code {
private string _selectedPort = string.Empty;
private string _commandInput = string.Empty;
private string? _connectError;
private List<string> _availablePorts = new();
private List<string> _displayLines = new();
private IJSObjectReference? _jsModule;
private bool _scrollPending = false;
private static readonly (string Label, string Cmd)[] _quickCommands =
[
("?", "?"),
("r 0000 4", "r 0000 4"),
("r 0000 10", "r 0000 10"),
("w 0000 AB CD", "w 0000 AB CD"),
("w 0000 FF FF", "w 0000 FF FF"),
];
protected override void OnInitialized()
{
RefreshPorts();
SerialService.DataReceived += OnDataReceived;
_displayLines = SerialService.GetLines().ToList();
}
private void RefreshPorts()
{
_availablePorts = SerialService.GetPortNames().ToList();
if (_availablePorts.Count > 0 && string.IsNullOrEmpty(_selectedPort))
_selectedPort = _availablePorts[0];
}
private async Task Connect()
{
_connectError = null;
try
{
await Task.Run(() => SerialService.Open(_selectedPort, 115200));
}
catch (Exception ex)
{
_connectError = ex.Message;
}
}
private async Task Disconnect()
{
await Task.Run(() => SerialService.Close());
}
private void SendCommand()
{
if (string.IsNullOrWhiteSpace(_commandInput)) return;
SerialService.WriteLine(_commandInput.Trim());
_commandInput = string.Empty;
RefreshLines();
}
private void SendQuickCommand(string cmd)
{
SerialService.WriteLine(cmd);
RefreshLines();
}
private void OnKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter") SendCommand();
}
private void OnDataReceived()
{
InvokeAsync(() =>
{
RefreshLines();
_scrollPending = true;
StateHasChanged();
});
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
_jsModule = await JS.InvokeAsync<IJSObjectReference>(
"import", "./Components/Pages/SerialTerminal.razor.js");
if (_scrollPending && _jsModule is not null)
{
_scrollPending = false;
await _jsModule.InvokeVoidAsync("scrollToBottom", "terminal-output");
}
}
private void RefreshLines()
=> _displayLines = SerialService.GetLines().ToList();
private void ClearTerminal()
{
SerialService.ClearLines();
_displayLines.Clear();
StateHasChanged();
}
private string GetTerminalText()
=> string.Join("\n", _displayLines);
public void Dispose()
{
SerialService.DataReceived -= OnDataReceived;
_jsModule?.DisposeAsync();
}
}