1. Overview: Why Implement FINS in .NET
Industrial gateways that bridge Omron CS/CJ-series PLCs to SCADA, MES, or cloud telemetry stacks often rely on third-party OPC servers or vendor-locked ActiveX controls. Both paths age poorly: classic Omron CX-Server COM components only work in 32-bit hosts, and many FINS toolkits on the market are abandoned. Building a native .NET FINS gateway against the CJ1M-CPU13 gives you a modern async, cross-platform, 64-bit-clean implementation that you control end-to-end.
The CJ1M-CPU13 is a compact, modular CPU that ships with one RS-232C port (peripheral or Host Link) and supports an Ethernet Communication Unit (ETN21) on the backplane. Both transports speak the same application layer — FINS (Factory Interface Network Service) — so a single .NET state machine can drive serial and TCP/UDP sessions with only the transport layer changing.
This reference documents the FINS frame format, the memory-area model, the command codes you need for CIO/HR/DM access, and a production-ready C# transport implementation with code samples, error handling, and verification steps.
2. FINS Protocol Architecture
FINS is a request/response application-layer protocol. The CPU (or a Communication Unit) accepts a command frame, executes it on the specified memory area, and returns a response frame. Every frame has a fixed header followed by command-specific data.
2.1 Layer Model
| Layer | Function | Notes |
|---|---|---|
| Application | FINS command/response | Defined in W421, identical across transports |
| Session | FINS header (ICF, SID, DA/SA) | Routes frames through FINS network / unit / node addresses |
| Transport (Ethernet) | UDP or TCP | Default UDP/TCP port 9600 |
| Transport (Serial) | Host Link (C-mode) or FINS-on-serial | Peripheral / RS-232C port, default 9600/19200 bps |
| Physical | 10/100Base-TX or RS-232C | ETN21, CP1W-CIF01, or onboard port |
2.2 Frame Topology
Every FINS frame is structured: [FINS Header (10 or 16 bytes)] [Command Code (2 bytes)] [Command Body] [Response Body]. For Ethernet, the header expands to 16 bytes to include the Gateway count and destination/source network addresses; serial frames use 10 bytes because the Host Link layer adds its own addressing wrapper.
3. FINS Header Layout (Ethernet / TCP-UDP)
| Offset | Field | Size (B) | Description |
|---|---|---|---|
| 0 | ICF | 1 | Information Control Field. 0x80 = response required, 0x81 = response not required. Set bit 6 (0x40) for command, bit 7 (0x80) for response. Typical request: 0x80. |
| 1 | RSC | 1 | Reserved, 0x00
|
| 2 | GCT | 1 | Gateway count. Number of networks to traverse. 0x02 when going through an ETN21 to a CJ1M on the same Ethernet segment. |
| 3 | DNA | 1 | Destination network address (0–127). The ETN21's network number. |
| 4 | DA1 | 1 | Destination node number (0–254). FINS node derived from the last octet of the unit's IP. |
| 5 | DA2 | 1 | Destination unit address. 0x00=CPU, 0xFE=ETN21 unit. |
| 6 | SNA | 1 | Source network address (your gateway's network) |
| 7 | SA1 | 1 | Source node (your PC's FINS node) |
| 8 | SA2 | 1 | Source unit (0x00 for PC application) |
| 9 | SID | 1 | Service ID. Use a rolling counter 0x00–0xFF to correlate requests/responses. |
| 10 | DNA2 / SNA2 | 2 | (Ethernet only) Relay network / node when GCT > 2 |
| … | DA1n / SA1n | varies | Additional hop pairs, length 3×(GCT–1) bytes |
The CPU responds with the same SID, swapped SA/DA, and bits 0–3 of ICF holding the completion code (MRES/SRES codes). The main response code is at offset 12 in the response payload, sub-code at 13.
4. Memory Areas and Address Encoding
Each FINS memory command encodes the target area as a 2-byte area code followed by a 3-byte address. Word addresses are little-endian; bit addresses use the lower 4 bits of byte 2 of the address (0x0–0xF).
| Area | Area Code (hex) | Range (CJ1M) | Notes |
|---|---|---|---|
| CIO | 0x30 |
0–6143 | Includes built-in I/O, CP-series link words, and CPU Bus Unit areas. CJ1M also exposes 0x00 as an alias for CIO in some firmware revisions. |
| WR (Work) | 0x31 |
0–511 | Internal work relays/words. |
| HR (Holding) | 0x32 |
0–511 | Retained on power loss. |
| AR (Auxiliary) | 0x33 |
0–959 | Read-only for most user code. |
| DM (Data Memory) |
0x02 (D20000+) / 0x82 (legacy D00000–D19999) |
D0–D32767 | Use 0x82 for the banked legacy region, 0x02 for the modern D20000+ region. CJ1M CPU13 supports EM banks 0–3 (0x18–0x20 for E0_0–E3_32767). |
| EM (Extended) |
0x18–0x20 (banks 0–3), 0x30–0x38 (banks 4–7 if enabled) |
32K words per bank | Requires EM allocation in PLC Setup. |
| TK (Task Flag) | 0x06 |
bit only | Rarely used by SCADA gateways. |
| Timer PV | 0x09 |
0–4095 | Read only PV values. |
| Counter PV | 0x05 |
0–4095 | Read only PV values. |
D00100, encode as bytes 0x64 0x00 0x00. To read CIO bit CIO 0.07, encode the word (0) and put 0x07 in the low nibble of the third address byte.5. Core FINS Commands
W421 enumerates ~80 commands. The following subset covers 95% of SCADA/IIoT gateway use cases against a CJ1M-CPU13.
| Command | Code (hex) | Function |
|---|---|---|
| Memory Area Read | 01 01 |
Read up to 999 consecutive words from one area |
| Memory Area Write | 01 02 |
Write up to 999 words to one area |
| Memory Area Fill | 01 04 |
Fill a region with a constant (useful for DMs on commissioning) |
| Multiple Memory Area Read | 01 05 |
Read up to 198 words from up to 4 disjoint areas in one round-trip (efficiency win for tag-list polling) |
| Memory Area Transfer | 01 06 |
Copy a block within or between areas |
| Run/Stop/Read CPU |
04 01 / 04 02
|
Mode change and status read |
| Controller Data Read | 05 01 |
Controller model, version, area ranges |
| Controller Status Read | 06 01 |
Operating mode, fatal/non-fatal flags |
| Time Read/Write |
07 01 / 07 02
|
Read or set PLC clock (BCD encoded) |
| Message Read/Write/Log |
21 01–21 04
|
FINS message instructions (MSG-SND/MSG-RCV) used by the PLC program |
| File Memory Read/Write |
22 01 / 22 02
|
EM file and Memory Card operations |
| Clock/Calendar 2nd Read | 07 03 |
Sub-second precision (CJ2 and later; CJ1M may not support) |
5.1 Command Body: Memory Area Read (01 01) Example
To read 10 words starting at DM D00100:
- Area code:
0x82(DM legacy) or0x02(DM 20000+). Pick one based on PLC Setup → DM Area Setup. - Address:
0x64 0x00 0x00(=100 decimal, little-endian). - Number of items:
0x0A 0x00(little-endian word count).
Command payload (after the 2-byte command code): 82 64 00 00 0A 00. Full request frame on Ethernet: 10-byte FINS header + 01 01 + payload. Response payload carries the 10 words little-endian (20 bytes), and the trailing MRES/SRES end codes (0x0000 = success).
5.2 Command Body: Multiple Memory Area Read (01 05) Example
Read CIO 0, HR 0, and DM 0, each 5 words:
Body: 03 00 // number of area entries
30 00 00 00 05 00 // CIO 0, 5 words
32 00 00 00 05 00 // HR 0, 5 words
02 64 00 00 05 00 // DM 100, 5 words
Response packs the three blocks in order. Maximum 198 words across all entries.
6. Transport: Ethernet (TCP / UDP)
The ETN21 supports both TCP and UDP FINS on port 9600. UDP gives the lowest latency for periodic polling; TCP gives a session model that survives PLC reboots and is recommended for write operations.
6.1 ETN21 Configuration (CX-Programmer)
- Set the unit's IP address, subnet, and gateway in the ETN21's I/O Table → Unit Setup dialog.
- Set FINS/UDP Port =
9600, FINS/TCP Port =9600. - Set FINS Node Number = last octet of the IP (e.g., 100 for
10.0.0.100). - Set Network Number (1–127) for routing across multiple subnets.
- Set Routing Table entries if crossing ETN21 → CPU.
- Transfer settings, then power-cycle the rack.
Reference: CS/CJ-series Ethernet Units Operation Manual W342 and the ProFace CS/CJ Ethernet Driver notes (PDF) for cross-vendor confirmation of port and FINS node behavior.
6.2 Connection Parameters to Cache in Your .NET App
| Parameter | Typical Value | Source |
|---|---|---|
| Remote IP | 10.0.0.100 |
ETN21 setup |
| Local FINS Node |
10.0.0.5 (your PC) |
Operating system |
| PLC FINS Network | 1 |
ETN21 routing table |
| PLC FINS Node | 100 |
ETN21 setup (last IP octet) |
| PLC Unit Number |
0 (CPU) or 0xFE (ETN21) |
Rack/slot position |
| Transport | TCP/UDP 9600
|
ETN21 setup |
| Connection Count | TCP: 16 simultaneous, UDP: unlimited (unreliable) | W342 §4-3 |
6.3 .NET UDP Transport Skeleton
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
public sealed class FinsUdpClient : IDisposable
{
private readonly UdpClient _socket;
private readonly IPEndPoint _remote;
private readonly byte _sid;
private int _sidCounter;
public byte LocalNode { get; }
public byte LocalNetwork { get; }
public byte RemoteNode { get; }
public byte RemoteNetwork { get; }
public byte RemoteUnit { get; }
public FinsUdpClient(string remoteIp, int port,
byte localNet, byte localNode,
byte remoteNet, byte remoteNode, byte remoteUnit = 0)
{
_socket = new UdpClient(0); // bind ephemeral
_socket.Client.ReceiveTimeout = 2000;
_remote = new IPEndPoint(IPAddress.Parse(remoteIp), port);
LocalNetwork = localNet; LocalNode = localNode;
RemoteNetwork = remoteNet; RemoteNode = remoteNode;
RemoteUnit = remoteUnit;
}
public async Task<byte[]> SendAsync(byte[] commandBody, CancellationToken ct = default)
{
byte sid = (byte)Interlocked.Increment(ref _sidCounter);
var frame = BuildFrame(sid, commandBody);
await _socket.SendAsync(frame, frame.Length, _remote).ConfigureAwait(false);
var result = await _socket.ReceiveAsync().WaitAsync(ct).ConfigureAwait(false);
ValidateResponse(frame, result.Buffer, sid);
return result.Buffer;
}
private byte[] BuildFrame(byte sid, byte[] body)
{
// 10-byte FINS header + 2-byte command code + body
var frame = new byte[10 + body.Length];
frame[0] = 0x80; // ICF: command, response required
frame[1] = 0x00; // RSC
frame[2] = 0x02; // GCT: 2 hops (PC -> ETN21 -> CPU)
frame[3] = RemoteNetwork;
frame[4] = RemoteNode;
frame[5] = RemoteUnit;
frame[6] = LocalNetwork;
frame[7] = LocalNode;
frame[8] = 0x00; // SA2 (PC unit)
frame[9] = sid;
Buffer.BlockCopy(body, 0, frame, 10, body.Length);
return frame;
}
private static void ValidateResponse(byte[] request, byte[] response, byte sid)
{
if (response.Length < 14)
throw new FinsException("Truncated FINS response");
if (response[9] != sid)
throw new FinsException($"SID mismatch: sent 0x{sid:X2} got 0x{response[9]:X2}");
int main = response[12];
int sub = response[13];
if (main != 0 || sub != 0)
throw new FinsException($"FINS error 0x{main:X2}{sub:X2}");
}
public void Dispose() => _socket?.Close();
}
public class FinsException : Exception { public FinsException(string m): base(m) {} }
6.4 .NET TCP Transport Notes
TCP FINS prepends a 4-byte length header to the FINS frame (big-endian, length in bytes of the FINS payload). The PLC closes the socket if the unit is reset, so wrap your read loop in a reconnect task. Use a single NetworkStream with a SemaphoreSlim(1,1) to serialize requests; FINS is half-duplex on a given socket.
7. Transport: Serial (Host Link / FINS-on-Serial)
The CJ1M's peripheral port and the optional CP1W-CIF01 RS-232C/485 Option Board can run Host Link (C-mode) or a serial FINS variant. FINS over serial is rare in practice; most legacy installs use Host Link because it is the path supported by older HMI panels.
7.1 Host Link (C-mode) Frame Format
@ [FA header (optional 2B)] [node (2B ASCII)] [header (2B)] [text] [FCS (2B)] [* CR]
Default baud is 9600, 7E2, node 00. The '@' sentinel indicates the start; '*' and CR mark the end. The Frame Check Sequence is the two-character ASCII hex of the XOR of every byte between '@' and the FCS. The Host Link Command Reference W227 details each text code (RR, WR, RD, WD, etc.).
7.2 Choosing the Transport
| Criterion | Use Ethernet (FINS/TCP or UDP) | Use Serial (Host Link) |
|---|---|---|
| Latency | 5–20 ms typical | 50–200 ms at 9600 baud |
| Throughput | Thousands of words/s | ~50 words/s with 1 ms scan |
| Cable distance | 100 m per hop, switches extend | 15 m RS-232C, 1200 m RS-485 |
| Modernity | Recommended for new gateways | Legacy / brownfield |
| PLC effort | Low (ETN21 handles routing) | CPU serial port cycles per char |
8. Tag Mapping and Polling Patterns
A SCADA gateway rarely reads one word at a time. The recommended pattern is to build a tag list, then issue a single 01 05 Multiple Memory Area Read per scan for all tags that share the same FINS node, batching by maximum 198 words. A second 01 05 handles any overflow or tags in different EM banks.
8.1 Address Resolution Rules
-
DM100(CX-Programmer notation) → area code0x82, word 100. -
D20100→ area code0x02, word 100. The D20000 offset is implied. -
CIO 0.07→ area code0x30, word 0, bit 0x07. -
HR10→ area code0x32, word 10. -
E0_0→ area code0x18, word 0 (EM bank 0).
8.2 Polling Cadence
On a CJ1M-CPU13, the FINS response time for a 198-word read is typically < 30 ms over a 100 Mbit LAN. A 100 ms scan gives three full reads per second and is a sensible default. Avoid issuing a new FINS request before the previous response arrives on the same TCP socket; on UDP, allow a 100 ms timeout for lost frames before retrying with the same SID.
9. .NET Implementation Building Blocks
9.1 Frame Encoder
public static class FinsEncoder
{
public static byte[] BuildReadWords(byte area, int address, ushort count)
{
if (count == 0 || count > 999)
throw new ArgumentOutOfRangeException(nameof(count));
return new byte[]
{
0x01, 0x01, // Memory Area Read
area,
(byte)(address & 0xFF),
(byte)((address >> 8) & 0xFF),
(byte)((address >> 16) & 0x0F),
(byte)(count & 0xFF),
(byte)((count >> 8) & 0xFF)
};
}
public static byte[] BuildWriteWords(byte area, int address, ushort[] data)
{
if (data.Length == 0 || data.Length > 999)
throw new ArgumentOutOfRangeException(nameof(data));
var body = new byte[6 + data.Length * 2];
body[0] = 0x01; body[1] = 0x02; // Memory Area Write
body[2] = area;
body[3] = (byte)(address & 0xFF);
body[4] = (byte)((address >> 8) & 0xFF);
body[5] = (byte)((address >> 16) & 0x0F);
body[6] = (byte)(data.Length & 0xFF);
body[7] = (byte)((data.Length >> 8) & 0xFF);
for (int i = 0; i < data.Length; i++)
{
body[8 + i*2] = (byte)(data[i] & 0xFF);
body[8 + i*2 + 1] = (byte)((data[i] >> 8) & 0xFF);
}
return body;
}
}
9.2 Async Tag Poller
public sealed class FinsTagPoller
{
private readonly FinsUdpClient _fins;
private readonly CancellationTokenSource _cts = new();
public event Action<TagSnapshot>? OnSnapshot;
public async Task RunAsync(TimeSpan period, IEnumerable<FinsTag> tags)
{
var grouped = tags.GroupBy(t => t.FinsNode).ToList();
while (!_cts.IsCancellationRequested)
{
var snap = new TagSnapshot(DateTime.UtcNow);
foreach (var g in grouped)
{
var body = FinsEncoder.BuildMultiRead(g.Select(t => t.Descriptor).ToArray());
var resp = await _fins.SendAsync(body, _cts.Token).ConfigureAwait(false);
FinsDecoder.UnpackMultiRead(resp, g.ToList(), snap);
}
OnSnapshot?.Invoke(snap);
await Task.Delay(period, _cts.Token).ConfigureAwait(false);
}
}
}
9.3 Graceful Shutdown and Reconnect (TCP)
while (!ct.IsCancellationRequested)
{
try
{
using var tcp = new TcpClient();
await tcp.ConnectAsync(_ip, _port, ct);
await using var ns = tcp.GetStream();
while (!ct.IsCancellationRequested)
{
var req = await _queue.Reader.ReadAsync(ct);
var frame = BuildFrame(req.Sid, req.Body);
var length = BitConverter.GetBytes(frame.Length).Reverse().ToArray();
await ns.WriteAsync(length, ct);
await ns.WriteAsync(frame, ct);
var hdr = new byte[4];
await ReadExactAsync(ns, hdr, ct);
int n = (hdr[0] << 24) | (hdr[1] << 16) | (hdr[2] << 8) | hdr[3];
var resp = new byte[n];
await ReadExactAsync(ns, resp, ct);
req.CompletionSource.TrySetResult(resp);
}
}
catch (Exception ex) when (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(2), ct);
// log ex, loop back to reconnect
}
}
10. Error Codes and Recovery
| MRES (hex) | SRES (hex) | Meaning | Recommended Action |
|---|---|---|---|
| 00 | 00 | Success | None |
| 02 | 01 | Local node not in network | Verify PLC FINS routing table; ensure ETN21 is online |
| 02 | 02 | Token timeout | Increase timeout, check for network congestion |
| 02 | 03 | Retries exhausted | Check cabling / switch, verify PLC in RUN/MONITOR |
| 03 | 01 | Destination node not in network | Wrong DA1 / DA2 — confirm FINS node = last IP octet |
| 03 | 02 | No such unit | Wrong DA2; use 0x00 for CPU, 0xFE for ETN21 |
| 04 | 01 | Undefined command | Check command code; CJ1M does not implement all of W421 |
| 05 | 01 | Destination address error | Address outside area range (e.g., DM 40000 on CPU13) |
| 05 | 02 | Wrong number of items | Item count 0, > 999, or 0 when non-zero required |
| 10 | 01 | Area classification error | Used the wrong area code (e.g., 0x00 vs 0x30 for CIO) |
| 11 | 01 | Read prohibited | UM/protect bits set, or area is write-only |
| 11 | 02 | Write prohibited | CPU is in PROGRAM mode and area is read-only then |
| 22 | 01 | CPU in PROGRAM mode and command not allowed | Switch CPU to MONITOR or RUN before issuing writes |
| 23 | 01 | File device error | EM file / memory card issue |
| 26 | 01 | Protection / password | UM read protect is on; supply password via FINS |
For a complete list, see W421-E1 §8 "Error Code Lists." The 2-byte end code appears at offsets 12–13 of the response frame.
11. Verification and Commissioning
-
Layer-2 ping. From a Windows admin shell, run
ping 10.0.0.100. The ETN21 should reply. -
Port probe. Use
Test-NetConnection 10.0.0.100 -Port 9600(PowerShell) to confirm TCP FINS is open. - CX-Programmer online. Connect via Ethernet in CX-Programmer and read CIO 0.00 to confirm the rack and ETN21 are correctly configured.
-
Minimal .NET test. Use the
FinsUdpClientclass above to read 5 words from DM 0. Expect a0000end code and 10 bytes of payload. -
Multi-area read smoke test. Combine CIO, HR, DM in a single
01 05and verify each unpacked word matches the CX-Programmer value. - Write / read-back. Write a known pattern (e.g., 0x1234) to DM 200, then read it back. This catches address/area-code mistakes that would otherwise silently corrupt data.
- Stress test. Poll 198 words at 100 ms for 10 minutes; verify no SID drops, no exception leaks, and no socket exhaustion.
- Fault injection. Disconnect the Ethernet cable; confirm reconnect logic restores communication without leaking sockets.
12. Common Pitfalls and Field Notes
- Wrong GCT. Setting GCT=0 in a FINS header going PC → ETN21 → CPU yields 0x02-01 "Local node not in network." GCT counts the number of networks to traverse, not the number of nodes.
- Confusing FINS node with IP last octet. A node number of 100 maps to a /24 address ending in .100 only when the network administrator has not manually remapped. Verify in ETN21 setup → FINS Node Number.
- DM area code mismatch. Mixing 0x02 and 0x82 for the same D-word reads two different physical locations on some firmware revisions. Standardize on one and document it.
-
UDP receive race. A single
UdpClientinstance can interleave responses if multiple SIDs are in flight. Either serialize with a SemaphoreSlim or use TCP. - Endianness. FINS is little-endian for both address and data. Reading a D-word and showing it as the wrong value is almost always an endianness bug, not a PLC bug.
-
Time data is BCD.
07 01returns time as packed BCD, not decimal.0x21= 21, not 33. - UM protection. Reading the program (UM) area without supplying the password returns 0x26-01. Most SCADA gateways should not need UM access; stay in CIO/HR/DM/AR/WR/EM.
13. Extending the Gateway
Once the CIO/HR/DM core is stable, layer in:
- Tag-list import from CX-Programmer CSV exports or a JSON manifest. Validate every area code and address against a built-in table to catch typos at startup, not at 2 a.m.
- OPC UA server on top of the poller. The Software Toolbox overview of Omron protocols highlights OPC UA bridges as the modern replacement for FINS-only SCADA.
- Event-driven reads using MSG instructions inside the ladder. The PLC sends unsolicited FINS messages to a registered SA1; the .NET listener must maintain an open UDP/TCP socket and a SID table for matching.
- Redundancy. Two ETN21 units on the same rack with different FINS networks and a watchdog in the gateway. Verify failover end-to-end by pulling an Ethernet cable.
- Modern replacements. Newer Omron CPUs (NJ/NX) drop FINS-on-TCP in favor of EtherNet/IP (CIP) and OPC UA. If a refresh is on the roadmap, plan a parallel implementation against the NX-series FINS Function Manual W596 and the EtherNet/IP EDS for CIP-Forward-Open.
14. Reference Summary
| Document | Number | Coverage |
|---|---|---|
| FINS Commands Reference Manual | W421-E1 | All FINS command codes, request/response bodies, error codes |
| CS/CJ Ethernet Units Operation Manual | W342 / W420 | ETN21 setup, routing tables, FINS node / port configuration |
| Host Link Command Reference | W227-E1 | Serial C-mode protocol, FA header, FCS |
| NX-series CPU Unit FINS Function | W596-E1 | Modern FINS behavior on NJ/NX; useful for forward-compatibility |
| CS/CJ Series Programming Manual | W394 / W474 | Memory map, EM bank allocation, PLC Setup |
| ProFace CS/CJ Ethernet Driver notes | n/a (PDF) | Cross-vendor confirmation of port, FINS node, and routing assumptions |
What is the default FINS port for an Omron ETN21?
Both TCP and UDP FINS use port 9600 on the ETN21. Some legacy bridges use 9600 plus an offset for parallel sessions, but a single TCP/UDP connection on 9600 is the modern default. Verify the value in CX-Programmer under ETN21 Setup → FINS/TCP Port.
How do I read DM D00100 from a CJ1M-CPU13 over Ethernet?
Send a FINS Memory Area Read (01 01) with area code 0x82 (legacy DM 0–19999), 3-byte little-endian address 64 00 00 (= 100), and item count 0A 00 for 10 words. The response carries the 10 words little-endian followed by end code 00 00. Use area code 0x02 for the D20000+ region.
Can I use one TCP connection for many concurrent reads?
Yes. FINS on a single TCP socket is half-duplex: issue one request, wait for the response, then issue the next. Serialize access with a SemaphoreSlim(1,1) or an async channel. For parallel polling, open up to 16 TCP connections to the ETN21 — that is the unit's documented maximum.
What causes FINS end code 0x05-01 "destination address error"?
The address you encoded is outside the area's valid range for the target CPU. Common offenders: writing DM 40000 on a CJ1M-CPU13 (max D32767), or reading EM bank 4 (0x20) when EM is not allocated in PLC Setup. Halve the address you are trying, then re-check the area map in W394.
Is FINS still recommended for new Omron projects?
For new designs on NJ/NX/CP-series CPUs, Omron recommends EtherNet/IP (CIP) and OPC UA. FINS remains the right choice for legacy CS/CJ installations like the CJ1M-CPU13 and for any gateway that must read existing programs without an OPC server. The W596 manual documents the modern FINS variants still supported on NX CPUs.