# 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 `Tag`s 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](https://store.omron.com.au/knowledge-base/1807412-using-cip-to-read-write-network-variables-with-nj-nx), [NJ/NX CPU Built-in EtherNet/IP Port User's Manual (W506)](https://files.omron.eu/downloads/latest/manual/en/w506_nj_nx-series_cpu_unit_built-in_ethernet_ip_port_users_manual_en.pdf). **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](https://www.myomron.com/downloads/1.Manuals/Networks/W227E12_FINS_Commands_Reference_Manual.pdf), [Wireshark FINS dissector](https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-omron-fins.c), [FieldServer Omron FINS driver sheet](https://cdn.chipkin.com/assets/uploads/2016/jan/PDS_Omron_FINS.pdf). ### 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](https://community.oxmaint.com/discussion-forum/omron-plc-memory-areas-understanding-ci-hr-ar-lr-dm-wr-em-extended-em-usage), [W227 reference](https://www.myomron.com/downloads/1.Manuals/Networks/W227E12_FINS_Commands_Reference_Manual.pdf). ### 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`](https://github.com/HslCommunication-Community/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=&path=&plc=omron-njnx&name=`. (Community-confirmed: NJ/NX needs `plc=omron-njnx` and a CIP-bridging `path`, e.g. `18,`.) Source: [libplctag Omron NJ/NX group thread](https://groups.google.com/g/libplctag/c/bU5MVQrPsOw). - **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: ~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 `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 ```json { "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 ```json { "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` = 0–15 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"](https://github.com/libplctag/libplctag/issues/466), [libplctag.NET #371 "List all tags in Omron NJ: ErrorUnsupported"](https://github.com/libplctag/libplctag.NET/issues/371). - 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)](https://files.omron.eu/downloads/latest/manual/en/w506_nj_nx-series_cpu_unit_built-in_ethernet_ip_port_users_manual_en.pdf), [Omron store KB](https://store.omron.com.au/knowledge-base/1807412-using-cip-to-read-write-network-variables-with-nj-nx). **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`](https://github.com/hiroeorz/omron-fins-simulator) (Ruby, UDP 9600) and [`ahmadfarisfs/fins_simulator_omron`](https://github.com/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 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 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 - [Omron store KB — Using CIP to Read/Write Network Variables with NJ/NX](https://store.omron.com.au/knowledge-base/1807412-using-cip-to-read-write-network-variables-with-nj-nx) - [Omron NJ/NX CPU Built-in EtherNet/IP Port User's Manual (W506)](https://files.omron.eu/downloads/latest/manual/en/w506_nj_nx-series_cpu_unit_built-in_ethernet_ip_port_users_manual_en.pdf) - [libplctag #466 — Support for Omron NX/NJ variable listing](https://github.com/libplctag/libplctag/issues/466) - [libplctag.NET #371 — List all tags in Omron NJ: ErrorUnsupported](https://github.com/libplctag/libplctag.NET/issues/371) - [libplctag Omron NJ/NX group thread (plc=omron-njnx, CIP path)](https://groups.google.com/g/libplctag/c/bU5MVQrPsOw) - [Omron W227 FINS Commands Reference Manual](https://www.myomron.com/downloads/1.Manuals/Networks/W227E12_FINS_Commands_Reference_Manual.pdf) - [Omron NX-series CPU Unit FINS Function User's Manual (W596)](https://files.omron.eu/downloads/latest/manual/en/w596_nx-series_-_cpu_unit_fins_function_users_manual_en.pdf) - [Wireshark FINS dissector (framing reference)](https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-omron-fins.c) - [FieldServer Omron FINS protocol driver sheet](https://cdn.chipkin.com/assets/uploads/2016/jan/PDS_Omron_FINS.pdf) - [Omron memory-area overview (CIO/HR/AR/DM/WR/EM)](https://community.oxmaint.com/discussion-forum/omron-plc-memory-areas-understanding-ci-hr-ar-lr-dm-wr-em-extended-em-usage) - [HslCommunication — commercial-license fork situation](https://github.com/HslCommunication-Community/HslCommunication-Community) - [omron-fins-simulator (open-source FINS sim)](https://github.com/hiroeorz/omron-fins-simulator) - [fins_simulator_omron (open-source FINS sim)](https://github.com/ahmadfarisfs/fins_simulator_omron)