diff --git a/mbproxy/install/mbproxy.config.template.json b/mbproxy/install/mbproxy.config.template.json index 1fe69f6..0adea7f 100644 --- a/mbproxy/install/mbproxy.config.template.json +++ b/mbproxy/install/mbproxy.config.template.json @@ -1,43 +1,22 @@ -// mbproxy configuration template — copy to %ProgramData%\mbproxy\appsettings.json -// and edit before starting the service. +// mbproxy configuration. Copy to %ProgramData%\mbproxy\appsettings.json and edit +// 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 semantics) when using the default Host.CreateApplicationBuilder path. +// JSONC: // and /* */ comments are accepted. The file is hot-reloaded on save. // -// IMPORTANT: This file is overwritten on each install ONLY if no appsettings.json -// already exists at the destination. An existing file is always preserved. +// FULL REFERENCE — every key, type, default, range, validation rule and hot-reload +// behaviour — lives in docs/Operations/Configuration.md. The notes below are brief +// pointers only; consult that document before editing. { "Mbproxy": { - // ── Global BCD tag list ───────────────────────────────────────────────────────────── - // These tags apply to EVERY PLC by default. - // Each entry: Address (Modbus PDU address, decimal), Width (16 or 32 bits). - // - // 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. + // Fleet-wide BCD tag list — applies to every PLC. Each entry: Address (Modbus + // PDU-decimal), Width (16 or 32), optional Name (debug-view label) and CacheTtlMs. + // Per-PLC Add/Remove overrides go under Plcs[].BcdTags. Trailing comments give the + // 4xxxx Modbus address and the DirectLOGIC V-memory reference. "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": [ - // ── 16-bit setpoints — BCD16, HMI-written ──────────────────────────── + // 16-bit setpoints { "Address": 1536, "Width": 16, "Name": "Left ArgonSP" }, // 41537 { "Address": 1539, "Width": 16, "Name": "Right ArgonSP" }, // 41540 { "Address": 1544, "Width": 16, "Name": "Left ChlorineSP" }, // 41545 · V3010 @@ -47,7 +26,7 @@ { "Address": 1548, "Width": 16, "Name": "Left AirSP" }, // 41549 · V3014 { "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": 4618, "Width": 32, "Name": "MTA Runtime Right (min)" }, // 44619/44620 · V11012 { "Address": 4626, "Width": 32, "Name": "FRR Runtime Left (min)" }, // 44627/44628 · V11022 @@ -55,166 +34,58 @@ ] }, - // ── PLC list ──────────────────────────────────────────────────────────────────────── - // Each entry maps one upstream proxy port → one backend PLC. - // 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. + // One entry per PLC: upstream clients connect to ListenPort, the proxy forwards to + // Host:Port. ListenPort must be unique. Optional per-PLC "BcdTags": { "Add", "Remove" }. "Plcs": [ { - "Name": "Z28061", // Human-readable name (shown on status page and in logs) - "ListenPort": 5020, // Port the proxy listens on (upstream clients connect here) - "Host": "10.210.192.5", // PLC IP address or hostname - "Port": 502 // PLC Modbus TCP port (almost always 502) - // No BcdTags override — uses the Global set as-is. Per-PLC overrides are - // available: "BcdTags": { "Add": [ ... ], "Remove": [ ... ] }. + "Name": "Z28061", + "ListenPort": 5020, + "Host": "10.210.192.5", + "Port": 502 } - // Add one entry per PLC. Ports must be unique per host. Typical fleet: 54 PLCs. ], - // ── Admin port ────────────────────────────────────────────────────────────────────── - // 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. + // Read-only HTTP status page / dashboard. Set to 0 to disable the admin endpoint. "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": { - // 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, - - // 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, - - // 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, - - // ── 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": { - // Master switch. false → no SO_KEEPALIVE and no heartbeat; the proxy - // behaves exactly as a pre-keepalive build. "Enabled": true, - - // SO_KEEPALIVE: idle time (ms) before the OS sends its first probe. "TcpIdleTimeMs": 30000, - // SO_KEEPALIVE: interval (ms) between probes once the idle time elapses. "TcpProbeIntervalMs": 5000, - // SO_KEEPALIVE: unanswered probes before the OS declares the socket dead. "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, - // FC03 PDU address the heartbeat reads. 0 = V0, valid on DL205/DL260. "BackendHeartbeatProbeAddress": 0 } }, - // ── Resilience policies ───────────────────────────────────────────────────────────── + // Polly policies: backend-connect retry, listener-bind recovery, read coalescing. + // BackendConnect / ListenerRecovery are restart-only (not hot-reloaded). "Resilience": { - - // Polly retry policy for backend TCP connect attempts. - // MaxAttempts: total connect tries (including the first). - // 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 - } + "BackendConnect": { "MaxAttempts": 3, "BackoffMs": [ 100, 500, 2000 ] }, + "ListenerRecovery": { "InitialBackoffMs": [ 1000, 2000, 5000, 15000, 30000 ], "SteadyStateMs": 30000 }, + "ReadCoalescing": { "Enabled": true, "MaxParties": 32 } }, - // ── Response cache (Phase 11) — opt-in bounded-staleness cache ────────────────── - // - // ⚠ DESIGN-CONTRACT PIVOT: with caching enabled the proxy is no longer purely - // 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. + // 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. + // See docs/Architecture/ResponseCache.md. "Cache": { - "AllowLongTtl": false, - "MaxEntriesPerPlc": 1000, + "AllowLongTtl": false, + "MaxEntriesPerPlc": 1000, "EvictionIntervalMs": 5000 } }, - // ── Serilog ───────────────────────────────────────────────────────────────────────────── - // Structured log output. Default: Information level, rolling-file under ProgramData. - // The EventLogBridge writes Error+ events to the Windows Application Event Log - // automatically when the service runs under the SCM (not under dotnet run). + // Structured logging — console + daily rolling file under %ProgramData%\mbproxy\logs. + // Error+ events also go to the Windows Application Event Log under the SCM (wired in + // code, not here). See docs/Operations/Troubleshooting.md. "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], "MinimumLevel": { @@ -234,8 +105,6 @@ { "Name": "File", "Args": { - // Rolling log: one file per day, kept for 30 days. - // Survives uninstall — logs are archived to %ProgramData%\mbproxy.archived-\. "path": "C:\\ProgramData\\mbproxy\\logs\\mbproxy-.log", "rollingInterval": "Day", "retainedFileCountLimit": 30, diff --git a/mbproxy/install/mbproxy.linux.config.template.json b/mbproxy/install/mbproxy.linux.config.template.json index 07405d4..51aecc0 100644 --- a/mbproxy/install/mbproxy.linux.config.template.json +++ b/mbproxy/install/mbproxy.linux.config.template.json @@ -1,47 +1,24 @@ -// mbproxy configuration template (Linux / systemd) — copy to /etc/mbproxy/appsettings.json -// and edit before starting the service. +// mbproxy configuration (Linux / systemd). Copy to /etc/mbproxy/appsettings.json and +// 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 semantics) when using the default Host.CreateApplicationBuilder path. +// JSONC: // and /* */ comments are accepted. The file is hot-reloaded on save. +// 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 -// appsettings.json already exists there. An existing file is always preserved. -// -// 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. +// FULL REFERENCE — every key, type, default, range, validation rule and hot-reload +// behaviour — lives in docs/Operations/Configuration.md. The notes below are brief +// pointers only; consult that document before editing. { "Mbproxy": { - // ── Global BCD tag list ───────────────────────────────────────────────────────────── - // These tags apply to EVERY PLC by default. - // Each entry: Address (Modbus PDU address, decimal), Width (16 or 32 bits). - // - // 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. + // Fleet-wide BCD tag list — applies to every PLC. Each entry: Address (Modbus + // PDU-decimal), Width (16 or 32), optional Name (debug-view label) and CacheTtlMs. + // Per-PLC Add/Remove overrides go under Plcs[].BcdTags. Trailing comments give the + // 4xxxx Modbus address and the DirectLOGIC V-memory reference. "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": [ - // ── 16-bit setpoints — BCD16, HMI-written ──────────────────────────── + // 16-bit setpoints { "Address": 1536, "Width": 16, "Name": "Left ArgonSP" }, // 41537 { "Address": 1539, "Width": 16, "Name": "Right ArgonSP" }, // 41540 { "Address": 1544, "Width": 16, "Name": "Left ChlorineSP" }, // 41545 · V3010 @@ -51,7 +28,7 @@ { "Address": 1548, "Width": 16, "Name": "Left AirSP" }, // 41549 · V3014 { "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": 4618, "Width": 32, "Name": "MTA Runtime Right (min)" }, // 44619/44620 · V11012 { "Address": 4626, "Width": 32, "Name": "FRR Runtime Left (min)" }, // 44627/44628 · V11022 @@ -59,167 +36,58 @@ ] }, - // ── PLC list ──────────────────────────────────────────────────────────────────────── - // Each entry maps one upstream proxy port → one backend PLC. - // 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. + // One entry per PLC: upstream clients connect to ListenPort, the proxy forwards to + // Host:Port. ListenPort must be unique. Optional per-PLC "BcdTags": { "Add", "Remove" }. "Plcs": [ { - "Name": "Z28061", // Human-readable name (shown on status page and in logs) - "ListenPort": 5020, // Port the proxy listens on (upstream clients connect here) - "Host": "10.210.192.5", // PLC IP address or hostname - "Port": 502 // PLC Modbus TCP port (almost always 502) - // No BcdTags override — uses the Global set as-is. Per-PLC overrides are - // available: "BcdTags": { "Add": [ ... ], "Remove": [ ... ] }. + "Name": "Z28061", + "ListenPort": 5020, + "Host": "10.210.192.5", + "Port": 502 } - // Add one entry per PLC. Ports must be unique per host. Typical fleet: 54 PLCs. ], - // ── Admin port ────────────────────────────────────────────────────────────────────── - // 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. + // Read-only HTTP status page / dashboard. Set to 0 to disable the admin endpoint. "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": { - // 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, - - // 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, - - // 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, - - // ── 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": { - // Master switch. false → no SO_KEEPALIVE and no heartbeat; the proxy - // behaves exactly as a pre-keepalive build. "Enabled": true, - - // SO_KEEPALIVE: idle time (ms) before the OS sends its first probe. "TcpIdleTimeMs": 30000, - // SO_KEEPALIVE: interval (ms) between probes once the idle time elapses. "TcpProbeIntervalMs": 5000, - // SO_KEEPALIVE: unanswered probes before the OS declares the socket dead. "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, - // FC03 PDU address the heartbeat reads. 0 = V0, valid on DL205/DL260. "BackendHeartbeatProbeAddress": 0 } }, - // ── Resilience policies ───────────────────────────────────────────────────────────── + // Polly policies: backend-connect retry, listener-bind recovery, read coalescing. + // BackendConnect / ListenerRecovery are restart-only (not hot-reloaded). "Resilience": { - - // Polly retry policy for backend TCP connect attempts. - // MaxAttempts: total connect tries (including the first). - // 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 - } + "BackendConnect": { "MaxAttempts": 3, "BackoffMs": [ 100, 500, 2000 ] }, + "ListenerRecovery": { "InitialBackoffMs": [ 1000, 2000, 5000, 15000, 30000 ], "SteadyStateMs": 30000 }, + "ReadCoalescing": { "Enabled": true, "MaxParties": 32 } }, - // ── Response cache (Phase 11) — opt-in bounded-staleness cache ────────────────── - // - // ⚠ DESIGN-CONTRACT PIVOT: with caching enabled the proxy is no longer purely - // 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. + // 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. + // See docs/Architecture/ResponseCache.md. "Cache": { - "AllowLongTtl": false, - "MaxEntriesPerPlc": 1000, + "AllowLongTtl": false, + "MaxEntriesPerPlc": 1000, "EvictionIntervalMs": 5000 } }, - // ── Serilog ───────────────────────────────────────────────────────────────────────────── - // Structured log output. Default: Information level, console + rolling-file. - // The console sink is captured by systemd-journald (view with `journalctl -u mbproxy`). - // 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). + // Structured logging — console (captured by systemd-journald) + daily rolling file + // under /var/log/mbproxy. Error+ events also go to local syslog under systemd (wired + // in code, not here). See docs/Operations/Troubleshooting.md. "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], "MinimumLevel": { @@ -239,9 +107,6 @@ { "Name": "File", "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-. "path": "/var/log/mbproxy/mbproxy-.log", "rollingInterval": "Day", "retainedFileCountLimit": 30,