npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ethernet-ip

v2.0.0

Published

A feature-complete EtherNet/IP client for Rockwell ControlLogix/CompactLogix PLCs

Readme


Node Ethernet/IP

A feature-complete EtherNet/IP client for Rockwell ControlLogix/CompactLogix PLCs.

  • Full TypeScript with strict types
  • Dependency injection for testability (MockTransport)
  • Connected messaging with Forward Open (Large/Small fallback)
  • Complete data type support (all atomics, STRING, SHORT_STRING, STRUCT, arrays)
  • Lazy tag type discovery with optional full tag list retrieval
  • Auto-reconnect with exponential backoff
  • Tag subscriptions with change detection
  • Typed error hierarchy with human-readable CIP status codes
  • Injectable logger (noop default)
  • 383+ unit tests

Prerequisites

Node.js >= 18.0.0

Install

npm install ethernet-ip

The API

Connecting to a PLC

import { PLC } from 'ethernet-ip';

const plc = new PLC();

// Connect to a CompactLogix at 192.168.1.1, slot 0
await plc.connect('192.168.1.1');

// Connect to a ControlLogix in slot 2
await plc.connect('192.168.1.1', { slot: 2 });

// Connect with full tag discovery (fetches all tags on connect)
await plc.connect('192.168.1.1', { discover: true });

// Connect with auto-reconnect
await plc.connect('192.168.1.1', { autoReconnect: true });

Connect Options

| Option | Type | Default | Description | | --------------- | ----------------------------- | ------- | ------------------------------------------------------------------------------- | | slot | number | 0 | Controller slot number (0 for CompactLogix) | | discover | boolean | false | Fetch full tag list on connect | | connected | boolean | true | Use connected messaging (Forward Open). Set false for unconnected (UCMM) only | | timeout | number | 10000 | Connection timeout in milliseconds | | autoReconnect | boolean \| ReconnectOptions | false | Enable auto-reconnect on disconnect |

ReconnectOptions

{
  enabled: true,
  initialDelay: 1000,   // First retry after 1 second
  maxDelay: 30000,      // Cap at 30 seconds
  multiplier: 2,        // Double the delay each attempt
  maxRetries: Infinity,  // Retry forever
}

Reading Tags

Read a single tag — the type is discovered automatically on first read and cached:

const value = await plc.read('MyDINT');
// value: 42 (number)

const temp = await plc.read('Temperature');
// temp: 72.5 (number)

const running = await plc.read('MotorRunning');
// running: true (boolean)

const name = await plc.read('MachineName');
// name: "Press 1" (string)

Read multiple tags — automatically batched into optimal multi-service packets:

const [speed, temp, status] = await plc.read(['Speed', 'Temperature', 'Status']);

Read a bit of a word:

// Read bit 5 of a DINT tag
const bit5 = await plc.read('MyDINT.5');
// bit5: true (boolean)

Read program-scoped tags:

const value = await plc.read('Program:MainProgram.LocalTag');

Read array elements:

const element = await plc.read('MyArray[3]');
const multiDim = await plc.read('Matrix[1,2]');

Read UDT members:

const member = await plc.read('MyUDT.Member1');

Return Types

| PLC Type | JavaScript Type | | ------------------------------------------------ | --------------- | | BOOL | boolean | | SINT, INT, DINT, USINT, UINT, UDINT, REAL, LREAL | number | | LINT, LWORD | bigint | | STRING, SHORT_STRING | string | | STRUCT (with template) | object | | STRUCT (unknown template) | Buffer |

Writing Tags

Write a single tag — the type must be known (read the tag first, or use registry.define()):

await plc.write('SetPoint', 72.5);
await plc.write('EnableMotor', true);
await plc.write('MachineName', 'Press 2');

Write multiple tags:

await plc.write({
  SetPoint: 72.5,
  EnableMotor: true,
  BatchCount: 0,
});

Write a bit of a word:

// Set bit 5 of a DINT tag to true
await plc.write('ControlWord.5', true);

Tag Registry

Types are discovered lazily — the first read() of a tag discovers its type and caches it. For optimal first-batch performance, you can pre-register types:

import { CIPDataType } from 'ethernet-ip';

