chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
|
||||
/// <summary>
|
||||
/// Static factory registration helper for <see cref="FocasDriver"/>. Server's
|
||||
/// Program.cs calls <see cref="Register"/> once at startup; the bootstrapper
|
||||
/// then materialises FOCAS DriverInstance rows from the central config DB
|
||||
/// into live driver instances.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The DriverConfig JSON selects the <see cref="IFocasClientFactory"/> backend:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>"Backend": "wire"</c> (default) — pure-managed FOCAS2 wire
|
||||
/// client (<see cref="WireFocasClientFactory"/>) speaking directly to
|
||||
/// the CNC on TCP:8193.</item>
|
||||
/// <item><c>"Backend": "unimplemented"</c> / <c>"none"</c> / <c>"stub"</c>
|
||||
/// — returns the no-op factory; useful for scaffolding DriverInstance
|
||||
/// rows before the CNC endpoint is reachable.</item>
|
||||
/// </list>
|
||||
/// Devices / Tags / Probe / Timeout / Series come from the same JSON and
|
||||
/// feed directly into <see cref="FocasDriverOptions"/>.
|
||||
/// </remarks>
|
||||
public static class FocasDriverFactoryExtensions
|
||||
{
|
||||
public const string DriverTypeName = "FOCAS";
|
||||
|
||||
/// <summary>
|
||||
/// Register the FOCAS driver factory in the supplied <see cref="DriverFactoryRegistry"/>.
|
||||
/// Throws if 'FOCAS' is already registered — single-instance per process.
|
||||
/// </summary>
|
||||
public static void Register(DriverFactoryRegistry registry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registry);
|
||||
registry.Register(DriverTypeName, CreateInstance);
|
||||
}
|
||||
|
||||
internal static FocasDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||
|
||||
var dto = JsonSerializer.Deserialize<FocasDriverConfigDto>(driverConfigJson, JsonOptions)
|
||||
?? throw new InvalidOperationException(
|
||||
$"FOCAS driver config for '{driverInstanceId}' deserialised to null");
|
||||
|
||||
// Eager-validate top-level Series so a typo fails fast regardless of whether Devices
|
||||
// are populated yet (common during rollout when rows are seeded before CNCs arrive).
|
||||
_ = ParseSeries(dto.Series);
|
||||
|
||||
var options = new FocasDriverOptions
|
||||
{
|
||||
Devices = dto.Devices is { Count: > 0 }
|
||||
? [.. dto.Devices.Select(d => new FocasDeviceOptions(
|
||||
HostAddress: d.HostAddress ?? throw new InvalidOperationException(
|
||||
$"FOCAS config for '{driverInstanceId}' has a device missing HostAddress"),
|
||||
DeviceName: d.DeviceName,
|
||||
Series: ParseSeries(d.Series ?? dto.Series)))]
|
||||
: [],
|
||||
Tags = dto.Tags is { Count: > 0 }
|
||||
? [.. dto.Tags.Select(t => new FocasTagDefinition(
|
||||
Name: t.Name ?? throw new InvalidOperationException(
|
||||
$"FOCAS config for '{driverInstanceId}' has a tag missing Name"),
|
||||
DeviceHostAddress: t.DeviceHostAddress ?? throw new InvalidOperationException(
|
||||
$"FOCAS tag '{t.Name}' in '{driverInstanceId}' missing DeviceHostAddress"),
|
||||
Address: t.Address ?? throw new InvalidOperationException(
|
||||
$"FOCAS tag '{t.Name}' in '{driverInstanceId}' missing Address"),
|
||||
DataType: ParseDataType(t.DataType, t.Name!, driverInstanceId),
|
||||
Writable: t.Writable ?? true,
|
||||
WriteIdempotent: t.WriteIdempotent ?? false))]
|
||||
: [],
|
||||
Probe = new FocasProbeOptions
|
||||
{
|
||||
Enabled = dto.Probe?.Enabled ?? true,
|
||||
Interval = TimeSpan.FromMilliseconds(dto.Probe?.IntervalMs ?? 5_000),
|
||||
Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000),
|
||||
},
|
||||
Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 2_000),
|
||||
};
|
||||
|
||||
var clientFactory = BuildClientFactory(dto, driverInstanceId);
|
||||
return new FocasDriver(options, driverInstanceId, clientFactory);
|
||||
}
|
||||
|
||||
internal static IFocasClientFactory BuildClientFactory(
|
||||
FocasDriverConfigDto dto, string driverInstanceId)
|
||||
{
|
||||
var backend = (dto.Backend ?? "wire").Trim().ToLowerInvariant();
|
||||
return backend switch
|
||||
{
|
||||
"wire" => new WireFocasClientFactory(),
|
||||
"unimplemented" or "none" or "stub" => new UnimplementedFocasClientFactory(),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"FOCAS driver config for '{driverInstanceId}' has unknown Backend '{dto.Backend}'. " +
|
||||
"Expected one of: wire, unimplemented. " +
|
||||
"(The legacy 'ipc' / 'fwlib' backends were retired in the Wire migration — " +
|
||||
"see docs/drivers/FOCAS.md.)"),
|
||||
};
|
||||
}
|
||||
|
||||
private static FocasCncSeries ParseSeries(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return FocasCncSeries.Unknown;
|
||||
return Enum.TryParse<FocasCncSeries>(raw, ignoreCase: true, out var s)
|
||||
? s
|
||||
: throw new InvalidOperationException(
|
||||
$"FOCAS Series '{raw}' is not one of {string.Join(", ", Enum.GetNames<FocasCncSeries>())}");
|
||||
}
|
||||
|
||||
private static FocasDataType ParseDataType(string? raw, string tagName, string driverInstanceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
throw new InvalidOperationException(
|
||||
$"FOCAS tag '{tagName}' in '{driverInstanceId}' missing DataType");
|
||||
return Enum.TryParse<FocasDataType>(raw, ignoreCase: true, out var dt)
|
||||
? dt
|
||||
: throw new InvalidOperationException(
|
||||
$"FOCAS tag '{tagName}' has unknown DataType '{raw}'. " +
|
||||
$"Expected one of {string.Join(", ", Enum.GetNames<FocasDataType>())}");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
};
|
||||
|
||||
internal sealed class FocasDriverConfigDto
|
||||
{
|
||||
public string? Backend { get; init; }
|
||||
public string? Series { get; init; }
|
||||
public int? TimeoutMs { get; init; }
|
||||
public List<FocasDeviceDto>? Devices { get; init; }
|
||||
public List<FocasTagDto>? Tags { get; init; }
|
||||
public FocasProbeDto? Probe { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class FocasDeviceDto
|
||||
{
|
||||
public string? HostAddress { get; init; }
|
||||
public string? DeviceName { get; init; }
|
||||
public string? Series { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class FocasTagDto
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public string? DeviceHostAddress { get; init; }
|
||||
public string? Address { get; init; }
|
||||
public string? DataType { get; init; }
|
||||
public bool? Writable { get; init; }
|
||||
public bool? WriteIdempotent { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class FocasProbeDto
|
||||
{
|
||||
public bool? Enabled { get; init; }
|
||||
public int? IntervalMs { get; init; }
|
||||
public int? TimeoutMs { get; init; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user