# Mitsubishi MELSEC SLMP / MC-Protocol driver — research

**Status:** research spike (no code). Recommendation is to build a native
**SLMP / MC-protocol** Equipment-kind driver, hand-rolling the 3E-binary frame,
patterned on the existing S7 (byte-oriented binary TCP) and Modbus (polling,
pre-declared-tag, no-browse) drivers.

**Scope note vs. `docs/v2/mitsubishi.md`:** that document catalogues reaching
MELSEC *over Modbus TCP* via bolt-on Ethernet modules (QJ71MT91, RJ71EN71 MODBUS
slave mode, FX5U built-in MODBUS, FX3U-ENET-P502…). This driver is the **native
alternative** that talks the PLC's own protocol and supersedes that lossy path.
The two can coexist — the Modbus path stays valid for sites that only enabled the
MODBUS slave — but SLMP is the strictly-better option wherever the CPU's native
Ethernet/MC endpoint is reachable.

---

## 1. Protocol summary + the gap it closes

### SLMP vs. MC 3E / 4E / 1E

**MC protocol** (MELSEC Communication protocol) is Mitsubishi's long-standing
client/server request-response protocol for reading and writing CPU device
memory over Ethernet (and serial). **SLMP** (SeamLess Message Protocol) is the
modern superset: SLMP's 3E and 4E frames are *bit-for-bit identical* to the
"QnA-compatible 3E/4E" MC-protocol frames, so an SLMP client talks to any
MC-protocol server and vice-versa. For our purposes **SLMP ≡ MC-protocol 3E/4E**;
we implement one framer and cover both product lines.

Frame families:

| Frame | Subheader (req/resp) | Use | Notes |
|---|---|---|---|
| **3E** | `5000` / `D000` | **Primary target.** Q / L / iQ-R / iQ-F / FX5 Ethernet | Stateless; no serial number. Simplest and most universal. |
| **4E** | `5400…0000` / `D400…0000` | iQ-R / iQ-F, when request/response correlation is wanted | 3E **plus** a 2-byte serial number echoed in the response — lets a client pipeline and match replies. Superset of 3E. |
| **1E** | `00`/`01`/`02`/`03` cmd bytes; `80`+cmd resp | Legacy A-series and some FX | Different, older layout. Only needed for very old CPUs. |

Each frame carries a **binary** or **ASCII** encoding. Binary is half the bytes
and what every modern deployment uses; ASCII exists mainly for text-only serial
links. We implement **3E binary** first, structure the framer so **4E binary**
is a thin superset, and treat 1E + ASCII as out-of-scope v1 (documented fallback).

### 3E binary request layout (the frame we hand-roll)

Reading `D200`, 1 word (from Mitsubishi *SLMP Reference Manual* SH-080956ENG and
the FA-Support worked example [3][4]):

```
50 00            Subheader (request 3E)
00               Network No.         (0x00 = own/local network)
FF               PC No.              (0xFF = local/host CPU)
FF 03            Request dest module I/O  (0x03FF = own CPU)
00               Request dest module station
0C 00            Request data length (little-endian; bytes that follow this field)
10 00            Monitoring timer    (0x0010 = 250 ms units; 0 = wait forever)
01 04            Command             (0x0401 Batch Read, little-endian on wire)
00 00            Subcommand          (0x0000 = word units; 0x0001 = bit units)
C8 00 00         Head device number  (200, 3 bytes little-endian)
A8               Device code         (0xA8 = D, data register)
01 00            Device point count  (1 word, little-endian)
```

Response: `D000` subheader, the routing echo, a data length, a **2-byte end code**
(`0000` = success; non-zero = error, e.g. `C051` device-count over range,
`4031` wrong device, `C059`/`C05C` command/subcommand error), then the payload.

Key commands:

| Command | Subcmd | Meaning | Max points |
|---|---|---|---|
| `0401` | `0000` | Batch Read, **word** units | 960 words (3E) |
| `0401` | `0001` | Batch Read, **bit** units | 7168 bits |
| `1401` | `0000` / `0001` | Batch Write, word / bit | 960 / 7168 |
| `0403` | `0000` | **Random Read** (scattered word/dword addresses) | 192 points |
| `1402` | `0000` | Random Write (scattered) | ~160 points |
| `0406` | — | Read block (multiple blocks) | — |
| `1001` / `1002` | — | Remote STOP / RUN | — |
| `0101` | — | Read CPU model name (cheap connectivity probe) | — |

