|
|
|
|
@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();
|
|
|
|
|
}
|
|
|
|
|
}
|