Files
lmxopcua/docs/research/drivers/omron.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

Omron PLC Driver — Research & Implementation Design

Status: Research / design proposal (no code yet). Author context: Standard Equipment-kind driver for the OtOpcUa server, same shape as Modbus / S7 / AbCip / TwinCAT / FOCAS — an in-process IDriver implementing the composable capability interfaces (IReadable / IWritable / ISubscribable / ITagDiscovery / IHostConnectivityProbe, optionally IDriverBrowser in AdminUI). Points are ordinary equipment Tags bound to the driver via TagConfig.FullName, authored on the /uns Tags tab.


0. TL;DR recommendation

  • Support both transports, CIP-first. Ship an Omron CIP (EtherNet/IP) path for modern NJ/NX/NY (Sysmac) controllers as Phase 1, then an Omron FINS path for legacy CJ/CS/CP/CV as Phase 2. They cover disjoint hardware generations; neither subsumes the other.
  • Library: libplctag (via libplctag.NET, MPL-2.0) for the CIP path — reuses almost the entire AbCip wire layer (plc=omron-njnx). For the FINS path, hand-roll the framing (it is a small, well-documented binary protocol) rather than take HslCommunication (see §1.3).
  • Browseability: CIP NJ/NX is only conditionally browsable, and NOT via libplctag's @tags walker — that is the single most important finding. FINS is never browsable (flat memory banks, no symbol table). Details in §4.

1. Transport decision

Omron exposes two entirely different Ethernet protocols depending on controller generation.

1.1 The two transports and what they cover

Transport Port / framing Controller families Addressing model Browsable?
EtherNet/IP CIP TCP/UDP 44818 (EtherNet/IP), CIP explicit messaging NJ / NX / NY (Sysmac), and CJ2/CP-family with a CIP-capable EtherNet/IP unit Named variables (symbolic tags), like Rockwell Logix Conditionally — see §4
FINS FINS/TCP and FINS/UDP on port 9600 CJ / CS / CP / CV, NSJ; NJ/NX also support FINS-over-EtherNet/IP for back-compat Memory areas (CIO/WR/HR/AR/DM/EM banks) + word/bit offset Never (flat memory, no symbol table)

Key architectural fact: NJ/NX are CIP-native. Their "variables" are named tags reached by CIP explicit messaging using a symbolic-segment request path — the same mechanism ControlLogix uses, which is why AbCip's libplctag layer already speaks it (plc=omron-njnx). Sources: Omron store KB — Using CIP to Read/Write Network Variables with NJ/NX, NJ/NX CPU Built-in EtherNet/IP Port User's Manual (W506).

FINS is the classic protocol: a compact binary header (ICF/RSV/GCT, destination & source net/node/unit addresses, SID) followed by a 2-byte command code and its parameters. Memory Area Read = 0x0101, Memory Area Write = 0x0102. FINS/TCP prepends a 16-byte "FINS" TCP header (the ASCII magic 46 49 4E 53 = "FINS", a length, and a command/error field) and requires a small handshake frame to obtain a node address before the first command. Sources: Omron W227 FINS Commands Reference Manual, Wireshark FINS dissector, FieldServer Omron FINS driver sheet.

1.2 FINS memory-area codes (for the TagConfig addressing model)

FINS memory-area codes are access-width-specific (a different code for word vs. bit access of the same bank). Representative word-access codes:

Bank Word code Bit code Notes
CIO (Core I/O) 0xB0 0x30 word + bit addressable
WR (Work) 0xB1 0x31 word + bit addressable
HR (Holding) 0xB2 0x32 word + bit addressable
AR (Auxiliary) 0xB3 0x33 word + bit addressable
DM (Data Memory) 0x82 0x02 primary bulk data bank; word addressable
EM (Extended, bank 0) 0xA0 0x20 banked; code varies per EM bank (0xA0+bank)

(Exact codes vary slightly by CPU family — the authoritative table is per-controller in W227 / the CS/CJ comms manual. The driver must treat the code table as CPU-family-parameterised.) Source: Omron memory-area discussion, W227 reference.

