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.
209 lines
7.8 KiB
209 lines
7.8 KiB
@page "/serial" |
|
@rendermode InteractiveServer |
|
@implements IDisposable |
|
|
|
@using IOModuleTestBlazor.Services |
|
@inject ISerialPortService SerialService |
|
|
|
<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 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(); |
|
StateHasChanged(); |
|
}); |
|
} |
|
|
|
private void RefreshLines() |
|
=> _displayLines = SerialService.GetLines().ToList(); |
|
|
|
private void ClearTerminal() |
|
{ |
|
_displayLines.Clear(); |
|
StateHasChanged(); |
|
} |
|
|
|
private string GetTerminalText() |
|
=> string.Join("\n", _displayLines); |
|
|
|
public void Dispose() |
|
=> SerialService.DataReceived -= OnDataReceived; |
|
}
|
|
|