# 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`](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 `A` decodes as `decimal = high * 10_000 + low` where `low` is the register at `A` and `high` is the register at `A+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 in [`docs/Features/BcdRewriting.md`](docs/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-x64` and `linux-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.ps1` requires elevation; `install.sh` requires root).
- **No COM dependency** — this is a pure .NET 10 socket-level proxy (unlike the `.NET Framework 4.8 / x86` siblings 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`](docs/Architecture/Overview.md) |
| DL205/DL260 Modbus quirks (BCD, CDAB, octal V-memory, FC limits) | [`docs/Reference/dl205.md`](docs/Reference/dl205.md) |
| pymodbus simulator profile (register seeds for E2E tests) | [`tests/sim/dl205.json`](tests/sim/dl205.json) |
| Agent-oriented coding guide (architecture bullets, device quirks, phase context) | [`CLAUDE.md`](CLAUDE.md) |

## Detailed documentation

The `docs/` tree is organized by topic. Start with [`Architecture/Overview.md`](docs/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`](docs/Architecture/Overview.md) — Listener topology, request flow, per-PLC isolation.
- [`Architecture/ConnectionModel.md`](docs/Architecture/ConnectionModel.md) — Single backend connection per PLC, TxId multiplexing, request-timeout watchdog, disconnect cascade.
- [`Architecture/ReadCoalescing.md`](docs/Architecture/ReadCoalescing.md) — In-flight FC03/FC04 deduplication via `InFlightByKeyMap`.
- [`Architecture/ResponseCache.md`](docs/Architecture/ResponseCache.md) — Opt-in per-tag response cache with bounded operator-configured staleness.
- [`Architecture/Keepalive.md`](docs/Architecture/Keepalive.md) — TCP `SO_KEEPALIVE` on every socket plus an idle-backend FC03 heartbeat.

### Features

- [`Features/BcdRewriting.md`](docs/Features/BcdRewriting.md) — BCD codec, CDAB word order, FC03/04/06/16 scope, partial-overlap policy.
- [`Features/HotReload.md`](docs/Features/HotReload.md) — `IOptionsMonitor`-driven config reload with per-change-kind reconcile rules.

### Operations

- [`Operations/Configuration.md`](docs/Operations/Configuration.md) — Full `appsettings.json` reference: every `Mbproxy:*` key, default, and validation rule.
- [`Operations/StatusPage.md`](docs/Operations/StatusPage.md) — Admin endpoint surface: the SignalR-backed web dashboard (`/`, `/plc/{name}`, `/hub/status`) and the `/status.json` twin, with every JSON field documented.
- [`Operations/Troubleshooting.md`](docs/Operations/Troubleshooting.md) — Diagnosis playbook keyed to log events and status counters.

### Reference

- [`Reference/LogEvents.md`](docs/Reference/LogEvents.md) — Stable `mbproxy.*` event catalog (31 events across 8 categories).

### Testing

- [`Testing/Simulator.md`](docs/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):**

```powershell
dotnet build Mbproxy.slnx -c Debug
```

**Publish (Release, single-file):**

```powershell
.\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:

```bash
./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:**

```powershell
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):**

```powershell
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:

```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:

```bash
./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`](../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.md`](../CLAUDE.md) index row.