1.3 .NET library options — license analysis

Library Transports License Maturity / .NET 10 Verdict
libplctag.NET (wraps native libplctag) CIP (plc=omron-njnx); also Modbus native core dual-licensed MPL-2.0 / LGPL-2.1; .NET wrapper MPL-2.0 — permissive, commercial-OK, already vetted & shipped in this repo for AbCip Mature, actively maintained; already a proven dependency (AbCip runs on it, net10.0) CIP path: adopt. Zero new license review — same dep AbCip already carries.
HslCommunication FINS and CIP (OmronFinsNet, OmronCipNet) The maintained/commercial builds are NOT free — the original author moved HSL to a paid commercial license; only a frozen old community fork (HslCommunication-Community) remains under the last free (MIT-era) snapshot. The "MIT" claim in casual sources refers to that stale fork, not current releases. Broad but the free fork is unmaintained Do NOT take a dependency. License risk + the free build is abandoned. Use only as a protocol reference to cross-check hand-rolled framing.
Hand-rolled FINS FINS/TCP + FINS/UDP our own code, no third-party license n/a — FINS framing is ~200 lines FINS path: hand-roll. Framing is simple and fully documented (W227 + Wireshark dissector); mirrors how ModbusTcpTransport is hand-rolled in this repo.
Sres.Net.EEIP EtherNet/IP (generic CIP) permissive Focused on generic CIP/assembly, not Omron symbolic variable access; weaker than libplctag for named-tag read/write Not needed — libplctag already covers CIP.
Plc.Omron.Standard (NuGet) FINS permissive but tiny/low-adoption Immature Reference only.

