413 lines
19 KiB
Plaintext
413 lines
19 KiB
Plaintext
@using MudBlazor
|
|
@using Microsoft.AspNetCore.SignalR.Client
|
|
@using RobotNet10.RobotApp.Client.Clients
|
|
@using RobotNet10.RobotApp.Client.Shared.Devices
|
|
@implements IAsyncDisposable
|
|
@inject ModbusTcpHubClient ModbusTcpHubClient
|
|
@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 Modbus data">
|
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
|
Color="Color.Primary"
|
|
Size="Size.Small"
|
|
OnClick="ReloadModbusDataAsync"
|
|
Disabled="@(!ModbusTcpHubClient.IsConnected || IsReloading)">
|
|
</MudIconButton>
|
|
</MudTooltip>
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
<MudCardContent Style="position: relative;">
|
|
<MudGrid>
|
|
<!-- Connection Info -->
|
|
<MudItem xs="12">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudGrid>
|
|
<MudItem xs="12" sm="3">
|
|
<MudText Typo="Typo.body1">
|
|
<strong>IP:</strong> @ModbusData.IpAddress
|
|
</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="3">
|
|
<MudText Typo="Typo.body1">
|
|
<strong>Port:</strong> @ModbusData.Port
|
|
</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="3">
|
|
<MudText Typo="Typo.body1">
|
|
<strong>Slave ID:</strong> @ModbusData.SlaveId
|
|
</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="3">
|
|
<MudChip T="string" Color="@(ModbusData.IsConnected ? Color.Success : Color.Error)"
|
|
Size="Size.Small"
|
|
Variant="Variant.Filled">
|
|
@(ModbusData.IsConnected ? "Connected" : "Disconnected")
|
|
</MudChip>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- Holding Registers -->
|
|
@if (ModbusData.HoldingRegisters != null && ModbusData.HoldingRegisters.Length > 0)
|
|
{
|
|
@foreach (var range in ModbusData.HoldingRegisters)
|
|
{
|
|
<MudItem xs="12">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h6" Color="Color.Primary">
|
|
Holding Registers: @range.Name
|
|
</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">
|
|
Address: @range.StartAddress - @(range.StartAddress + range.Quantity - 1) (@range.Quantity registers)
|
|
</MudText>
|
|
<MudTable Items="@range.Values" Hover="true" Dense="true" Striped="true">
|
|
<HeaderContent>
|
|
<MudTh>Address</MudTh>
|
|
<MudTh>Index</MudTh>
|
|
<MudTh>Name</MudTh>
|
|
<MudTh>Value (Decimal)</MudTh>
|
|
<MudTh>Value (Hex)</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Address">@context.Address</MudTd>
|
|
<MudTd DataLabel="Index">@context.Index</MudTd>
|
|
<MudTd DataLabel="Name">
|
|
@if (!string.IsNullOrEmpty(context.Name))
|
|
{
|
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Text" Color="Color.Info">
|
|
@context.Name
|
|
</MudChip>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">-</MudText>
|
|
}
|
|
</MudTd>
|
|
<MudTd DataLabel="Value (Decimal)">
|
|
<strong>@context.Value</strong>
|
|
</MudTd>
|
|
<MudTd DataLabel="Value (Hex)">
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">0x@(context.Value.ToString("X4"))</MudText>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudCard>
|
|
</MudItem>
|
|
}
|
|
}
|
|
|
|
<!-- Input Registers -->
|
|
@if (ModbusData.InputRegisters != null && ModbusData.InputRegisters.Length > 0)
|
|
{
|
|
@foreach (var range in ModbusData.InputRegisters)
|
|
{
|
|
<MudItem xs="12">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h6" Color="Color.Info">
|
|
Input Registers: @range.Name
|
|
</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">
|
|
Address: @range.StartAddress - @(range.StartAddress + range.Quantity - 1) (@range.Quantity registers)
|
|
</MudText>
|
|
<MudTable Items="@range.Values" Hover="true" Dense="true" Striped="true">
|
|
<HeaderContent>
|
|
<MudTh>Address</MudTh>
|
|
<MudTh>Index</MudTh>
|
|
<MudTh>Name</MudTh>
|
|
<MudTh>Value (Decimal)</MudTh>
|
|
<MudTh>Value (Hex)</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Address">@context.Address</MudTd>
|
|
<MudTd DataLabel="Index">@context.Index</MudTd>
|
|
<MudTd DataLabel="Name">
|
|
@if (!string.IsNullOrEmpty(context.Name))
|
|
{
|
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Text" Color="Color.Info">
|
|
@context.Name
|
|
</MudChip>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">-</MudText>
|
|
}
|
|
</MudTd>
|
|
<MudTd DataLabel="Value (Decimal)">
|
|
<strong>@context.Value</strong>
|
|
</MudTd>
|
|
<MudTd DataLabel="Value (Hex)">
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">0x@(context.Value.ToString("X4"))</MudText>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudCard>
|
|
</MudItem>
|
|
}
|
|
}
|
|
|
|
<!-- Coils -->
|
|
@if (ModbusData.Coils != null && ModbusData.Coils.Length > 0)
|
|
{
|
|
@foreach (var range in ModbusData.Coils)
|
|
{
|
|
<MudItem xs="12">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h6" Color="Color.Success">
|
|
Coils: @range.Name
|
|
</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">
|
|
Address: @range.StartAddress - @(range.StartAddress + range.Quantity - 1) (@range.Quantity coils)
|
|
</MudText>
|
|
<MudTable Items="@range.BoolValues" Hover="true" Dense="true" Striped="true">
|
|
<HeaderContent>
|
|
<MudTh>Address</MudTh>
|
|
<MudTh>Index</MudTh>
|
|
<MudTh>Name</MudTh>
|
|
<MudTh>Value</MudTh>
|
|
<MudTh>Action</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Address">@context.Address</MudTd>
|
|
<MudTd DataLabel="Index">@context.Index</MudTd>
|
|
<MudTd DataLabel="Name">
|
|
@if (!string.IsNullOrEmpty(context.Name))
|
|
{
|
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Text" Color="Color.Info">
|
|
@context.Name
|
|
</MudChip>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">-</MudText>
|
|
}
|
|
</MudTd>
|
|
<MudTd DataLabel="Value">
|
|
<MudChip T="string" Size="Size.Small"
|
|
Variant="Variant.Filled"
|
|
Color="@(context.Value ? Color.Success : Color.Default)">
|
|
@(context.Value ? "ON" : "OFF")
|
|
</MudChip>
|
|
</MudTd>
|
|
<MudTd DataLabel="Action">
|
|
@{
|
|
var coilKey = $"{range.StartAddress}_{context.Index}";
|
|
var isWriting = WritingCoils.ContainsKey(coilKey) && WritingCoils[coilKey];
|
|
}
|
|
<MudSwitch Value="@context.Value"
|
|
Disabled="@(!ModbusTcpHubClient.IsConnected || isWriting)"
|
|
Color="Color.Success"
|
|
Size="Size.Small"
|
|
ValueChanged="@((bool value) => HandleCoilToggle(context.Address, value, coilKey))">
|
|
</MudSwitch>
|
|
@if (isWriting)
|
|
{
|
|
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="ml-2" />
|
|
}
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudCard>
|
|
</MudItem>
|
|
}
|
|
}
|
|
|
|
<!-- Discrete Inputs -->
|
|
@if (ModbusData.DiscreteInputs != null && ModbusData.DiscreteInputs.Length > 0)
|
|
{
|
|
@foreach (var range in ModbusData.DiscreteInputs)
|
|
{
|
|
<MudItem xs="12">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h6" Color="Color.Warning">
|
|
Discrete Inputs: @range.Name
|
|
</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">
|
|
Address: @range.StartAddress - @(range.StartAddress + range.Quantity - 1) (@range.Quantity inputs)
|
|
</MudText>
|
|
<MudTable Items="@range.BoolValues" Hover="true" Dense="true" Striped="true">
|
|
<HeaderContent>
|
|
<MudTh>Address</MudTh>
|
|
<MudTh>Index</MudTh>
|
|
<MudTh>Name</MudTh>
|
|
<MudTh>Value</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Address">@context.Address</MudTd>
|
|
<MudTd DataLabel="Index">@context.Index</MudTd>
|
|
<MudTd DataLabel="Name">
|
|
@if (!string.IsNullOrEmpty(context.Name))
|
|
{
|
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Text" Color="Color.Info">
|
|
@context.Name
|
|
</MudChip>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">-</MudText>
|
|
}
|
|
</MudTd>
|
|
<MudTd DataLabel="Value">
|
|
<MudChip T="string" Size="Size.Small"
|
|
Variant="Variant.Filled"
|
|
Color="@(context.Value ? Color.Success : Color.Default)">
|
|
@(context.Value ? "ON" : "OFF")
|
|
</MudChip>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudCard>
|
|
</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 ModbusTcpData ModbusData = new();
|
|
private bool IsLoading => !ModbusTcpHubClient.IsConnected;
|
|
private bool IsReloading = false;
|
|
private Dictionary<string, bool> WritingCoils = new();
|
|
|
|
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 ModbusTcpHubClient.StartAsync();
|
|
|
|
// Lấy DeviceName từ ModbusTcpHub
|
|
var deviceInfo = await ModbusTcpHubClient.GetDeviceInfoAsync(DeviceId);
|
|
if (deviceInfo != null)
|
|
{
|
|
DeviceName = deviceInfo.DeviceName;
|
|
}
|
|
else
|
|
{
|
|
DeviceName = DeviceId; // Fallback to DeviceId if not found
|
|
}
|
|
|
|
ModbusData = await ModbusTcpHubClient.GetModbusDataAsync(DeviceId);
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to connect: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task DisconnectAsync()
|
|
{
|
|
try
|
|
{
|
|
await ModbusTcpHubClient.StopAsync();
|
|
ModbusData = new ModbusTcpData(); // Reset về giá trị mặc định
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to disconnect: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task ReloadModbusDataAsync()
|
|
{
|
|
if (!ModbusTcpHubClient.IsConnected || IsReloading)
|
|
return;
|
|
|
|
try
|
|
{
|
|
IsReloading = true;
|
|
StateHasChanged();
|
|
|
|
ModbusData = await ModbusTcpHubClient.GetModbusDataAsync(DeviceId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to reload Modbus data: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
IsReloading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task HandleCoilToggle(ushort address, bool value, string coilKey)
|
|
{
|
|
if (!ModbusTcpHubClient.IsConnected)
|
|
{
|
|
Snackbar.Add("Not connected to Modbus device", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
// Set writing state
|
|
WritingCoils[coilKey] = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
var success = await ModbusTcpHubClient.WriteCoilAsync(DeviceId, address, value);
|
|
if (success)
|
|
{
|
|
Snackbar.Add($"Coil {address} set to {(value ? "ON" : "OFF")}", Severity.Success);
|
|
|
|
// Reload data để cập nhật giá trị mới nhất
|
|
await ReloadModbusDataAsync();
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add($"Failed to write coil {address}", Severity.Error);
|
|
// Reload để khôi phục giá trị cũ
|
|
await ReloadModbusDataAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error writing coil {address}: {ex.Message}", Severity.Error);
|
|
// Reload để khôi phục giá trị cũ
|
|
await ReloadModbusDataAsync();
|
|
}
|
|
finally
|
|
{
|
|
WritingCoils[coilKey] = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await DisconnectAsync();
|
|
}
|
|
}
|
|
|