# 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 `` (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` 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 ``. 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__` 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` + `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).