Recommendation: libplctag.NET for CIP (reuse AbCip's proven stack), hand-rolled FINS transport for the memory-area path. This keeps the dependency surface identical to what the repo already ships and dodges the HslCommunication license trap.


2. Capability mapping

The driver implements the same capability set as AbCip/Modbus. Both transports map cleanly onto the composable interfaces in Core.Abstractions.

2.1 CIP path (NJ/NX) — reuses AbCip machinery

  • Connect / InitializeAsync: libplctag Forward-Open per device. Canonical host address form mirroring AbCip's ab://gateway[:port]/cip-path, e.g. omron://gateway/path — but for NJ/NX the libplctag attribute string is protocol=ab-eip&gateway=<ip>&path=<cip-path>&plc=omron-njnx&name=<tag>. (Community-confirmed: NJ/NX needs plc=omron-njnx and a CIP-bridging path, e.g. 18,<ip>.) Source: libplctag Omron NJ/NX group thread.
  • Read (IReadable): direct reuse of AbCip's per-tag runtime pattern — create tag handle, ReadAsync, GetStatus, decode. Batch-read + per-host resilience (IPerCallHostResolver) reuse verbatim.
  • Write (IWritable): full read/write. NJ/NX BOOL-in-word and array handling map onto AbCip's existing EncodeValue / bit-RMW code paths.
  • Subscribe (ISubscribable): poll-based via the shared PollGroupEngine (identical to AbCip/Modbus — Omron CIP has no native subscription/unsolicited push for explicit messaging).
  • Discover (ITagDiscovery): emit pre-declared tags always; controller-side browse is the caveat — see §4 (libplctag's @tags walker does not work on Omron).
  • Health/Probe (IHostConnectivityProbe): reuse AbCip's probe-loop pattern (cheap tag read at interval → HostState transitions).

Reuse estimate: ~7080% of AbCip's driver body is directly reusable for the CIP path. The honest way to capture this is to extract a shared CIP core (tag runtime, poll wiring, status mapping, host-address parsing, resilience seam) that both AbCip and OmronCip consume — rather than copy-paste. The Omron-specific deltas are: the plc=omron-njnx attribute string, Omron's data-type code set (see §2.3), string encoding differences, and the browse story.

2.2 FINS path (CJ/CS/CP) — new transport, Modbus-shaped

  • Connect: open TCP to :9600, send the FINS/TCP node-address-request handshake, cache the assigned client node number. (FINS/UDP variant skips the handshake.)
  • Read: build a 0x0101 Memory Area Read frame (area code + 3-byte address + word count), send, parse the 2-byte end-code + payload. Word-oriented; multi-word reads for 32/64-bit types.
  • Write: 0x0102 Memory Area Write, symmetric. Bit writes use the bit-access area codes; word writes use the word codes. RMW not required for FINS bit writes (bit codes address bits directly).
  • Subscribe: poll-based via PollGroupEngine (FINS has no subscription).
  • Discover: pre-declared tags only — there is no symbol table to enumerate (§4).
  • Probe: cheap DM-word read at interval.

The FINS transport is structurally a sibling of ModbusTcpTransport (IModbusTransport): a thin framed request/response codec behind an IFinsTransport seam, unit-testable against a byte-level fake.

2.3 Data-type mapping

Omron CIP (NJ/NX) uses IEC 61131 variable types that align closely with the existing AbCipDataType / DriverDataType set; FINS is raw words that the TagConfig must type explicitly.

Omron type CIP (NJ/NX) FINS (word interpretation) OPC UA / DriverDataType
BOOL native BOOL bit-area read, or bit N of a word Boolean
BYTE/USINT/SINT 1-byte low byte of word Byte / SByte
WORD/UINT/INT 2-byte 1 word UInt16 / Int16
DWORD/UDINT/DINT 4-byte 2 words (endianness matters) UInt32 / Int32
LWORD/ULINT/LINT 8-byte 4 words UInt64 / Int64
REAL IEEE-754 32 2 words Float
LREAL IEEE-754 64 4 words Double
STRING CIP string word-packed ASCII, fixed length String
DATE_AND_TIME/TIME Sysmac time types banked words DateTime (best-effort)

Word/byte-order caveat (FINS): Omron stores multi-word values with a specific word order that differs from naive concatenation; the FINS codec must expose a per-tag or per-device byte/word-swap option (same class of concern the S7/Modbus drivers already handle). CIP via libplctag handles endianness internally.


3. TagConfig JSON shape

TagConfig.FullName (or a JSON blob under it) carries the driver-specific addressing. Following the AbCip equipment-tag parser convention (AbCipEquipmentTagParser: a leading { marks a TagConfig blob; camelCase property names; strict enum reads via TagConfigJson.TryReadEnumStrict), the two transports need distinct shapes discriminated by a transport field.

3.1 CIP (NJ/NX) — named-variable addressing

{
  "transport": "Cip",
  "deviceHostAddress": "omron://10.100.0.40/1,0",
  "tagName": "Conveyor.Speed",
  "dataType": "Real",
  "isArray": false,
  "arrayLength": 1,
  "writable": true
}

(tagName is the Sysmac global-variable name; near-identical to AbCip's tagPath. The CIP path reuses AbCip's isArray/arrayLength/writable/dataType semantics verbatim.)

3.2 FINS — memory-area addressing

{
  "transport": "Fins",
  "deviceHostAddress": "fins://10.100.0.41:9600/0.1.0",
  "memoryArea": "DM",
  "address": 100,
  "bit": null,
  "dataType": "Int16",
  "wordSwap": false,
  "isArray": false,
  "arrayLength": 1,
  "writable": true
}
  • memoryArea{CIO, WR, HR, AR, DM, EM} (+ optional emBank for extended memory); the driver maps the pair (memoryArea, bit==null?word:bit) to the correct FINS area code (§1.2).
  • address = word offset; bit = 015 for bit access (null ⇒ word access).
  • deviceHostAddress encodes the FINS destination net.node.unit triple.
  • wordSwap handles Omron multi-word ordering for 32/64-bit types.

4. BROWSEABILITY VERDICT (transport-dependent) — the critical finding

4.1 FINS — NOT browsable. Full stop.

FINS addresses flat memory banks with no symbol/metadata table on the wire. There is nothing to enumerate. The AdminUI must offer only manual entry (memory-area dropdown + word/bit offset). No IDriverBrowser for the FINS transport.

4.2 CIP (NJ/NX) — browsable in principle, but NOT via libplctag's @tags walker.

This is the finding to flag loudly and to coordinate with the AbCip-browseability audit:

  • The AbCip browse mechanism relies on libplctag's @tags pseudo-tag (CIP Symbol Object class 0x6B) to walk the controller symbol table. libplctag's tag-listing works only on ControlLogix / CompactLogix — it does NOT work on Omron NJ/NX, returning ErrorUnsupported / PLCTAG_ERR_NOT_FOUND. Confirmed upstream: libplctag #466 "Support for Omron NX/NJ variable listing", libplctag.NET #371 "List all tags in Omron NJ: ErrorUnsupported".
  • So the naive assumption "CIP controller-tag enumeration IS browsable, reuse the AbCip walker" is only half true for Omron: the protocol capability exists (published variables are reachable), but Omron NJ/NX exposes the tag list through a different CIP object/service than Rockwell and libplctag hasn't implemented the Omron variant.
  • Additional gate: on NJ/NX a variable is only reachable over the network at all if the programmer set its Network Publish attribute (Publish Only) in Sysmac Studio. Tags without Network Publish are invisible to any external client. And when no user variables are published, an enumeration returns only the reserved system variables (names starting with _). Source: Omron NJ/NX EtherNet/IP User's Manual (W506), Omron store KB.

Verdict: CIP-Omron browse is achievable but requires new protocol work, not free reuse. Two viable routes, in preference order:

  1. Import the Sysmac Studio tag export (offline). NJ/NX projects export the published global variable table (CSV/tag file). An AdminUI importer that parses this into candidate TagConfigs is the lowest-risk, highest-value browse experience and needs no online CIP enumeration at all.
  2. Implement the Omron CIP tag-list service directly (online IBrowseSession). Omron NJ/NX supports reading the published-variable list via a CIP service Rockwell-differently; this means hand-writing the request/response against the NJ/NX EtherNet/IP object model (not going through libplctag's @tags). This is a research-grade effort — feasible (third-party tools like Kepware discover Omron published tags) but non-trivial.

4.3 Browse-seam design (CIP path only)

If/when online CIP browse is implemented, it fits the existing seam exactly (mirroring OpcUaClient.Browser / Galaxy.Browser, which live as separate *.Browser projects registered in AdminUI DI and indexed by DriverType):

  • New project ZB.MOM.WW.OtOpcUa.Driver.OmronCip.Browser implementing IDriverBrowser (DriverType = "OmronCip"), OpenAsync(configJson)IBrowseSession.
  • IBrowseSession: RootAsync = one folder per device; ExpandAsync = published global variables (and struct-member fan-out for UDT-typed variables, reusing AbCip's template-decode pattern); AttributesAsync = data type / access / array shape side-panel.
  • Share a CIP-browse core with AbCip only if the enumeration transport is unified. Since Omron's tag-list service differs from Rockwell's, the honest boundary is: share the BrowseNode/IBrowseSession plumbing and struct-fan-out logic; do NOT expect to share the wire-level enumerator. Recommend a common Cip.Browsing helper library the AbCip browser (if built) and OmronCip browser both consume for tree-shaping, with transport-specific enumerators underneath. Coordinate this boundary with the AbCip-browseability audit so the two efforts agree on the shared-core seam.

Note: the driver is still fully usable without a browser — like every driver, unmapped types fall back to the raw-JSON TagConfig editor, and pre-declared tags always work. Browse is a convenience layer, not a correctness dependency.


5. Test-fixture strategy

Consistent with the repo's Docker-fixture model (driver sims on the Linux host 10.100.0.35, controlled via lmxopcua-fix, compose files under tests/.../Docker/).

5.1 FINS

  • Hand-rolled byte-level fake for unit tests (mirror the Modbus IModbusTransport fake) — the primary and most valuable test surface; deterministic, no container.
  • Open-source FINS simulators for an integration fixture: hiroeorz/omron-fins-simulator (Ruby, UDP 9600) and ahmadfarisfs/fins_simulator_omron. Neither ships a Docker image → wrap in a small Dockerfile under tests/Drivers/.../Omron/Docker/ and register with the project=lmxopcua label. Both are UDP-oriented; FINS/TCP handshake coverage stays on the hand-rolled fake.
  • Omron's own CX-Simulator speaks FINS but is Windows-only, license-gated (bundled in CX-One, not separately purchasable) → not a CI fixture; reserve for a manual/live gate against real hardware if available.

5.2 CIP (NJ/NX)

  • No credible free NJ/NX CIP simulator exists. libplctag's own test harness targets Rockwell. Options, in order:
    • Hand-rolled IAbCipTagRuntime-style fake (Omron variant) for read/write/decode unit tests — the same seam AbCip uses (IAbCipTagRuntime / factory injection) makes the driver fully unit-testable without hardware. This is the primary CI surface.
    • Live gate against real NJ/NX hardware (env-gated Category=LiveIntegration, skips cleanly when the env var is absent — the pattern the HistorianGateway live suite already uses). This is the only way to truly validate the plc=omron-njnx libplctag path, Network-Publish behavior, and Omron string/array quirks. Flag as an infra-gated known limitation until hardware is on the bench.

6. Effort / risk / phasing

Recommended order: CIP-first, then FINS.

Rationale: (a) CIP targets the modern/current Omron line (NJ/NX/NY) most likely in new installs; (b) the CIP path reuses 7080% of the already-shipped, already-hardened AbCip stack (libplctag, poll engine, resilience, status mapping), so it is the fastest path to a working read/write driver; (c) FINS, while simpler as a protocol, is a net-new transport codec with no reuse and targets legacy hardware.

Phase 1 — Omron CIP read/write (NJ/NX) · LowMedium effort

  • Extract a shared CIP core from AbCip (or, pragmatically, copy AbCip and specialise) → Driver.OmronCip + Driver.OmronCip.Contracts, DriverType="OmronCip", plc=omron-njnx.
  • Capabilities: Connect/Read/Write/Subscribe(poll)/Probe; pre-declared tags via ITagDiscovery.
  • Typed tag editor: copy AbCipTagConfigModel/editor, register in TagConfigEditorMap + TagConfigValidator.
  • Tests: hand-rolled runtime fake + env-gated live gate.
  • Risk: libplctag Omron string/array edge cases (community-reported); word/UDT quirks. Medium.

Phase 2 — Omron FINS read/write (CJ/CS/CP) · Medium effort

  • New IFinsTransport codec (TCP+UDP, 0x0101/0x0102, area-code table, word-swap) behind the same IDriver shell; memory-area TagConfig shape (§3.2); manual-entry editor (no browse).
  • Tests: byte-level fake (primary) + open-source simulator container (secondary).
  • Risk: per-CPU-family area-code/word-order variance. Medium. No dependency/license risk (hand-rolled).

Phase 3 — CIP online browse (NJ/NX) · High effort / research-grade

  • Sysmac Studio tag-export importer first (cheap, high value), then optional online Omron CIP tag-list IBrowseSession (Driver.OmronCip.Browser) — not libplctag @tags.
  • Coordinate the shared CIP-browse-core boundary with the AbCip-browseability audit.
  • Risk: High — Omron's tag-list service is undocumented-in-libplctag and Rockwell-different.

Top risks (summary)

  1. CIP browse is not free reuse. libplctag's tag-list walker does not work on Omron NJ/NX (ErrorUnsupported); online browse needs net-new Omron-specific CIP work, and even then only Network-Published variables are visible. Mitigate by shipping the offline Sysmac-export importer first and treating online browse as a later research phase.
  2. No free NJ/NX CIP simulator → CIP correctness for the real wire (string/array/UDT/Network- Publish semantics) can only be proven on live hardware behind an env-gated LiveIntegration suite; treat as an infra-gated known limitation until an NJ/NX is on the bench.
  3. (Secondary) FINS per-family variance in memory-area codes and multi-word ordering — contain with a CPU-family-parameterised code table + per-tag wordSwap.

Sources