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>
mbproxy
A .NET 10 background service — a Windows Service or a Linux systemd unit — that sits inline as a Modbus TCP proxy in front of a fleet of AutomationDirect DirectLOGIC DL205/DL260 controllers, rewriting BCD-encoded registers bidirectionally so upstream clients can read and write them as plain integers. The proxy also offers an opt-in per-tag response cache (default OFF) for FC03/FC04 reads with bounded operator-configured staleness — see docs/Architecture/ResponseCache.md before enabling it.
⚠ 32-bit BCD wire format is "two base-10000 digits in CDAB", not standard CDAB binary Int32. A 32-bit BCD tag at address
Adecodes asdecimal = high * 10_000 + lowwherelowis the register atAandhighis the register atA+1. Each word independently must be 0–9999. Standard Modbus clients (NModbus, FluentModbus, Wonderware DAServer) that interpret CDAB as straight binary Int32 will silently corrupt any value > 9999 on writes and read garbage on reads. Configure your client to send/receive each register as a separate base-10000 BCD digit pair, not as a single binary Int32. Full details indocs/Features/BcdRewriting.md.
Hard constraints / prerequisites
- Windows (10 / Server 2019+) or Linux (any systemd distro), 64-bit. Ships as a Windows Service (Application Event Log integration) or a systemd unit (syslog integration); builds single-file for
win-x64andlinux-x64. macOS is not a deployment target — it runs only as a foreground console process. - Modbus TCP backends reachable from the proxy host on port 502 (or the port configured per PLC). The H2-ECOM100 module caps simultaneous connections at 4 per PLC — a fifth upstream client will fail to connect.
- Admin / root rights to install the service (
install.ps1requires elevation;install.shrequires root). - No COM dependency — this is a pure .NET 10 socket-level proxy (unlike the
.NET Framework 4.8 / x86siblings in this repo). - Python 3.10+ on the test machine to run the pymodbus-backed E2E simulator (not needed to run the service in production).
Layout
src/Mbproxy/ Main C# project (net10.0, Microsoft.NET.Sdk.Worker)
tests/Mbproxy.Tests/ xUnit v3 test project (unit + simulator-backed E2E tests)
install/ Install/uninstall + publish scripts (PowerShell + shell), systemd unit, config templates
docs/ Architecture, features, operations, reference, and testing docs
Resource index
| Task | Go to |
|---|---|
| Architecture entry point — listener topology, request flow, per-PLC isolation | docs/Architecture/Overview.md |
| DL205/DL260 Modbus quirks (BCD, CDAB, octal V-memory, FC limits) | docs/Reference/dl205.md |
| pymodbus simulator profile (register seeds for E2E tests) | tests/sim/dl205.json |
| Agent-oriented coding guide (architecture bullets, device quirks, phase context) | CLAUDE.md |
Detailed documentation
The docs/ tree is organized by topic. Start with Architecture/Overview.md for the end-to-end picture; jump to the focused pages below when you need depth on one area.
Architecture
Architecture/Overview.md— Listener topology, request flow, per-PLC isolation.Architecture/ConnectionModel.md— Single backend connection per PLC, TxId multiplexing, request-timeout watchdog, disconnect cascade.Architecture/ReadCoalescing.md— In-flight FC03/FC04 deduplication viaInFlightByKeyMap.Architecture/ResponseCache.md— Opt-in per-tag response cache with bounded operator-configured staleness.Architecture/Keepalive.md— TCPSO_KEEPALIVEon every socket plus an idle-backend FC03 heartbeat.
Features
Features/BcdRewriting.md— BCD codec, CDAB word order, FC03/04/06/16 scope, partial-overlap policy.Features/HotReload.md—IOptionsMonitor-driven config reload with per-change-kind reconcile rules.
Operations
Operations/Configuration.md— Fullappsettings.jsonreference: everyMbproxy:*key, default, and validation rule.Operations/StatusPage.md— Admin endpoint surface: the SignalR-backed web dashboard (/,/plc/{name},/hub/status) and the/status.jsontwin, with every JSON field documented.Operations/Troubleshooting.md— Diagnosis playbook keyed to log events and status counters.
Reference
Reference/LogEvents.md— Stablembproxy.*event catalog (31 events across 8 categories).
Testing
Testing/Simulator.md— pymodbus DL205 fixture, skip policy, and the load-bearing pymodbus 3.13 framer quirk.
Build and run
Build (Debug, multi-file — fast for iteration):
dotnet build Mbproxy.slnx -c Debug
Publish (Release, single-file):
.\install\publish.ps1 -Clean # win-x64 (default)
.\install\publish.ps1 -Rid linux-x64 -Clean # cross-publish for linux-x64
On a Linux build host, use the shell counterpart:
./install/publish.sh --clean # linux-x64 (default)
Each run produces both flavours under publish-out\:
| Flavour | Path (win-x64) | Size | Target prerequisite |
|---|---|---|---|
| Self-contained | publish-out\self-contained\Mbproxy.exe |
~100 MB | None — bundles .NET 10 + ASP.NET Core runtime |
| Framework-dependent | publish-out\framework-dependent\Mbproxy.exe |
~1.6 MB | .NET 10 + ASP.NET Core preinstalled |
On linux-x64 the binary is Mbproxy (no extension) and ships the Linux config template. Pass -OutputDir/-o to publish elsewhere; omit -Clean/--clean to skip the wipe. The scripts wrap dotnet publish src/Mbproxy/Mbproxy.csproj -c Release -r <rid> [-p:SelfContained=false] — run that directly if you only need one flavour.
Run tests:
dotnet test Mbproxy.slnx -c Debug # all tests
dotnet test Mbproxy.slnx -c Debug --filter Category=Unit # unit tests only (no Python required)
dotnet test Mbproxy.slnx -c Debug --filter Category=E2E # E2E tests (require Python + pymodbus)
Run interactively (without installing as a service):
cd src/Mbproxy
dotnet run --configuration Debug
Edit src/Mbproxy/appsettings.json to configure PLCs before running. The admin dashboard will be at http://localhost:8080/ by default — a live SignalR-backed fleet view; click any PLC row for its per-connection detail page and real-time BCD debug view.
Install
The install/ directory holds the publish, install, and uninstall scripts for both platforms.
Windows — elevated PowerShell:
.\install\publish.ps1 -Clean
.\install\install.ps1 -PublishOutput .\publish-out\self-contained -Start
# Config is placed at %ProgramData%\mbproxy\appsettings.json — edit it, then:
# Restart-Service mbproxy
Invoke-WebRequest http://localhost:8080/ -UseBasicParsing
Linux — root / sudo on a systemd host:
./install/publish.sh --clean
sudo ./install/install.sh --publish-dir ./publish-out/self-contained
# Config is placed at /etc/mbproxy/appsettings.json — edit it, then:
# sudo systemctl restart mbproxy
curl http://localhost:8080/
uninstall.ps1 / uninstall.sh reverse the install; both archive log files rather than deleting them. The systemd unit runs mbproxy as Type=exec under a dedicated mbproxy service account.
Maintenance
Documentation doctrine for this repo: ../DOCS-GUIDE.md.
- This README routes to deep docs — it does not duplicate them.
- Design decisions and rationale live in the
docs/tree (Architecture, Features, Operations, Reference, Testing). - When the service's public surface or task→tool mapping changes, update this README and the root
../CLAUDE.mdindex row.