### Device codes (3E binary, 1-byte code)

Bit-vs-word classification is the crux of the addressing model. Non-exhaustive;
**verify the full table against SH-080956ENG before coding** — these are the
common ones:

| Device | Code (hex) | Kind | Number base in engineering tools |
|---|---|---|---|
| X input | `9C` | **bit** | **hex** (Q/L/iQ-R), octal (FX/iQ-F) |
| Y output | `9D` | **bit** | **hex** (Q/L/iQ-R), octal (FX/iQ-F) |
| M internal relay | `90` | bit | decimal |
| L latch relay | `92` | bit | decimal |
| F annunciator | `93` | bit | decimal |
| B link relay | `A0` | **bit** | **hex** |
| SM special relay | `91` | bit | decimal |
| D data register | `A8` | **word** | decimal |
| W link register | `B4` | **word** | **hex** |
| R file register | `AF` | word | decimal |
| ZR extended file reg | `B0` | word | **hex** |
| SD special register | `A9` | word | decimal |
| TN timer current | `C2` | word | decimal |
| CN counter current | `C5` | word | decimal |
| TS/CS timer/counter contact | `C1`/`C4` | bit | decimal |

### The gap SLMP closes over the Modbus-TCP path

`docs/v2/mitsubishi.md` documents, in detail, why the Modbus path is lossy. SLMP
removes each of those failure modes:

1. **No per-site "Modbus Device Assignment" block.** SLMP addresses the CPU's
   device memory directly by `<device code, number>` (e.g. `D200`, `M100`).
   There is *no* 16-entry assignment table to configure in GX Works and no
   "two sites with the same module expose different maps" problem — the biggest
   single source of Modbus-path fragility disappears.
2. **X/Y reachable natively** as their own bit devices, not shoehorned into a
   second non-zero coil bank (Modbus default maps X/Y at offset 8192+). The
   hex-vs-octal number-base trap **remains** (it is a CPU convention, not a
   transport artifact) — see §2/§3 — but there is no *second* Modbus-offset
   translation layered on top.
3. **Full device coverage.** L, F, B, W, R, ZR, SM/SD, timers/counters are all
   directly addressable. The Modbus path can only see whatever the engineer
   chose to expose in the assignment block.
4. **No FC caps / sub-spec quirks.** No "QJ71MT91 doesn't support FC16", no
   125-register FC03 ceiling, no odd-coil-byte truncation. SLMP batch read is
   960 words in one PDU vs. Modbus' 125.
5. **Random read/write** of scattered addresses in one round-trip (`0403`/`1402`)
   — impossible in Modbus without one PDU per contiguous run.
6. **Word-order (CDAB) is still a per-tag concern** (§3) — 32-bit values still
   span two consecutive words low-word-first — but this is now *our* decode
   choice, not something filtered through a module's fixed behavior.

### .NET library options — LICENSE analysis

| Option | License | .NET / maturity | Verdict |
|---|---|---|---|
| **HslCommunication** (`MelsecMcNet`) | **Commercial / NOT free** — "公对公签订合同", company-to-company contract + VAT invoice; source only with paid license [5][6] | net35+; very mature, widely used | **BLOCKER — do not use.** Requires a signed commercial contract. Rules it out for this repo. |
| **McProtocol** (SecondShiftEngineer) | **LGPL-3.0** [7] | netstandard2.0 (loads on net10); MC1E/3E/4E; last updated **2018** | LGPL is usable when consumed as an unmodified dynamic library, but it is a **copyleft** dependency to vet with legal, and it is **7+ years stale**. Useful as a *reference*, weak as a *dependency*. |
| **McpX** | **MIT** [8] | net7/8/9 + netstandard, cross-platform; TCP/UDP, 3E/4E binary+ASCII, batch + random + monitor + remote password; first release 2025, actively developed (v0.7.0 Jun 2026), ~53★, single maintainer | **MIT is clean.** The most license-friendly library and feature-complete, but young + one-maintainer + pre-1.0 → supply-chain/bus-factor risk for a production driver. |
| **`s-pms/melsec_mc_net`** | **MIT** [9] | **C** (not .NET) — Windows/Linux; 3E binary+ASCII, batch + typed read/write; full device-code table | Not consumable from .NET, but an **excellent MIT reference** for a hand-roll (device codes, framing, transaction serialization). |
| **libslmp / libslmp2** (Neucrede) | open-source C/C++ | C/C++ | Reference only, not .NET. |

