mbproxy: initial commit through Phase 9 (TxId multiplexing)
Adds the mbproxy service end-to-end. Phases 00-08 implement the production-ready single-listener / 1:1-backend transparent Modbus TCP proxy with bidirectional BCD rewriting for the ~54-PLC DL205/DL260 fleet. Phase 9 replaces the connection layer with a single backend socket per PLC plus MBAP TxId rewriting, lifting the H2-ECOM100's 4-concurrent-client cap as an operational ceiling. Phase 9 additions of note: - PlcMultiplexer + UpstreamPipe + TxIdAllocator + CorrelationMap - InFlightRequest with IReadOnlyList<InterestedParty> (load-bearing for Phase 10 read coalescing — do not collapse to a single field) - Per-request watchdog: surfaces Modbus exception 0x0B to upstream on BackendRequestTimeoutMs, defending against lost responses, dead-PLC paths, and pymodbus 3.13.0's concurrent-multiplexed- request bug (its ServerRequestHandler.last_pdu state race) - Status DTO + HTML gain inFlight / maxInFlight / txIdWraps / disconnectCascades / queueDepth (Tier 1.6 in docs/kpi.md) Tests: 263 unit + 38 E2E. Multiplexer correctness under truly concurrent backend traffic is proved against a stub backend in PlcMultiplexerTests; MultiplexerE2ETests paces requests so pymodbus 3.13's single-PDU framer stays in known-good mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Mbproxy.Options;
|
||||
|
||||
public sealed class MbproxyOptions
|
||||
{
|
||||
public BcdTagListOptions BcdTags { get; init; } = new();
|
||||
public IReadOnlyList<PlcOptions> Plcs { get; init; } = [];
|
||||
public int AdminPort { get; init; } = 8080;
|
||||
public ConnectionOptions Connection { get; init; } = new();
|
||||
public ResilienceOptions Resilience { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schema-level validation for <see cref="MbproxyOptions"/>.
|
||||
/// Business-rule validation (duplicate addresses, port conflicts) is deferred to phase 06.
|
||||
/// </summary>
|
||||
public sealed class MbproxyOptionsValidator : IValidateOptions<MbproxyOptions>
|
||||
{
|
||||
public ValidateOptionsResult Validate(string? name, MbproxyOptions options)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
foreach (var tag in options.BcdTags.Global)
|
||||
{
|
||||
if (tag.Width != 16 && tag.Width != 32)
|
||||
errors.Add($"BcdTags.Global: Address {tag.Address} has invalid Width {tag.Width}; must be 16 or 32.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < options.Plcs.Count; i++)
|
||||
{
|
||||
var plc = options.Plcs[i];
|
||||
if (plc.BcdTags is { } overrides)
|
||||
{
|
||||
foreach (var tag in overrides.Add)
|
||||
{
|
||||
if (tag.Width != 16 && tag.Width != 32)
|
||||
errors.Add($"Plcs[{i}] ({plc.Name}): BcdTags.Add Address {tag.Address} has invalid Width {tag.Width}; must be 16 or 32.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Count > 0
|
||||
? ValidateOptionsResult.Fail(errors)
|
||||
: ValidateOptionsResult.Success;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user