docs: driver-expansion program — 8 research reports + 7 design docs, parallel-reviewed
v2-ci / build (push) Successful in 3m14s
v2-ci / unit-tests (push) Failing after 9m13s

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
This commit is contained in:
Joseph Doherty
2026-07-15 16:40:36 -04:00
parent 37cdbef7a5
commit 8fc147d8d4
17 changed files with 6408 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
# 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=<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](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: ~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
```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` = 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"](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 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
- [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)
</content>
</invoke>