404b54add0
Brings seven FOCAS-related files into git that shipped as part of earlier FOCAS work but were never staged. Adding them now so the tree reflects the compilable state + pre-empts dead references from the migration commit that follows: - src/.../Driver.FOCAS/FocasAlarmProjection.cs — raise/clear diffing + severity mapping surfaced via IAlarmSource on FocasDriver. Referenced by committed FocasDriver.cs; tests in FocasAlarmProjectionTests.cs. - src/.../Admin/Services/FocasDriverDetailService.cs — Admin UI per-instance detail page data source. - src/.../Admin/Components/Pages/Drivers/FocasDetail.razor — Blazor page rendering the above (from task #69). - tests/.../Admin.Tests/FocasDriverDetailServiceTests.cs — exercises the detail service. - tests/.../Driver.FOCAS.Tests/FocasAlarmProjectionTests.cs — raise/clear diff semantics against FakeFocasClient. - tests/.../Driver.FOCAS.Tests/FocasHandleRecycleTests.cs — proactive recycle cadence test. - docs/v2/implementation/focas-wire-protocol.md — captured FOCAS/2 Ethernet wire protocol reference. Useful going forward even though the Tier-C / simulator plan docs are historical. No runtime behaviour change — these files compile today and the solution build/test pass already depends on them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
4.7 KiB
C#
124 lines
4.7 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
|
|
|
|
/// <summary>
|
|
/// Per-instance detail view for FOCAS driver rows. Loads the latest
|
|
/// <see cref="DriverInstance"/> row for the requested <c>DriverInstanceId</c> (most-recent
|
|
/// draft wins when multiple rows exist across generations), parses the schemaless
|
|
/// <c>DriverConfig</c> JSON into <see cref="FocasDriverConfigView"/>, and joins the
|
|
/// per-device <see cref="DriverHostStatus"/> rows so the Admin page can render host
|
|
/// state + consecutive-failure counters next to each configured device.
|
|
/// </summary>
|
|
public sealed class FocasDriverDetailService(OtOpcUaConfigDbContext db)
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
|
};
|
|
|
|
public async Task<FocasDriverDetail?> GetAsync(string driverInstanceId, CancellationToken ct = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(driverInstanceId)) return null;
|
|
|
|
var instance = await db.DriverInstances.AsNoTracking()
|
|
.Where(d => d.DriverInstanceId == driverInstanceId
|
|
&& d.DriverType.ToLower() == "focas")
|
|
.OrderByDescending(d => d.GenerationId)
|
|
.FirstOrDefaultAsync(ct);
|
|
if (instance is null) return null;
|
|
|
|
FocasDriverConfigView? config = null;
|
|
string? parseError = null;
|
|
try { config = JsonSerializer.Deserialize<FocasDriverConfigView>(instance.DriverConfig, JsonOpts); }
|
|
catch (JsonException ex) { parseError = ex.Message; }
|
|
|
|
var hostStatuses = await (from s in db.DriverHostStatuses.AsNoTracking()
|
|
where s.DriverInstanceId == driverInstanceId
|
|
join r in db.DriverInstanceResilienceStatuses.AsNoTracking()
|
|
on new { s.DriverInstanceId, s.HostName }
|
|
equals new { r.DriverInstanceId, r.HostName } into rj
|
|
from r in rj.DefaultIfEmpty()
|
|
orderby s.HostName
|
|
select new FocasHostStatusRow(
|
|
s.NodeId,
|
|
s.HostName,
|
|
s.State.ToString(),
|
|
s.StateChangedUtc,
|
|
s.LastSeenUtc,
|
|
s.Detail,
|
|
r != null ? r.ConsecutiveFailures : 0,
|
|
r != null ? r.LastCircuitBreakerOpenUtc : null,
|
|
r != null ? r.LastRecycleUtc : null)).ToListAsync(ct);
|
|
|
|
return new FocasDriverDetail(instance, config, parseError, hostStatuses);
|
|
}
|
|
}
|
|
|
|
/// <summary>Projected view of a FOCAS driver's parsed config. Unknown fields are ignored.</summary>
|
|
public sealed record FocasDriverConfigView
|
|
{
|
|
public List<FocasDeviceView>? Devices { get; set; }
|
|
public List<FocasTagView>? Tags { get; set; }
|
|
public FocasProbeView? Probe { get; set; }
|
|
public FocasAlarmProjectionView? AlarmProjection { get; set; }
|
|
public FocasHandleRecycleView? HandleRecycle { get; set; }
|
|
}
|
|
|
|
public sealed record FocasDeviceView
|
|
{
|
|
public string? HostAddress { get; set; }
|
|
public string? DeviceName { get; set; }
|
|
public string? Series { get; set; }
|
|
}
|
|
|
|
public sealed record FocasTagView
|
|
{
|
|
public string? Name { get; set; }
|
|
public string? DeviceHostAddress { get; set; }
|
|
public string? Address { get; set; }
|
|
public string? DataType { get; set; }
|
|
public bool Writable { get; set; } = true;
|
|
}
|
|
|
|
public sealed record FocasProbeView
|
|
{
|
|
public bool Enabled { get; set; } = true;
|
|
public string? Interval { get; set; }
|
|
}
|
|
|
|
public sealed record FocasAlarmProjectionView
|
|
{
|
|
public bool Enabled { get; set; }
|
|
public string? PollInterval { get; set; }
|
|
}
|
|
|
|
public sealed record FocasHandleRecycleView
|
|
{
|
|
public bool Enabled { get; set; }
|
|
public string? Interval { get; set; }
|
|
}
|
|
|
|
/// <summary>Composite payload returned to the Admin page.</summary>
|
|
public sealed record FocasDriverDetail(
|
|
DriverInstance Instance,
|
|
FocasDriverConfigView? Config,
|
|
string? ParseError,
|
|
IReadOnlyList<FocasHostStatusRow> HostStatuses);
|
|
|
|
public sealed record FocasHostStatusRow(
|
|
string NodeId,
|
|
string HostName,
|
|
string State,
|
|
DateTime StateChangedUtc,
|
|
DateTime LastSeenUtc,
|
|
string? Detail,
|
|
int ConsecutiveFailures,
|
|
DateTime? LastCircuitBreakerOpenUtc,
|
|
DateTime? LastRecycleUtc);
|