**Recommendation: hand-roll the 3E-binary framer.** Rationale:
- The frame is small and fully specified (SH-080956ENG); the existing **S7 driver
  already proves this repo hand-rolls byte-oriented binary TCP** with a clean
  `IS7Plc` seam and a fake for tests. SLMP is *simpler* than S7's PDU negotiation.
- The only clean-licence library (McpX, MIT) is young/one-maintainer/pre-1.0 —
  taking it as a hard dependency in a production OT server is more risk than a
  ~600-line framer we own and test.
- HslCommunication (the mature option) is a hard **commercial-licence blocker**.
- Keep McpX and the MIT `melsec_mc_net`/`libslmp` sources as **cross-check
  references** for the framer + device-code table.

The repo already carries a **head-start**: `MelsecAddress` +`MelsecFamily` in
`ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing` encode the hex-vs-octal X/Y family
logic (written for the Modbus path). That logic ports directly into the new
driver's addressing project.

---

## 2. Capability mapping

Same capability-interface set as Modbus/S7 (`IDriver, ITagDiscovery, IReadable,
IWritable, ISubscribable, IHostConnectivityProbe`). This is a **full read/write**
PLC driver.

| Capability | SLMP mapping |
|---|---|
| **Connect** (`IDriver.InitializeAsync`) | TCP client to CPU Ethernet port (default **502** is Modbus; SLMP default is engineer-configured, commonly `1025`/`5007` or a user-set port; **UDP optional** — SLMP supports both; start TCP-only like Modbus). Open socket, optionally issue `0101` Read-CPU-model as a connect assertion. Reconnect/keepalive/idle-disconnect knobs mirror `ModbusDriverOptions`. |
| **Read** (`IReadable`) | **Batch Read `0401`** for contiguous runs (word subcmd `0000` for D/W/R/ZR/TN/CN; bit subcmd `0001` for M/X/Y/B/L). **Random Read `0403`** to coalesce scattered addresses into one PDU. A read planner (like Modbus' block coalescing / `MaxReadGap`) groups tags by device code into batch reads, splitting at the 960-word PDU cap. |
| **Write** (`IWritable`) | **Batch Write `1401`** (word/bit) for runs; **Random Write `1402`** for scattered. Bit-write to M/Y honored; word-write to D/W/R. Write-through gated by the standard `WriteOperate` node authz + `NodeWriteRouter` like every other protocol driver (the `EquipmentTagRefResolver<TDef>` pattern). |
| **Subscribe** (`ISubscribable`) | **Poll-based**, exactly like Modbus/S7 — **SLMP has no native push/unsolicited path** for general device polling. Reuse the shared polling overlay engine (the same `ISubscribable` polling helper Modbus uses). (SLMP *does* have a "device monitor register" `0801`/monitor `0802` mechanism, but it is a stateful convenience, not a change-push; poll is the right model.) |
| **Discover** (`ITagDiscovery`) | **Offline / config-driven.** SLMP exposes **no on-wire symbol table** — the driver returns exactly the pre-declared tags from `SlmpDriverOptions.Tags`, identical to Modbus. No online enumeration. |
| **Probe** (`IHostConnectivityProbe`) | Cheap `0101` Read-CPU-model, or a 1-word `0401` read of a known device, on an interval; raise `OnHostStatusChanged` on transitions. |
| **Alarms / History** | None native. (A future scripted-alarm layer works the same as for any polling driver.) |

### Data-type mapping (SLMP words/bits → OPC UA)

| OPC UA type | SLMP encoding | Notes |
|---|---|---|
| Boolean | 1 bit device (M/X/Y/B/L) via bit-subcmd; or 1 bit of a word device | Bit-in-word needs a `bitIndex` like Modbus `BitInRegister`. |
| Int16 / UInt16 | 1 word device | Native width. |
| Int32 / UInt32 | 2 consecutive words | **word order** matters — MELSEC native is low-word-first (`CDAB` when viewed as a word pair). Per-tag `wordOrder` knob. |
| Float (Single) | 2 words | Same word-order concern. |
| Int64 / UInt64 / Double | 4 words | Same. |
| String | N words, 2 ASCII chars/word | Byte-order-within-word knob (like Modbus `StringByteOrder`); MELSEC packs low byte = first char in some setups. |
| DateTime | vendor-specific packing | v1: skip or map from a documented word layout. |
| Array | `ValueRank=1`, `arrayLength` × element-words, one batch read | Cap at 960-word PDU; auto-chunk. |

**Addressing model = device code + number + base.** A tag names a *device code*
(`D`,`M`,`X`,`Y`,`W`,`B`,`R`,`ZR`,…), a *device number*, and — critically — the
number's **base**: X/Y/B/W/ZR are **hex** on Q/L/iQ-R and X/Y are **octal** on
FX/iQ-F; D/M/L/F/R/timers/counters are **decimal** everywhere. The driver must
preserve the engineering-tool base the operator typed (the `MelsecFamily` enum
already models this). Word-vs-bit is a property of the device code and selects
the batch-read subcommand.

---

## 3. TagConfig JSON shape

Mirrors `ModbusTagDefinition` / `ModbusEquipmentTagParser` (leading-`{` marks an
equipment-tag TagConfig blob; strict enum reads reject typos → `BadNodeIdUnknown`).

Proposed per-tag fields:

| Field | Type | Meaning |
|---|---|---|
| `device` | enum string | Device code: `D`,`M`,`X`,`Y`,`W`,`B`,`R`,`ZR`,`L`,`F`,`SM`,`SD`,`TN`,`CN`,… |
| `number` | string | Device number **as the operator types it in GX Works** (kept as string to preserve hex/octal). |
| `numberBase` | enum | `Decimal` / `Hex` / `Octal` — defaulted from the driver-level `MelsecFamily`, overridable per tag. |
| `dataType` | enum | `Boolean,Int16,UInt16,Int32,UInt32,Int64,UInt64,Float,Double,String`. |
| `bitIndex` | int 0–15 | For a Boolean read from a bit of a word device (omit for true bit devices). |
| `wordOrder` | enum | `ABCD` / `CDAB` — 32/64-bit word order (MELSEC native = `CDAB`, low word first). Default `CDAB`. |
| `stringLength` | int | ASCII chars for `String` (2 per word). |
| `stringByteOrder` | enum | High-byte-first vs low-byte-first within a word. |
| `arrayLength` | int | `isArray && arrayLength>=1` → OPC UA array. |
| `writable` | bool | Defaults true; node authz is the real gate. |

**Example** — a 32-bit float production count at `D200` (hex-family Q CPU, native
CDAB word order), and a bit alarm at `M100`:

```json
{
  "device": "D",
  "number": "200",
  "numberBase": "Decimal",
  "dataType": "Float",
  "wordOrder": "CDAB",
  "writable": false
}
```

```json
{
  "device": "M",
  "number": "100",
  "numberBase": "Decimal",
  "dataType": "Boolean"
}
```

```json
{
  "device": "X",
  "number": "1A",
  "numberBase": "Hex",
  "dataType": "Boolean",
  "_comment": "Q-series X1A = physical input 26 decimal; hex base preserved from GX Works"
}
```

Driver-level `SlmpDriverOptions` mirrors `ModbusDriverOptions`: `Host`, `Port`,
`Frame` (`ThreeE`/`FourE`), `Encoding` (`Binary`), `NetworkNo`/`PcNo`/`DestModuleIo`/
`DestStation` routing bytes (defaults `0x00/0xFF/0x03FF/0x00` = local CPU),
`MonitoringTimer`, `Family` (`Q_L_iQR`/`F_iQF`), `Timeout`, reconnect/keepalive/
idle knobs, `MaxPointsPerRead` (≤960), a read-coalescing gap budget, and the
pre-declared `Tags` list.

---

## 4. BROWSEABILITY VERDICT — **NO**

**Definitively not browseable. No `*.Browser` project is warranted.**

SLMP/MC-protocol exposes a **flat, typed device-memory space** (`D`, `M`, `X`,
`Y`, `W`, `R`, …) addressed purely by `<device code, number>`. There is **no
on-wire symbol table, no tag directory, and no metadata service** in the
protocol. The commands enumerated in the SLMP Reference Manual are memory
read/write, remote CPU control, and self-test — none returns "what tags exist."
This is structurally identical to Modbus and S7, both of which are (correctly)
non-browseable in this repo: the address space is a raw memory map, not a
discoverable namespace. The symbolic tag names live only in the GX Works project
file on the engineer's PC, never on the wire.

**Searched-for exceptions, none qualifying:**
- **CPU model / capability reads** (`0101`, self-test `0619`) return device
  *types and counts*, not a symbol list — useful for a connect assertion, not
  browse.
- **Device monitor register** (`0801`/`0802`) is a client-side convenience for
  re-reading a *previously specified* set — the client supplies the addresses;
  the CPU never volunteers them.
- **Label/tag communication** (iQ-R "device/label access via SLMP" with a name
  string) exists in newer firmware but requires the client to *already know* the
  global-label name and only resolves a name the engineer defined — it is a
  by-name read, still **not an enumeration**. Not a browse source.
- **GX Works project (`.gx3`) / CSV label export** could seed tags *offline*, but
  that is a file-import feature, not an on-wire `IDriverBrowser` session, and is
  out of scope here.

Verdict: **NO browser.** Tags are authored via the pre-declared list + the typed
tag editor (§7), exactly like Modbus/S7. In `TagConfigEditorMap` the driver gets
a typed editor but **no** `IDriverBrowser`/address-picker.

---

## 5. Test-fixture strategy

Follow the repo's established **hand-rolled TCP stub** pattern (S7's `IS7Plc`
fake, FOCAS's mock, Modbus' transport seam). SLMP is a request/response binary
protocol over TCP, so a deterministic stub is straightforward and CI-friendly.