plc.registry.define('MyDINT', CIPDataType.DINT, 4);
plc.registry.define('MyString', CIPDataType.STRING, 88);

// Now batch reads can be optimally packed without discovery round trips
const values = await plc.read(['MyDINT', 'MyString']);

Or discover all tags on connect:

await plc.connect('192.168.1.1', { discover: true });
// plc.registry now has every tag's type and UDT templates

UDT / Struct Support

Struct tags are automatically decoded into JS objects when the template is available:

const motor = await plc.read('MotorStatus');
// motor: { Running: true, Speed: 1750, Current: 12.5 }

await plc.write('MotorControl', { Enable: true, SpeedSP: 1800 });

Discover tags and inspect struct shapes:

const tags = await plc.discover();
// tags: [{ name: 'MotorStatus', type: { code: 0x3b2, isStruct: true, arrayDims: 0, dimSizes: [] } }, ...]

// Array tags include dimension sizes
const arr = tags.find((t) => t.name === 'Matrix');
// arr.type.arrayDims = 2, arr.type.dimSizes = [10, 5]  →  Matrix[10, 5]

const shape = plc.getShape('MotorStatus');
// { name: 'stMotorStatus', members: {
//     Running: { type: 'BOOL' },
//     Speed:   { type: 'REAL' },
//     Current: { type: 'REAL' },
// }}

const template = plc.getTemplate('MotorStatus');
// Raw template with byte offsets, member info, structureSize

const dims = plc.getDimensions('Matrix');
// [10, 5]  →  Matrix[10, 5]
// Returns [] for scalars or unknown tags

Scanning / Subscriptions

Monitor tags for changes. All tags share a single scan rate, set at construction:

import { Scanner } from 'ethernet-ip';

// Create a scanner with 200ms scan rate (default)
const scanner = new Scanner(async (tags) => plc.read(tags), { rate: 200 });

// Inject a logger for scan metrics (logged every ~5 minutes at debug level)
const scannerWithMetrics = new Scanner(async (tags) => plc.read(tags), { rate: 200, logger });

// Subscribe tags — can add/remove while scanning
scanner.subscribe('Temperature');
scanner.subscribe('BatchCount');

// Listen for changes
scanner.on('tagInitialized', (tag, value) => {
  console.log(`${tag} initialized: ${value}`);
});

scanner.on('tagChanged', (tag, value, previousValue) => {
  console.log(`${tag} changed: ${previousValue} → ${value}`);
});

scanner.on('scanError', (err) => {
  console.error('Scan error:', err.message);
});

// Start scanning
scanner.scan();

// Add/remove tags while running — picked up on next tick
scanner.subscribe('NewTag');
scanner.unsubscribe('BatchCount');

// Pause scanning (subscriptions preserved)
scanner.pause();

// Resume
scanner.scan();

Auto-Reconnect

await plc.connect('192.168.1.1', {
  autoReconnect: {
    enabled: true,
    initialDelay: 1000,
    maxDelay: 30000,
    multiplier: 2,
    maxRetries: Infinity,
  },
});

plc.on('disconnected', () => {
  console.log('Connection lost');
});

plc.on('reconnecting', (attempt) => {
  console.log(`Reconnect attempt ${attempt}...`);
});

plc.on('connected', () => {
  console.log('Connected');
  // Tag registry is preserved — no re-discovery needed
});

plc.on('error', (err) => {
  console.error('Error:', err.message);
});

Connection State

plc.isConnected; // true when connected, false otherwise

Logger

Inject a logger for observability. Default is noop — no console output unless you provide one:

import { PLC, Logger } from 'ethernet-ip';

const logger: Logger = {
  debug: (msg, ctx) => console.log('[DEBUG]', msg, ctx),
  info: (msg, ctx) => console.log('[INFO]', msg, ctx),
  warn: (msg, ctx) => console.warn('[WARN]', msg, ctx),
  error: (msg, ctx) => console.error('[ERROR]', msg, ctx),
};

const plc = new PLC({ logger });

Generic CIP Messaging

Escape hatch for raw CIP requests — specify service, class, instance, and optionally attribute:

import { buildGenericCIPMessage } from 'ethernet-ip';

