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
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(vialibplctag.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 takeHslCommunication(see §1.3). - Browseability: CIP NJ/NX is only conditionally browsable, and NOT via libplctag's
@tagswalker — 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'sab://gateway[:port]/cip-path, e.g.omron://gateway/path— but for NJ/NX the libplctag attribute string isprotocol=ab-eip&gateway=<ip>&path=<cip-path>&plc=omron-njnx&name=<tag>. (Community-confirmed: NJ/NX needsplc=omron-njnxand a CIP-bridgingpath, 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 existingEncodeValue/ bit-RMW code paths. - Subscribe (
ISubscribable): poll-based via the sharedPollGroupEngine(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@tagswalker does not work on Omron). - Health/Probe (
IHostConnectivityProbe): reuse AbCip's probe-loop pattern (cheap tag read at interval →HostStatetransitions).
Reuse estimate: ~70–80% 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
0x0101Memory 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:
0x0102Memory 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}(+ optionalemBankfor extended memory); the driver maps the pair(memoryArea, bit==null?word:bit)to the correct FINS area code (§1.2).address= word offset;bit= 0–15 for bit access (null ⇒ word access).deviceHostAddressencodes the FINS destinationnet.node.unittriple.wordSwaphandles 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
@tagspseudo-tag (CIP Symbol Object class0x6B) to walk the controller symbol table. libplctag's tag-listing works only on ControlLogix / CompactLogix — it does NOT work on Omron NJ/NX, returningErrorUnsupported/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:
- 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.
- 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.BrowserimplementingIDriverBrowser(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/IBrowseSessionplumbing and struct-fan-out logic; do NOT expect to share the wire-level enumerator. Recommend a commonCip.Browsinghelper 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
IModbusTransportfake) — 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) andahmadfarisfs/fins_simulator_omron. Neither ships a Docker image → wrap in a smallDockerfileundertests/Drivers/.../Omron/Docker/and register with theproject=lmxopcualabel. 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 theplc=omron-njnxlibplctag path, Network-Publish behavior, and Omron string/array quirks. Flag as an infra-gated known limitation until hardware is on the bench.
- Hand-rolled
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 70–80% 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) · Low–Medium 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 inTagConfigEditorMap+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
IFinsTransportcodec (TCP+UDP,0x0101/0x0102, area-code table, word-swap) behind the sameIDrivershell; 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)
- 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. - 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
LiveIntegrationsuite; treat as an infra-gated known limitation until an NJ/NX is on the bench. - (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
- Omron store KB — Using CIP to Read/Write Network Variables with NJ/NX
- Omron NJ/NX CPU Built-in EtherNet/IP Port User's Manual (W506)
- libplctag #466 — Support for Omron NX/NJ variable listing
- libplctag.NET #371 — List all tags in Omron NJ: ErrorUnsupported
- libplctag Omron NJ/NX group thread (plc=omron-njnx, CIP path)
- Omron W227 FINS Commands Reference Manual
- Omron NX-series CPU Unit FINS Function User's Manual (W596)
- Wireshark FINS dissector (framing reference)
- FieldServer Omron FINS protocol driver sheet
- Omron memory-area overview (CIO/HR/AR/DM/WR/EM)
- HslCommunication — commercial-license fork situation
- omron-fins-simulator (open-source FINS sim)
- fins_simulator_omron (open-source FINS sim)