**Recommended layers:**

1. **Unit — framer round-trip tests** (no socket). Encode/decode 3E-binary
   request/response byte arrays against golden vectors taken from SH-080956ENG
   and the MIT `melsec_mc_net`/McpX examples. This is where the device-code table,
   hex/octal number parsing, word-order (CDAB), and end-code handling get pinned.
   Port the existing `MelsecAddress` address tests.

2. **In-process fake `ISlmpClient`** (mirrors `FakeFocasClient` / S7's fake) for
   driver-level read/write/subscribe/discover behavior without a network.

3. **Integration — a hand-rolled SLMP server stub** (a `TcpListener` that decodes
   3E-binary requests and serves a seeded device-memory dictionary), packaged as a
   Docker fixture under `tests/.../Docker/` with the `project=lmxopcua` label and
   an env-gated skip (like `FocasSimFixture`'s `localhost:PORT` probe). This gives
   real-socket read/write round-trips, batch-read chunking, and reconnect tests
   deterministically. **This is the primary integration path** — write the stub;
   don't depend on vendor tooling in CI.

4. **Optional real-target gates (not in CI):**
   - **GX Works3 / GX Works2 simulator (GX Simulator3)** exposes an SLMP-capable
     virtual CPU but is **Windows-only, licensed, GUI-driven** — usable for a
     manual bring-up gate on a Windows box, not for automated CI (same posture as
     the AVEVA/mxaccessgw live gates).
   - **Open-source sims:** community MC/SLMP server sims exist (e.g. Go
     `moge800/gomcprotocol`, Node `plcpeople/mcprotocol` has a server mode,
     `libslmp`/`libmelcli` samples). Any could back a container, but a
     repo-owned .NET stub is lower-maintenance and matches house style.
   - A real FX5U / iQ-R on the bench is the final acceptance gate (a `LiveIntegration`
     env-gated suite, mirroring the historian live gate).

Reuse `docs/v2/mitsubishi.md`'s `Mitsubishi_<model>_<behavior>` test-naming
convention for the behavioral cases (CDAB word order, hex X20 = 32, octal X20 = 16,
960-word batch cap, end-code `C051` on over-range).

---

## 6. Effort / risk / phasing

**Overall effort: moderate** — comparable to the S7 driver. The framer is small;
the complexity budget is almost entirely in the **addressing model** (device
codes × bit/word × hex/octal/decimal base × CDAB word order), which is exactly
where MELSEC drivers go wrong. Front-load it.

**Top risks:**
1. **Addressing correctness** (highest). The hex/octal/decimal base split per
   device family, plus CDAB word order for 32/64-bit values, is the #1 real-world
   bug source (per `docs/v2/mitsubishi.md`). Mitigation: exhaustive framer/address
   unit tests with golden vectors *before* any driver wiring; reuse `MelsecAddress`.
2. **Library/licensing** — resolved by hand-rolling (avoids the HslCommunication
   commercial blocker and the McpX bus-factor risk), but it means we own the
   protocol correctness. Cross-check against the MIT `melsec_mc_net` + McpX + the
   SLMP Reference Manual.
3. **Frame/port/family fragmentation** — 3E vs 4E, binary vs ASCII, TCP vs UDP,
   Q/L/iQ-R hex vs FX/iQ-F octal, engineer-chosen port. Mitigation: ship **3E
   binary / TCP** only in v1, structure the framer so 4E is a superset, document
   the rest as fallbacks (same discipline as S7).

**Phasing:**
- **Phase 0 — Addressing + framer (no I/O).** New `…Driver.Slmp.Addressing`
  project (device codes, base parsing, word order — port `MelsecAddress`) and a
  3E-binary encoder/decoder with golden-vector unit tests. `…Driver.Slmp.Contracts`
  with `SlmpDriverOptions` + `SlmpTagDefinition` + `SlmpEquipmentTagParser`.
- **Phase 1 — Read path.** `SlmpDriver : IDriver, ITagDiscovery, IReadable,
  IHostConnectivityProbe` over an `ISlmpClient` TCP seam (S7-style), with a fake
  + the Docker stub server. Batch Read `0401` + read planner/coalescing + all
  scalar/array/string/word-order decoding.
- **Phase 2 — Write + Subscribe.** Add `IWritable` (Batch Write `1401`, Random
  Write `1402`; write-through via the `EquipmentTagRefResolver<TDef>` +
  `NodeWriteRouter` pattern) and `ISubscribable` on the shared polling overlay.
- **Phase 3 — AdminUI typed tag editor.** `SlmpTagConfigEditor` +
  `SlmpTagConfigModel` (FromJson/ToJson/Validate), registered in
  `TagConfigEditorMap` + `TagConfigValidator`. Driver-edit page + `IDriverProbe`
  (with the `JsonStringEnumConverter` fix from the driver enum-serialization
  memory). **No** `IDriverBrowser` (§4).
- **Phase 4 — Random read/write coalescing + 4E frame + live gate.** `0403`/`1402`
  optimization, optional 4E, and an env-gated `LiveIntegration` suite against a
  real FX5U/iQ-R or GX Simulator3.

---

## References

1. Mitsubishi Electric, *SLMP Reference Manual* (SH-080956ENG) —
   https://dl.mitsubishielectric.com/dl/fa/document/manual/plc/sh080956eng/sh080956engl.pdf
2. Mitsubishi Electric, *MELSEC iQ-F FX5 User's Manual (SLMP)* (JY997D56001) —
   https://dl.mitsubishielectric.com/dl/fa/document/manual/plcf/jy997d56001/jy997d56001k.pdf
3. Inductive Automation, *Understanding Mitsubishi PLCs* (3E frame, D-register
   word order) —
   https://support.inductiveautomation.com/hc/en-us/articles/16517576753165-Understanding-Mitsubishi-PLCs
4. FA Support Me, *PLC and PC communication via SLMP Protocol* (3E-binary worked
   example) —
   https://www.fasupportme.com/portal/en/kb/articles/plc-and-pc-communication-via-slmp-protocol
5. dathlin/HslCommunication (GitHub) — "Not free open source" —
   https://github.com/dathlin/hslcommunication
6. HslCommunication commercial-licence page —
   http://www.hslcommunication.cn/Cooperation
7. McProtocol NuGet (LGPL-3.0, MC1E/3E/4E, last updated 2018) —
   https://www.nuget.org/packages/McProtocol/
8. McpX NuGet / repo (MIT, .NET 7/8/9, 3E/4E binary+ASCII, batch+random) —
   https://libraries.io/nuget/McpX
9. s-pms/melsec_mc_net (GitHub, MIT, C reference impl, full device-code table) —
   https://github.com/s-pms/melsec_mc_net
10. Neucrede/libslmp2 (open-source C/C++ SLMP library, reference) —
    https://github.com/Neucrede/libslmp2
11. In-repo: `docs/v2/mitsubishi.md` (MELSEC-over-Modbus quirks this driver
    supersedes) and `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/MelsecAddress.cs`
    (hex/octal family logic to port).