// Get Attribute Single: service=0x0E, class=0x8B, instance=0x01, attribute=0x05
const request = buildGenericCIPMessage(0x0e, 0x8b, 0x01, 0x05);

// Get Attribute All: service=0x01, class=0x01, instance=0x01
const identityRequest = buildGenericCIPMessage(0x01, 0x01, 0x01);

// Set Attribute Single with data
const data = Buffer.alloc(4);
data.writeUInt32LE(42, 0);
const writeRequest = buildGenericCIPMessage(0x10, 0x01, 0x01, 0x05, data);

Controller Info

import {
  buildGetControllerPropsRequest,
  parseControllerProps,
  buildReadWallClockRequest,
  parseWallClockResponse,
  buildWriteWallClockRequest,
} from 'ethernet-ip';

Testing with MockTransport

Every layer can be tested without PLC hardware:

import { PLC, MockTransport } from 'ethernet-ip';

const transport = new MockTransport();
const plc = new PLC({ transport });

// transport.sentData contains all packets sent
// transport.injectResponse(buf) simulates PLC responses
// transport.triggerClose() simulates disconnect

EPATH Builder

Fluent builder for CIP EPATH construction:

import { EPathBuilder, LogicalType } from 'ethernet-ip';

// CIP object addressing
const path = new EPathBuilder()
  .logical(LogicalType.ClassID, 0x06)
  .logical(LogicalType.InstanceID, 0x01)
  .build();

// Tag path: "MyTag[3].Member"
const tagPath = new EPathBuilder().symbolic('MyTag').element(3).symbolic('Member').build();

// Routing: backplane port 1, slot 2
const routePath = new EPathBuilder().port(1, 2).build();

Architecture

Layer 6  User API          PLC · Scanner · Discovery
Layer 5  Session Manager   State machine · Auto-reconnect · Forward Open fallback
Layer 4  Request Pipeline  Serial queue · Timeout · TCP reassembly · Fragmentation
Layer 3  CIP Protocol      EPATH · DataTypeCodec · MessageRouter · BatchBuilder
Layer 2  EIP Encapsulation Headers · CPF · Commands
Layer 1  Transport (DI)    ITransport → TCP / UDP / Mock

See architecture.md for the full design document.

Testing

npm test              # Run all tests
npm run test:watch    # Watch mode
npm run test:coverage # With coverage report
npm run lint          # ESLint
npm run format        # Prettier (write)
npm run format:check  # Prettier (check only)
npm run check         # All checks: lint + format + tsc + tests

Migration from v1

Breaking Changes

| v1 | v2 | | ---------------------------------------------- | ----------------------------------------------- | | JavaScript | TypeScript (strict mode) | | new Controller() | new PLC() | | PLC.connect(ip, slot) | plc.connect(ip, { slot }) | | new Tag('name'); PLC.readTag(tag) | plc.read('name') | | tag.value = 42; PLC.writeTag(tag) | plc.write('name', 42) | | PLC.subscribe(tag); PLC.scan() | scanner.subscribe('name'); scanner.scan() | | Extends net.Socket | Composition with ITransport | | Event strings ("Read Tag") | Typed events ('tagChanged') | | sendUnitData uses SequencedAddrItem (0x8002) | Uses ConnectionBased (0xA1) per CIP spec | | No connected messaging | Forward Open with Large/Small fallback | | Atomic types only | All types including STRING, STRUCT, LINT, LREAL |

Before (v1)

const { Controller, Tag, TagGroup } = require('ethernet-ip');

const PLC = new Controller();
await PLC.connect('192.168.1.1', 0);

const tag = new Tag('MyTag');
await PLC.readTag(tag);
console.log(tag.value);

tag.value = 42;
await PLC.writeTag(tag);

After (v2)

import { PLC } from 'ethernet-ip';

const plc = new PLC();
await plc.connect('192.168.1.1');

const value = await plc.read('MyTag');
console.log(value);

await plc.write('MyTag', 42);

Contributors

  • Canaan SeatonOwnerGitHubWebsite
  • Patrick McDonaghCollaboratorGitHub
  • Jeremy HensonCollaboratorGitHub

Related Projects

Wanna become a contributor? Here's how!

License

This project is licensed under the MIT License — see the LICENSE file for details.