mbproxy/install: slim config templates, point to Configuration.md
The publish-out appsettings templates carried ~250 lines of mostly prose — BCD/CDAB encoding, coalescing rationale, the full cache contract — all of which is already documented in docs/Operations/Configuration.md. Replaced the prose with brief per-section pointers and a header directing operators to that reference; all config values are unchanged. Also dropped a stale comment claiming a 1:1 connection model and a 4-client cap (lifted by the Phase-9 TxId multiplexer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,43 +1,22 @@
|
|||||||
// mbproxy configuration template — copy to %ProgramData%\mbproxy\appsettings.json
|
// mbproxy configuration. Copy to %ProgramData%\mbproxy\appsettings.json and edit
|
||||||
// and edit before starting the service.
|
// before starting the service. install.ps1 seeds this file only when none exists —
|
||||||
|
// an existing appsettings.json is always preserved across reinstalls.
|
||||||
//
|
//
|
||||||
// The .NET configuration loader accepts // and /* */ comments in JSON files
|
// JSONC: // and /* */ comments are accepted. The file is hot-reloaded on save.
|
||||||
// (JSONC semantics) when using the default Host.CreateApplicationBuilder path.
|
|
||||||
//
|
//
|
||||||
// IMPORTANT: This file is overwritten on each install ONLY if no appsettings.json
|
// FULL REFERENCE — every key, type, default, range, validation rule and hot-reload
|
||||||
// already exists at the destination. An existing file is always preserved.
|
// behaviour — lives in docs/Operations/Configuration.md. The notes below are brief
|
||||||
|
// pointers only; consult that document before editing.
|
||||||
{
|
{
|
||||||
"Mbproxy": {
|
"Mbproxy": {
|
||||||
|
|
||||||
// ── Global BCD tag list ─────────────────────────────────────────────────────────────
|
// Fleet-wide BCD tag list — applies to every PLC. Each entry: Address (Modbus
|
||||||
// These tags apply to EVERY PLC by default.
|
// PDU-decimal), Width (16 or 32), optional Name (debug-view label) and CacheTtlMs.
|
||||||
// Each entry: Address (Modbus PDU address, decimal), Width (16 or 32 bits).
|
// Per-PLC Add/Remove overrides go under Plcs[].BcdTags. Trailing comments give the
|
||||||
//
|
// 4xxxx Modbus address and the DirectLOGIC V-memory reference.
|
||||||
// Width 16 — one register holds 4 BCD digits (0–9999).
|
|
||||||
// Wire value 0x1234 decodes to decimal 1234.
|
|
||||||
//
|
|
||||||
// Width 32 — a CDAB-ordered register pair (Address = low word, Address+1 = high word).
|
|
||||||
// Decoded decimal = high * 10000 + low (DirectLOGIC CDAB word order).
|
|
||||||
//
|
|
||||||
// Per-PLC overrides (see Plcs[].BcdTags below):
|
|
||||||
// Add — appends extra tags beyond what Global defines, or overrides a
|
|
||||||
// Global entry's Width when the same Address appears in both.
|
|
||||||
// Remove — removes specific addresses from the effective set for that PLC.
|
|
||||||
// Effective set = (Global ∪ Add) − Remove, resolved per PDU.
|
|
||||||
"BcdTags": {
|
"BcdTags": {
|
||||||
// Fleet-wide BCD tag list from tags.txt — applies to every PLC.
|
|
||||||
// Address = Modbus PDU-decimal = (4xxxx Modbus address − 40001), which also
|
|
||||||
// equals the DirectLOGIC V-memory address converted octal → decimal
|
|
||||||
// (e.g. 41549 / V3014 → 41549 − 40001 = 1548 ; octal 3014 = 1548).
|
|
||||||
// A 32-bit tag is ONE entry at its low/base address; it covers Address &
|
|
||||||
// Address+1 (CDAB: low word at Address, high word at Address+1).
|
|
||||||
// Name (optional) is a human-friendly label shown on the connection-detail
|
|
||||||
// debug view; it has no effect on rewriting. tags.txt's "Data Direction" is
|
|
||||||
// informational — the proxy rewrites BCD on whichever FC touches the address.
|
|
||||||
// CacheTtlMs (optional, per entry) opts a tag into the Phase-11 response cache;
|
|
||||||
// omitted / 0 = uncached (the default for every tag).
|
|
||||||
"Global": [
|
"Global": [
|
||||||
// ── 16-bit setpoints — BCD16, HMI-written ────────────────────────────
|
// 16-bit setpoints
|
||||||
{ "Address": 1536, "Width": 16, "Name": "Left ArgonSP" }, // 41537
|
{ "Address": 1536, "Width": 16, "Name": "Left ArgonSP" }, // 41537
|
||||||
{ "Address": 1539, "Width": 16, "Name": "Right ArgonSP" }, // 41540
|
{ "Address": 1539, "Width": 16, "Name": "Right ArgonSP" }, // 41540
|
||||||
{ "Address": 1544, "Width": 16, "Name": "Left ChlorineSP" }, // 41545 · V3010
|
{ "Address": 1544, "Width": 16, "Name": "Left ChlorineSP" }, // 41545 · V3010
|
||||||
@@ -47,7 +26,7 @@
|
|||||||
{ "Address": 1548, "Width": 16, "Name": "Left AirSP" }, // 41549 · V3014
|
{ "Address": 1548, "Width": 16, "Name": "Left AirSP" }, // 41549 · V3014
|
||||||
{ "Address": 1549, "Width": 16, "Name": "Right AirSP" }, // 41550 · V3015
|
{ "Address": 1549, "Width": 16, "Name": "Right AirSP" }, // 41550 · V3015
|
||||||
|
|
||||||
// ── 32-bit runtimes — BCD32, read; CDAB pair spans Address & Address+1 ─
|
// 32-bit runtimes — CDAB pair spanning Address and Address+1
|
||||||
{ "Address": 4616, "Width": 32, "Name": "MTA Runtime Left (min)" }, // 44617/44618 · V11010
|
{ "Address": 4616, "Width": 32, "Name": "MTA Runtime Left (min)" }, // 44617/44618 · V11010
|
||||||
{ "Address": 4618, "Width": 32, "Name": "MTA Runtime Right (min)" }, // 44619/44620 · V11012
|
{ "Address": 4618, "Width": 32, "Name": "MTA Runtime Right (min)" }, // 44619/44620 · V11012
|
||||||
{ "Address": 4626, "Width": 32, "Name": "FRR Runtime Left (min)" }, // 44627/44628 · V11022
|
{ "Address": 4626, "Width": 32, "Name": "FRR Runtime Left (min)" }, // 44627/44628 · V11022
|
||||||
@@ -55,166 +34,58 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── PLC list ────────────────────────────────────────────────────────────────────────
|
// One entry per PLC: upstream clients connect to ListenPort, the proxy forwards to
|
||||||
// Each entry maps one upstream proxy port → one backend PLC.
|
// Host:Port. ListenPort must be unique. Optional per-PLC "BcdTags": { "Add", "Remove" }.
|
||||||
// Upstream clients connect to ListenPort; the proxy forwards to Host:Port.
|
|
||||||
//
|
|
||||||
// IMPORTANT: H2-ECOM100 modules accept at most 4 simultaneous TCP connections.
|
|
||||||
// With the 1:1 upstream↔backend model, a fifth upstream client to the same proxy
|
|
||||||
// port will cause a backend connect failure and an immediate upstream disconnect.
|
|
||||||
"Plcs": [
|
"Plcs": [
|
||||||
{
|
{
|
||||||
"Name": "Z28061", // Human-readable name (shown on status page and in logs)
|
"Name": "Z28061",
|
||||||
"ListenPort": 5020, // Port the proxy listens on (upstream clients connect here)
|
"ListenPort": 5020,
|
||||||
"Host": "10.210.192.5", // PLC IP address or hostname
|
"Host": "10.210.192.5",
|
||||||
"Port": 502 // PLC Modbus TCP port (almost always 502)
|
"Port": 502
|
||||||
// No BcdTags override — uses the Global set as-is. Per-PLC overrides are
|
|
||||||
// available: "BcdTags": { "Add": [ ... ], "Remove": [ ... ] }.
|
|
||||||
}
|
}
|
||||||
// Add one entry per PLC. Ports must be unique per host. Typical fleet: 54 PLCs.
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// ── Admin port ──────────────────────────────────────────────────────────────────────
|
// Read-only HTTP status page / dashboard. Set to 0 to disable the admin endpoint.
|
||||||
// Read-only HTTP status page.
|
|
||||||
// GET / → self-contained HTML (auto-refreshes every 5 s)
|
|
||||||
// GET /status.json → same data as JSON for monitoring scrapers
|
|
||||||
//
|
|
||||||
// Authentication is assumed at the network layer (trusted internal segment).
|
|
||||||
// Set to 0 to disable the admin endpoint.
|
|
||||||
"AdminPort": 8080,
|
"AdminPort": 8080,
|
||||||
|
|
||||||
// ── Connection timeouts ─────────────────────────────────────────────────────────────
|
// Backend connect / request / graceful-shutdown timeouts (ms), plus TCP keepalive
|
||||||
|
// and the idle-backend FC03 heartbeat. BackendHeartbeatIdleMs must exceed
|
||||||
|
// BackendRequestTimeoutMs. See docs/Architecture/Keepalive.md.
|
||||||
"Connection": {
|
"Connection": {
|
||||||
// Max time (ms) to wait for a TCP connect to the PLC backend.
|
|
||||||
// Each Polly retry attempt gets its own copy of this timeout.
|
|
||||||
"BackendConnectTimeoutMs": 3000,
|
"BackendConnectTimeoutMs": 3000,
|
||||||
|
|
||||||
// Max time (ms) to wait for the PLC to respond to a forwarded PDU.
|
|
||||||
// Non-idempotent FC06/FC16 writes are one-shot — the upstream client
|
|
||||||
// is disconnected immediately on timeout (no retry).
|
|
||||||
"BackendRequestTimeoutMs": 3000,
|
"BackendRequestTimeoutMs": 3000,
|
||||||
|
|
||||||
// Max time (ms) to wait for in-flight PDUs to complete during graceful shutdown
|
|
||||||
// (sc.exe stop / Windows Service stop signal). After this deadline the coordinator
|
|
||||||
// cancels remaining work and proceeds. Keep at or below the SCM wait-hint (30 s).
|
|
||||||
"GracefulShutdownTimeoutMs": 10000,
|
"GracefulShutdownTimeoutMs": 10000,
|
||||||
|
|
||||||
// ── Keepalive / connection monitoring ───────────────────────────────────
|
|
||||||
// The DL205/DL260 ECOM does not emit TCP keepalives, so an idle backend
|
|
||||||
// socket can be silently dropped by a middlebox (switch, firewall, NAT)
|
|
||||||
// after 2-5 minutes. This section enables OS-level SO_KEEPALIVE on both
|
|
||||||
// backend and upstream sockets, and drives a periodic Modbus FC03 heartbeat
|
|
||||||
// on each idle backend socket so a dead path is detected before a real
|
|
||||||
// client request hits it. See docs/Architecture/Keepalive.md.
|
|
||||||
"Keepalive": {
|
"Keepalive": {
|
||||||
// Master switch. false → no SO_KEEPALIVE and no heartbeat; the proxy
|
|
||||||
// behaves exactly as a pre-keepalive build.
|
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
|
|
||||||
// SO_KEEPALIVE: idle time (ms) before the OS sends its first probe.
|
|
||||||
"TcpIdleTimeMs": 30000,
|
"TcpIdleTimeMs": 30000,
|
||||||
// SO_KEEPALIVE: interval (ms) between probes once the idle time elapses.
|
|
||||||
"TcpProbeIntervalMs": 5000,
|
"TcpProbeIntervalMs": 5000,
|
||||||
// SO_KEEPALIVE: unanswered probes before the OS declares the socket dead.
|
|
||||||
"TcpProbeCount": 4,
|
"TcpProbeCount": 4,
|
||||||
|
|
||||||
// Backend heartbeat: after this much backend idle (ms) the proxy issues a
|
|
||||||
// synthetic FC03 qty=1 read to keep the path warm and prove the ECOM is
|
|
||||||
// still answering Modbus. Must be greater than BackendRequestTimeoutMs.
|
|
||||||
"BackendHeartbeatIdleMs": 30000,
|
"BackendHeartbeatIdleMs": 30000,
|
||||||
// FC03 PDU address the heartbeat reads. 0 = V0, valid on DL205/DL260.
|
|
||||||
"BackendHeartbeatProbeAddress": 0
|
"BackendHeartbeatProbeAddress": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Resilience policies ─────────────────────────────────────────────────────────────
|
// Polly policies: backend-connect retry, listener-bind recovery, read coalescing.
|
||||||
|
// BackendConnect / ListenerRecovery are restart-only (not hot-reloaded).
|
||||||
"Resilience": {
|
"Resilience": {
|
||||||
|
"BackendConnect": { "MaxAttempts": 3, "BackoffMs": [ 100, 500, 2000 ] },
|
||||||
// Polly retry policy for backend TCP connect attempts.
|
"ListenerRecovery": { "InitialBackoffMs": [ 1000, 2000, 5000, 15000, 30000 ], "SteadyStateMs": 30000 },
|
||||||
// MaxAttempts: total connect tries (including the first).
|
"ReadCoalescing": { "Enabled": true, "MaxParties": 32 }
|
||||||
// BackoffMs: delay between each attempt (must have MaxAttempts−1 entries).
|
|
||||||
"BackendConnect": {
|
|
||||||
"MaxAttempts": 3,
|
|
||||||
"BackoffMs": [ 100, 500, 2000 ]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Polly recovery policy for listener bind failures.
|
|
||||||
// If a PLC's listen port can't be bound (in-use, bad IP, transient OS error),
|
|
||||||
// the supervisor retries according to this schedule.
|
|
||||||
// InitialBackoffMs: backoff per step (first N retries).
|
|
||||||
// SteadyStateMs: backoff for all subsequent retries (runs indefinitely).
|
|
||||||
"ListenerRecovery": {
|
|
||||||
"InitialBackoffMs": [ 1000, 2000, 5000, 15000, 30000 ],
|
|
||||||
"SteadyStateMs": 30000
|
|
||||||
},
|
|
||||||
|
|
||||||
// Phase 10 — in-flight read coalescing.
|
|
||||||
//
|
|
||||||
// When two or more upstream clients (HMI / historian / engineering workstation /
|
|
||||||
// gateway) issue the SAME FC03 or FC04 read while a matching backend round-trip is
|
|
||||||
// already in flight, the proxy attaches the late arrivals to the existing in-flight
|
|
||||||
// entry and fans the single PLC response out to every attached client — saving the
|
|
||||||
// ECOM's per-scan PDU budget on duplicated reads.
|
|
||||||
//
|
|
||||||
// Zero post-response staleness: coalescing operates ONLY between "first request
|
|
||||||
// sent to PLC" and "response received from PLC" (microseconds to ~10 ms typical).
|
|
||||||
// Each upstream client still sees its own MBAP transaction ID echoed correctly;
|
|
||||||
// the proxy is transparent.
|
|
||||||
//
|
|
||||||
// FC06 / FC16 writes are NEVER coalesced (non-idempotent). FC03 vs FC04 are
|
|
||||||
// separate Modbus tables and never share a coalescing key. Different unit IDs
|
|
||||||
// (multi-drop / gateway-backed setups) never coalesce.
|
|
||||||
//
|
|
||||||
// Enabled — master switch. Hot-reloadable; flipping to false leaves running
|
|
||||||
// coalesced entries to drain naturally.
|
|
||||||
// MaxParties — per-entry cap on attached parties. Past the cap, the next
|
|
||||||
// identical request opens a fresh backend round-trip (load-shedding
|
|
||||||
// safety valve for very fan-out-heavy fleets).
|
|
||||||
"ReadCoalescing": {
|
|
||||||
"Enabled": true,
|
|
||||||
"MaxParties": 32
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Response cache (Phase 11) — opt-in bounded-staleness cache ──────────────────
|
// Opt-in response cache — OFF by default per tag. A tag opts in via its CacheTtlMs
|
||||||
//
|
// (or a PLC's DefaultCacheTtlMs); these are service-wide safety knobs only.
|
||||||
// ⚠ DESIGN-CONTRACT PIVOT: with caching enabled the proxy is no longer purely
|
// See docs/Architecture/ResponseCache.md.
|
||||||
// transparent. Upstream FC03/FC04 reads for cache-enabled tags may return values
|
|
||||||
// up to CacheTtlMs MILLISECONDS OLD. Operators opt tags in by setting a non-zero
|
|
||||||
// CacheTtlMs on a BcdTagOptions entry (or DefaultCacheTtlMs on a PlcOptions entry).
|
|
||||||
//
|
|
||||||
// The cache is OFF BY DEFAULT for every tag. A deployment with NO TTL config (this
|
|
||||||
// section entirely absent and no BcdTags.*.CacheTtlMs / Plcs[i].DefaultCacheTtlMs)
|
|
||||||
// behaves IDENTICALLY to a pre-Phase-11 deployment — no behaviour change.
|
|
||||||
//
|
|
||||||
// AllowLongTtl — gate for any CacheTtlMs > 60_000. Reload validation
|
|
||||||
// rejects configs that exceed 60 s without this opt-in,
|
|
||||||
// to prevent accidentally-stale-for-an-hour deployments.
|
|
||||||
// MaxEntriesPerPlc — LRU cap per-PLC. Past this cap, the next insert evicts
|
|
||||||
// the least-recently-used entry. Defaults to 1000.
|
|
||||||
// EvictionIntervalMs — background eviction tick. Scans each PLC's cache and
|
|
||||||
// removes entries past their TTL. Defaults to 5000.
|
|
||||||
//
|
|
||||||
// Properties (full text in docs/Architecture/ResponseCache.md):
|
|
||||||
// * Cache hits SHORT-CIRCUIT coalescing entirely (cache → coalesce → backend).
|
|
||||||
// * Successful FC06/FC16 write responses invalidate every cached FC03/FC04 entry
|
|
||||||
// whose address range OVERLAPS the write — not just exact-key match.
|
|
||||||
// * Multi-tag read range: effective TTL = min(TTLs). Any tag with TTL=0 in the
|
|
||||||
// range disables caching for the whole read.
|
|
||||||
// * Cache stores POST-rewriter bytes; hits never re-invoke the BCD rewriter.
|
|
||||||
// * Tag-list hot-reload flushes the affected PLC's whole cache.
|
|
||||||
// * No persistence — process restart wipes the cache.
|
|
||||||
"Cache": {
|
"Cache": {
|
||||||
"AllowLongTtl": false,
|
"AllowLongTtl": false,
|
||||||
"MaxEntriesPerPlc": 1000,
|
"MaxEntriesPerPlc": 1000,
|
||||||
"EvictionIntervalMs": 5000
|
"EvictionIntervalMs": 5000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Serilog ─────────────────────────────────────────────────────────────────────────────
|
// Structured logging — console + daily rolling file under %ProgramData%\mbproxy\logs.
|
||||||
// Structured log output. Default: Information level, rolling-file under ProgramData.
|
// Error+ events also go to the Windows Application Event Log under the SCM (wired in
|
||||||
// The EventLogBridge writes Error+ events to the Windows Application Event Log
|
// code, not here). See docs/Operations/Troubleshooting.md.
|
||||||
// automatically when the service runs under the SCM (not under dotnet run).
|
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
@@ -234,8 +105,6 @@
|
|||||||
{
|
{
|
||||||
"Name": "File",
|
"Name": "File",
|
||||||
"Args": {
|
"Args": {
|
||||||
// Rolling log: one file per day, kept for 30 days.
|
|
||||||
// Survives uninstall — logs are archived to %ProgramData%\mbproxy.archived-<ts>\.
|
|
||||||
"path": "C:\\ProgramData\\mbproxy\\logs\\mbproxy-.log",
|
"path": "C:\\ProgramData\\mbproxy\\logs\\mbproxy-.log",
|
||||||
"rollingInterval": "Day",
|
"rollingInterval": "Day",
|
||||||
"retainedFileCountLimit": 30,
|
"retainedFileCountLimit": 30,
|
||||||
|
|||||||
@@ -1,47 +1,24 @@
|
|||||||
// mbproxy configuration template (Linux / systemd) — copy to /etc/mbproxy/appsettings.json
|
// mbproxy configuration (Linux / systemd). Copy to /etc/mbproxy/appsettings.json and
|
||||||
// and edit before starting the service.
|
// edit before starting the service. install.sh seeds this file only when none exists —
|
||||||
|
// an existing appsettings.json is always preserved across reinstalls.
|
||||||
//
|
//
|
||||||
// The .NET configuration loader accepts // and /* */ comments in JSON files
|
// JSONC: // and /* */ comments are accepted. The file is hot-reloaded on save.
|
||||||
// (JSONC semantics) when using the default Host.CreateApplicationBuilder path.
|
// This is the Linux counterpart of mbproxy.config.template.json — identical keys, with
|
||||||
|
// a /var/log/mbproxy log path; shipped as appsettings.json by a `dotnet publish -r linux-*`.
|
||||||
//
|
//
|
||||||
// IMPORTANT: install.sh overwrites this file at the destination ONLY if no
|
// FULL REFERENCE — every key, type, default, range, validation rule and hot-reload
|
||||||
// appsettings.json already exists there. An existing file is always preserved.
|
// behaviour — lives in docs/Operations/Configuration.md. The notes below are brief
|
||||||
//
|
// pointers only; consult that document before editing.
|
||||||
// This is the Linux counterpart of mbproxy.config.template.json — identical except
|
|
||||||
// for the rolling-log path (/var/log/mbproxy) and a few platform notes. It is shipped
|
|
||||||
// as appsettings.json by a `dotnet publish -r linux-*` build.
|
|
||||||
{
|
{
|
||||||
"Mbproxy": {
|
"Mbproxy": {
|
||||||
|
|
||||||
// ── Global BCD tag list ─────────────────────────────────────────────────────────────
|
// Fleet-wide BCD tag list — applies to every PLC. Each entry: Address (Modbus
|
||||||
// These tags apply to EVERY PLC by default.
|
// PDU-decimal), Width (16 or 32), optional Name (debug-view label) and CacheTtlMs.
|
||||||
// Each entry: Address (Modbus PDU address, decimal), Width (16 or 32 bits).
|
// Per-PLC Add/Remove overrides go under Plcs[].BcdTags. Trailing comments give the
|
||||||
//
|
// 4xxxx Modbus address and the DirectLOGIC V-memory reference.
|
||||||
// Width 16 — one register holds 4 BCD digits (0–9999).
|
|
||||||
// Wire value 0x1234 decodes to decimal 1234.
|
|
||||||
//
|
|
||||||
// Width 32 — a CDAB-ordered register pair (Address = low word, Address+1 = high word).
|
|
||||||
// Decoded decimal = high * 10000 + low (DirectLOGIC CDAB word order).
|
|
||||||
//
|
|
||||||
// Per-PLC overrides (see Plcs[].BcdTags below):
|
|
||||||
// Add — appends extra tags beyond what Global defines, or overrides a
|
|
||||||
// Global entry's Width when the same Address appears in both.
|
|
||||||
// Remove — removes specific addresses from the effective set for that PLC.
|
|
||||||
// Effective set = (Global ∪ Add) − Remove, resolved per PDU.
|
|
||||||
"BcdTags": {
|
"BcdTags": {
|
||||||
// Fleet-wide BCD tag list from tags.txt — applies to every PLC.
|
|
||||||
// Address = Modbus PDU-decimal = (4xxxx Modbus address − 40001), which also
|
|
||||||
// equals the DirectLOGIC V-memory address converted octal → decimal
|
|
||||||
// (e.g. 41549 / V3014 → 41549 − 40001 = 1548 ; octal 3014 = 1548).
|
|
||||||
// A 32-bit tag is ONE entry at its low/base address; it covers Address &
|
|
||||||
// Address+1 (CDAB: low word at Address, high word at Address+1).
|
|
||||||
// Name (optional) is a human-friendly label shown on the connection-detail
|
|
||||||
// debug view; it has no effect on rewriting. tags.txt's "Data Direction" is
|
|
||||||
// informational — the proxy rewrites BCD on whichever FC touches the address.
|
|
||||||
// CacheTtlMs (optional, per entry) opts a tag into the Phase-11 response cache;
|
|
||||||
// omitted / 0 = uncached (the default for every tag).
|
|
||||||
"Global": [
|
"Global": [
|
||||||
// ── 16-bit setpoints — BCD16, HMI-written ────────────────────────────
|
// 16-bit setpoints
|
||||||
{ "Address": 1536, "Width": 16, "Name": "Left ArgonSP" }, // 41537
|
{ "Address": 1536, "Width": 16, "Name": "Left ArgonSP" }, // 41537
|
||||||
{ "Address": 1539, "Width": 16, "Name": "Right ArgonSP" }, // 41540
|
{ "Address": 1539, "Width": 16, "Name": "Right ArgonSP" }, // 41540
|
||||||
{ "Address": 1544, "Width": 16, "Name": "Left ChlorineSP" }, // 41545 · V3010
|
{ "Address": 1544, "Width": 16, "Name": "Left ChlorineSP" }, // 41545 · V3010
|
||||||
@@ -51,7 +28,7 @@
|
|||||||
{ "Address": 1548, "Width": 16, "Name": "Left AirSP" }, // 41549 · V3014
|
{ "Address": 1548, "Width": 16, "Name": "Left AirSP" }, // 41549 · V3014
|
||||||
{ "Address": 1549, "Width": 16, "Name": "Right AirSP" }, // 41550 · V3015
|
{ "Address": 1549, "Width": 16, "Name": "Right AirSP" }, // 41550 · V3015
|
||||||
|
|
||||||
// ── 32-bit runtimes — BCD32, read; CDAB pair spans Address & Address+1 ─
|
// 32-bit runtimes — CDAB pair spanning Address and Address+1
|
||||||
{ "Address": 4616, "Width": 32, "Name": "MTA Runtime Left (min)" }, // 44617/44618 · V11010
|
{ "Address": 4616, "Width": 32, "Name": "MTA Runtime Left (min)" }, // 44617/44618 · V11010
|
||||||
{ "Address": 4618, "Width": 32, "Name": "MTA Runtime Right (min)" }, // 44619/44620 · V11012
|
{ "Address": 4618, "Width": 32, "Name": "MTA Runtime Right (min)" }, // 44619/44620 · V11012
|
||||||
{ "Address": 4626, "Width": 32, "Name": "FRR Runtime Left (min)" }, // 44627/44628 · V11022
|
{ "Address": 4626, "Width": 32, "Name": "FRR Runtime Left (min)" }, // 44627/44628 · V11022
|
||||||
@@ -59,167 +36,58 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── PLC list ────────────────────────────────────────────────────────────────────────
|
// One entry per PLC: upstream clients connect to ListenPort, the proxy forwards to
|
||||||
// Each entry maps one upstream proxy port → one backend PLC.
|
// Host:Port. ListenPort must be unique. Optional per-PLC "BcdTags": { "Add", "Remove" }.
|
||||||
// Upstream clients connect to ListenPort; the proxy forwards to Host:Port.
|
|
||||||
//
|
|
||||||
// IMPORTANT: H2-ECOM100 modules accept at most 4 simultaneous TCP connections.
|
|
||||||
// With the 1:1 upstream↔backend model, a fifth upstream client to the same proxy
|
|
||||||
// port will cause a backend connect failure and an immediate upstream disconnect.
|
|
||||||
"Plcs": [
|
"Plcs": [
|
||||||
{
|
{
|
||||||
"Name": "Z28061", // Human-readable name (shown on status page and in logs)
|
"Name": "Z28061",
|
||||||
"ListenPort": 5020, // Port the proxy listens on (upstream clients connect here)
|
"ListenPort": 5020,
|
||||||
"Host": "10.210.192.5", // PLC IP address or hostname
|
"Host": "10.210.192.5",
|
||||||
"Port": 502 // PLC Modbus TCP port (almost always 502)
|
"Port": 502
|
||||||
// No BcdTags override — uses the Global set as-is. Per-PLC overrides are
|
|
||||||
// available: "BcdTags": { "Add": [ ... ], "Remove": [ ... ] }.
|
|
||||||
}
|
}
|
||||||
// Add one entry per PLC. Ports must be unique per host. Typical fleet: 54 PLCs.
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// ── Admin port ──────────────────────────────────────────────────────────────────────
|
// Read-only HTTP status page / dashboard. Set to 0 to disable the admin endpoint.
|
||||||
// Read-only HTTP status page.
|
|
||||||
// GET / → self-contained HTML (auto-refreshes every 5 s)
|
|
||||||
// GET /status.json → same data as JSON for monitoring scrapers
|
|
||||||
//
|
|
||||||
// Authentication is assumed at the network layer (trusted internal segment).
|
|
||||||
// Set to 0 to disable the admin endpoint.
|
|
||||||
"AdminPort": 8080,
|
"AdminPort": 8080,
|
||||||
|
|
||||||
// ── Connection timeouts ─────────────────────────────────────────────────────────────
|
// Backend connect / request / graceful-shutdown timeouts (ms), plus TCP keepalive
|
||||||
|
// and the idle-backend FC03 heartbeat. BackendHeartbeatIdleMs must exceed
|
||||||
|
// BackendRequestTimeoutMs. See docs/Architecture/Keepalive.md.
|
||||||
"Connection": {
|
"Connection": {
|
||||||
// Max time (ms) to wait for a TCP connect to the PLC backend.
|
|
||||||
// Each Polly retry attempt gets its own copy of this timeout.
|
|
||||||
"BackendConnectTimeoutMs": 3000,
|
"BackendConnectTimeoutMs": 3000,
|
||||||
|
|
||||||
// Max time (ms) to wait for the PLC to respond to a forwarded PDU.
|
|
||||||
// Non-idempotent FC06/FC16 writes are one-shot — the upstream client
|
|
||||||
// is disconnected immediately on timeout (no retry).
|
|
||||||
"BackendRequestTimeoutMs": 3000,
|
"BackendRequestTimeoutMs": 3000,
|
||||||
|
|
||||||
// Max time (ms) to wait for in-flight PDUs to complete during graceful shutdown
|
|
||||||
// (systemctl stop → SIGTERM). After this deadline the coordinator cancels
|
|
||||||
// remaining work and proceeds. Keep at or below the unit's TimeoutStopSec.
|
|
||||||
"GracefulShutdownTimeoutMs": 10000,
|
"GracefulShutdownTimeoutMs": 10000,
|
||||||
|
|
||||||
// ── Keepalive / connection monitoring ───────────────────────────────────
|
|
||||||
// The DL205/DL260 ECOM does not emit TCP keepalives, so an idle backend
|
|
||||||
// socket can be silently dropped by a middlebox (switch, firewall, NAT)
|
|
||||||
// after 2-5 minutes. This section enables OS-level SO_KEEPALIVE on both
|
|
||||||
// backend and upstream sockets, and drives a periodic Modbus FC03 heartbeat
|
|
||||||
// on each idle backend socket so a dead path is detected before a real
|
|
||||||
// client request hits it. See docs/Architecture/Keepalive.md.
|
|
||||||
"Keepalive": {
|
"Keepalive": {
|
||||||
// Master switch. false → no SO_KEEPALIVE and no heartbeat; the proxy
|
|
||||||
// behaves exactly as a pre-keepalive build.
|
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
|
|
||||||
// SO_KEEPALIVE: idle time (ms) before the OS sends its first probe.
|
|
||||||
"TcpIdleTimeMs": 30000,
|
"TcpIdleTimeMs": 30000,
|
||||||
// SO_KEEPALIVE: interval (ms) between probes once the idle time elapses.
|
|
||||||
"TcpProbeIntervalMs": 5000,
|
"TcpProbeIntervalMs": 5000,
|
||||||
// SO_KEEPALIVE: unanswered probes before the OS declares the socket dead.
|
|
||||||
"TcpProbeCount": 4,
|
"TcpProbeCount": 4,
|
||||||
|
|
||||||
// Backend heartbeat: after this much backend idle (ms) the proxy issues a
|
|
||||||
// synthetic FC03 qty=1 read to keep the path warm and prove the ECOM is
|
|
||||||
// still answering Modbus. Must be greater than BackendRequestTimeoutMs.
|
|
||||||
"BackendHeartbeatIdleMs": 30000,
|
"BackendHeartbeatIdleMs": 30000,
|
||||||
// FC03 PDU address the heartbeat reads. 0 = V0, valid on DL205/DL260.
|
|
||||||
"BackendHeartbeatProbeAddress": 0
|
"BackendHeartbeatProbeAddress": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Resilience policies ─────────────────────────────────────────────────────────────
|
// Polly policies: backend-connect retry, listener-bind recovery, read coalescing.
|
||||||
|
// BackendConnect / ListenerRecovery are restart-only (not hot-reloaded).
|
||||||
"Resilience": {
|
"Resilience": {
|
||||||
|
"BackendConnect": { "MaxAttempts": 3, "BackoffMs": [ 100, 500, 2000 ] },
|
||||||
// Polly retry policy for backend TCP connect attempts.
|
"ListenerRecovery": { "InitialBackoffMs": [ 1000, 2000, 5000, 15000, 30000 ], "SteadyStateMs": 30000 },
|
||||||
// MaxAttempts: total connect tries (including the first).
|
"ReadCoalescing": { "Enabled": true, "MaxParties": 32 }
|
||||||
// BackoffMs: delay between each attempt (must have MaxAttempts−1 entries).
|
|
||||||
"BackendConnect": {
|
|
||||||
"MaxAttempts": 3,
|
|
||||||
"BackoffMs": [ 100, 500, 2000 ]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Polly recovery policy for listener bind failures.
|
|
||||||
// If a PLC's listen port can't be bound (in-use, bad IP, transient OS error),
|
|
||||||
// the supervisor retries according to this schedule.
|
|
||||||
// InitialBackoffMs: backoff per step (first N retries).
|
|
||||||
// SteadyStateMs: backoff for all subsequent retries (runs indefinitely).
|
|
||||||
"ListenerRecovery": {
|
|
||||||
"InitialBackoffMs": [ 1000, 2000, 5000, 15000, 30000 ],
|
|
||||||
"SteadyStateMs": 30000
|
|
||||||
},
|
|
||||||
|
|
||||||
// Phase 10 — in-flight read coalescing.
|
|
||||||
//
|
|
||||||
// When two or more upstream clients (HMI / historian / engineering workstation /
|
|
||||||
// gateway) issue the SAME FC03 or FC04 read while a matching backend round-trip is
|
|
||||||
// already in flight, the proxy attaches the late arrivals to the existing in-flight
|
|
||||||
// entry and fans the single PLC response out to every attached client — saving the
|
|
||||||
// ECOM's per-scan PDU budget on duplicated reads.
|
|
||||||
//
|
|
||||||
// Zero post-response staleness: coalescing operates ONLY between "first request
|
|
||||||
// sent to PLC" and "response received from PLC" (microseconds to ~10 ms typical).
|
|
||||||
// Each upstream client still sees its own MBAP transaction ID echoed correctly;
|
|
||||||
// the proxy is transparent.
|
|
||||||
//
|
|
||||||
// FC06 / FC16 writes are NEVER coalesced (non-idempotent). FC03 vs FC04 are
|
|
||||||
// separate Modbus tables and never share a coalescing key. Different unit IDs
|
|
||||||
// (multi-drop / gateway-backed setups) never coalesce.
|
|
||||||
//
|
|
||||||
// Enabled — master switch. Hot-reloadable; flipping to false leaves running
|
|
||||||
// coalesced entries to drain naturally.
|
|
||||||
// MaxParties — per-entry cap on attached parties. Past the cap, the next
|
|
||||||
// identical request opens a fresh backend round-trip (load-shedding
|
|
||||||
// safety valve for very fan-out-heavy fleets).
|
|
||||||
"ReadCoalescing": {
|
|
||||||
"Enabled": true,
|
|
||||||
"MaxParties": 32
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Response cache (Phase 11) — opt-in bounded-staleness cache ──────────────────
|
// Opt-in response cache — OFF by default per tag. A tag opts in via its CacheTtlMs
|
||||||
//
|
// (or a PLC's DefaultCacheTtlMs); these are service-wide safety knobs only.
|
||||||
// ⚠ DESIGN-CONTRACT PIVOT: with caching enabled the proxy is no longer purely
|
// See docs/Architecture/ResponseCache.md.
|
||||||
// transparent. Upstream FC03/FC04 reads for cache-enabled tags may return values
|
|
||||||
// up to CacheTtlMs MILLISECONDS OLD. Operators opt tags in by setting a non-zero
|
|
||||||
// CacheTtlMs on a BcdTagOptions entry (or DefaultCacheTtlMs on a PlcOptions entry).
|
|
||||||
//
|
|
||||||
// The cache is OFF BY DEFAULT for every tag. A deployment with NO TTL config (this
|
|
||||||
// section entirely absent and no BcdTags.*.CacheTtlMs / Plcs[i].DefaultCacheTtlMs)
|
|
||||||
// behaves IDENTICALLY to a pre-Phase-11 deployment — no behaviour change.
|
|
||||||
//
|
|
||||||
// AllowLongTtl — gate for any CacheTtlMs > 60_000. Reload validation
|
|
||||||
// rejects configs that exceed 60 s without this opt-in,
|
|
||||||
// to prevent accidentally-stale-for-an-hour deployments.
|
|
||||||
// MaxEntriesPerPlc — LRU cap per-PLC. Past this cap, the next insert evicts
|
|
||||||
// the least-recently-used entry. Defaults to 1000.
|
|
||||||
// EvictionIntervalMs — background eviction tick. Scans each PLC's cache and
|
|
||||||
// removes entries past their TTL. Defaults to 5000.
|
|
||||||
//
|
|
||||||
// Properties (full text in docs/Architecture/ResponseCache.md):
|
|
||||||
// * Cache hits SHORT-CIRCUIT coalescing entirely (cache → coalesce → backend).
|
|
||||||
// * Successful FC06/FC16 write responses invalidate every cached FC03/FC04 entry
|
|
||||||
// whose address range OVERLAPS the write — not just exact-key match.
|
|
||||||
// * Multi-tag read range: effective TTL = min(TTLs). Any tag with TTL=0 in the
|
|
||||||
// range disables caching for the whole read.
|
|
||||||
// * Cache stores POST-rewriter bytes; hits never re-invoke the BCD rewriter.
|
|
||||||
// * Tag-list hot-reload flushes the affected PLC's whole cache.
|
|
||||||
// * No persistence — process restart wipes the cache.
|
|
||||||
"Cache": {
|
"Cache": {
|
||||||
"AllowLongTtl": false,
|
"AllowLongTtl": false,
|
||||||
"MaxEntriesPerPlc": 1000,
|
"MaxEntriesPerPlc": 1000,
|
||||||
"EvictionIntervalMs": 5000
|
"EvictionIntervalMs": 5000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Serilog ─────────────────────────────────────────────────────────────────────────────
|
// Structured logging — console (captured by systemd-journald) + daily rolling file
|
||||||
// Structured log output. Default: Information level, console + rolling-file.
|
// under /var/log/mbproxy. Error+ events also go to local syslog under systemd (wired
|
||||||
// The console sink is captured by systemd-journald (view with `journalctl -u mbproxy`).
|
// in code, not here). See docs/Operations/Troubleshooting.md.
|
||||||
// In addition, when mbproxy runs as a systemd service the SyslogBridge writes Error+
|
|
||||||
// events to the local syslog with proper RFC5424 severity (wired in code, not here).
|
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
@@ -239,9 +107,6 @@
|
|||||||
{
|
{
|
||||||
"Name": "File",
|
"Name": "File",
|
||||||
"Args": {
|
"Args": {
|
||||||
// Rolling log: one file per day, kept for 30 days, under /var/log/mbproxy
|
|
||||||
// (created by install.sh and owned by the mbproxy service account).
|
|
||||||
// Survives uninstall — uninstall.sh archives logs to /var/log/mbproxy.archived-<ts>.
|
|
||||||
"path": "/var/log/mbproxy/mbproxy-.log",
|
"path": "/var/log/mbproxy/mbproxy-.log",
|
||||||
"rollingInterval": "Day",
|
"rollingInterval": "Day",
|
||||||
"retainedFileCountLimit": 30,
|
"retainedFileCountLimit": 30,
|
||||||
|
|||||||
Reference in New Issue
Block a user