Files
lmxopcua/docs/research/drivers/melsec-slmp.md
T
Joseph Doherty 8fc147d8d4
v2-ci / build (push) Successful in 3m14s
v2-ci / unit-tests (push) Failing after 9m13s
docs: driver-expansion program — 8 research reports + 7 design docs, parallel-reviewed
Adds the driver-expansion program design (umbrella: universal Discover-backed
browser + MTConnect, MQTT/Sparkplug B, BACnet/IP, SQL poll, Omron, Modbus RTU;
MELSEC deferred) plus the per-driver research reports.

All docs went through a 7-agent parallel review against the codebase before
this commit. Highlights fixed in review:

- universal browser: FOCAS FixedTree fills post-connect -> UntilStable settle
  + FixedTree.Enabled patch; MQTT reconciled to bespoke (was contradicting the
  program doc's SupportsOnlineDiscovery=false verdict)
- modbus-rtu: SerialPort.ReadTimeout doesn't bound async BaseStream reads ->
  linked-CTS per-op deadline (R2-01 class); BCL enum reuse would leak
  System.IO.Ports into Contracts
- bacnet: DiscoveryRediscoverPolicy enum name; UDP 47808 contention; live
  suite rewritten around unicast Who-Is + BBMD (broadcast doesn't cross VMs)
- sql-poll: real tier registration via DriverFactoryRegistry.Register;
  blackhole gate must not docker-pause the shared central SQL Server
- mqtt: Sparkplug v3.0 STATE topic form; first-in-repo proto codegen noted
- omron: host hardcodes isIdempotent:false today (retry seam unshipped);
  v1 scopes UDTs to dotted-leaf access
- mtconnect: SecurityClassification.ViewOnly; factory ParseEnum<T> pattern
- program doc: both valid enum-serialization patterns; IRediscoverable is
  change-signal-gated; RTU P2 adds System.IO.Ports; label is host-side
2026-07-15 16:40:36 -04:00

23 KiB
Raw Blame History

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

{
  "device": "D",
  "number": "200",
  "numberBase": "Decimal",
  "dataType": "Float",
  "wordOrder": "CDAB",
  "writable": false
}
{
  "device": "M",
  "number": "100",
  "numberBase": "Decimal",
  "dataType": "Boolean"
}
{
  "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).