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
23 KiB
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:
- 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. - 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.
- 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.
- 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.
- Random read/write of scattered addresses in one round-trip (
0403/1402) — impossible in Modbus without one PDU per contiguous run. - 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
IS7Plcseam 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/libslmpsources 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:
{
"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-test0619) 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-wireIDriverBrowsersession, 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:
-
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 existingMelsecAddressaddress tests. -
In-process fake
ISlmpClient(mirrorsFakeFocasClient/ S7's fake) for driver-level read/write/subscribe/discover behavior without a network. -
Integration — a hand-rolled SLMP server stub (a
TcpListenerthat decodes 3E-binary requests and serves a seeded device-memory dictionary), packaged as a Docker fixture undertests/.../Docker/with theproject=lmxopcualabel and an env-gated skip (likeFocasSimFixture'slocalhost:PORTprobe). 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. -
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, Nodeplcpeople/mcprotocolhas a server mode,libslmp/libmelclisamples). 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
LiveIntegrationenv-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:
- 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; reuseMelsecAddress. - 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. - 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.Addressingproject (device codes, base parsing, word order — portMelsecAddress) and a 3E-binary encoder/decoder with golden-vector unit tests.…Driver.Slmp.ContractswithSlmpDriverOptions+SlmpTagDefinition+SlmpEquipmentTagParser. - Phase 1 — Read path.
SlmpDriver : IDriver, ITagDiscovery, IReadable, IHostConnectivityProbeover anISlmpClientTCP seam (S7-style), with a fake- the Docker stub server. Batch Read
0401+ read planner/coalescing + all scalar/array/string/word-order decoding.
- the Docker stub server. Batch Read
- Phase 2 — Write + Subscribe. Add
IWritable(Batch Write1401, Random Write1402; write-through via theEquipmentTagRefResolver<TDef>+NodeWriteRouterpattern) andISubscribableon the shared polling overlay. - Phase 3 — AdminUI typed tag editor.
SlmpTagConfigEditor+SlmpTagConfigModel(FromJson/ToJson/Validate), registered inTagConfigEditorMap+TagConfigValidator. Driver-edit page +IDriverProbe(with theJsonStringEnumConverterfix from the driver enum-serialization memory). NoIDriverBrowser(§4). - Phase 4 — Random read/write coalescing + 4E frame + live gate.
0403/1402optimization, optional 4E, and an env-gatedLiveIntegrationsuite against a real FX5U/iQ-R or GX Simulator3.
References
- Mitsubishi Electric, SLMP Reference Manual (SH-080956ENG) — https://dl.mitsubishielectric.com/dl/fa/document/manual/plc/sh080956eng/sh080956engl.pdf
- Mitsubishi Electric, MELSEC iQ-F FX5 User's Manual (SLMP) (JY997D56001) — https://dl.mitsubishielectric.com/dl/fa/document/manual/plcf/jy997d56001/jy997d56001k.pdf
- Inductive Automation, Understanding Mitsubishi PLCs (3E frame, D-register word order) — https://support.inductiveautomation.com/hc/en-us/articles/16517576753165-Understanding-Mitsubishi-PLCs
- 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
- dathlin/HslCommunication (GitHub) — "Not free open source" — https://github.com/dathlin/hslcommunication
- HslCommunication commercial-licence page — http://www.hslcommunication.cn/Cooperation
- McProtocol NuGet (LGPL-3.0, MC1E/3E/4E, last updated 2018) — https://www.nuget.org/packages/McProtocol/
- McpX NuGet / repo (MIT, .NET 7/8/9, 3E/4E binary+ASCII, batch+random) — https://libraries.io/nuget/McpX
- s-pms/melsec_mc_net (GitHub, MIT, C reference impl, full device-code table) — https://github.com/s-pms/melsec_mc_net
- Neucrede/libslmp2 (open-source C/C++ SLMP library, reference) — https://github.com/Neucrede/libslmp2
- In-repo:
docs/v2/mitsubishi.md(MELSEC-over-Modbus quirks this driver supersedes) andsrc/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/MelsecAddress.cs(hex/octal family logic to port).