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

ina2000

v2.0.0

Published

Client for the B&R INA2000 PLC protocol over Ethernet (UDP) or serial (RS232/RS485): browse tasks and variables, read, write, subscribe, decode the logbook and discover PLCs

Readme

ina2000

Node.js client for the B&R INA2000 protocol, the service every B&R PLC (and the ARsim simulator of Automation Studio) offers for listing tasks and reading and writing process variables. Works over Ethernet (UDP) or serial (RS232). Browse tasks and variables, read and write values, subscribe to changes, read and set the PLC clock, decode the logbook, run service functions, and discover PLCs. Promise-based API, TypeScript declarations, no required dependencies, a test suite with a fake PLC, a Wireshark dissector and a serial decoder.

Background

IoT solutions are increasingly relevant in the automation world, and together with IoT comes JavaScript. This module was written to read and write data of B&R PLCs from Node.js. INA2000 (Industrial Network Architecture) is a protocol B&R has used since the 1990s; it targets the old generation of PLCs but also works with the current ones, and the service is active by default. Newer B&R PLCs additionally offer ANSL (Automation Network Service Line), which is TCP based, whereas INA2000 is UDP based.

The protocol was reverse engineered from PVI traffic. B&R specific payloads used here comes from observation and is described in the comments of lib/.

Requirements

Node.js 20 or newer. Windows, Linux and macOS.

Install

npm i ina2000

Quick start

const { Ina2000Client } = require('ina2000');

async function main() {
  const plc = new Ina2000Client({ host: '192.168.250.10', port: 11159 });
  await plc.connect();

  // optional: what is running on the PLC?
  const { tasks } = await plc.browse();
  for (const task of tasks) console.log(task.name, task.variables.map((v) => `${v.name}:${v.type.name}`));

  // read and write by name ("Task:variable" or "globalVariable")
  const [counter, label] = await plc.read(['Program:counter', 'Program:label']);
  await plc.write('Program:counter', counter + 1);
  await plc.writeMany({ 'Program:label': 'hello', 'Program:enabled': true });

  // cyclic reading with change notification
  const subscription = await plc.subscribe(['Program:counter', 'gStatus'], { interval: 500 });
  subscription.on('change', (variable, value, previous) => {
    console.log(`${variable.name}: ${previous} -> ${value}`);
  });

  await new Promise((resolve) => setTimeout(resolve, 10000));
  subscription.stop();
  await plc.disconnect();
}

main().catch(console.error);

The package is CommonJS; import { Ina2000Client } from 'ina2000' works in ES modules too. Ports: real PLCs listen on 11159, ARsim on 11160.

Connecting: Ethernet or serial

The same client works over Ethernet (UDP) or serial. Only the constructor options differ; every method below behaves identically once connected.

Ethernet (default). Pass host, and optionally port and timing options:

const { Ina2000Client } = require('ina2000');

const plc = new Ina2000Client({
  host: '192.168.137.50',
  port: 11159,        // 11159 on real PLCs, 11160 on ARsim; default 11159
  timeout: 1000,      // ms to wait for a reply before resending
  retries: 3,         // resends before a request fails
});
await plc.connect();

Serial (RS232). Pass serial instead of host. This needs the optional serialport package, so install it once:

npm i serialport
const plc = new Ina2000Client({
  serial: {
    path: 'COM1',       // or '/dev/ttyUSB0'
    baudRate: 57600,
    parity: 'even',     // 'even' | 'odd' | 'none'
    dataBits: 8,
    stopBits: 1,
  },
});
await plc.connect();
// browse / read / write / subscribe exactly as over Ethernet
console.log(plc.medium);   // 'serial'

Match the serial parameters to the PLC's interface (Automation Studio, Online Settings). INA2000 over serial is the same protocol; only the transport differs, the FDL telegram without the 6-byte UDP header. The web console (npm run webui) has an Ethernet/Serial selector for the same reason.

API

new Ina2000Client(options)

| option | default | meaning | | --- | --- | --- | | host | required for Ethernet | PLC address (omit when using serial) | | port | 11159 | INA2000 UDP port | | serial | none | { path, baudRate, parity, dataBits, stopBits } to connect over serial instead of Ethernet (needs serialport) | | transport | none | inject a custom transport (advanced) | | timeout | 1000 | milliseconds to wait for a reply before resending | | retries | 3 | resends of an unanswered telegram before the request fails | | keepAliveInterval | 1000 | interval of the POLL telegram that keeps the connection open | | stringEncoding | 'latin1' | encoding of STRING values | | localNode | 69 | node number written into the transport header | | logger | silent | object with debug, info, warn functions |

Properties: state (disconnected, connecting, connected, disconnecting), connected, connectionId, tasks (result of the last browse()), variables (registered variables), stats (telegram counters).

Methods

All methods return promises. Requests are sent one at a time (the protocol is stop-and-wait), so calls may be issued concurrently and are queued.

  • connect() establishes the connection and starts the keep-alive.
  • disconnect() stops subscriptions, sends the abort telegram and closes the socket.
  • browse() returns { modules, tasks }. Every module has id, name, type, status, address, size, version, timestamp; task modules (type 17) additionally list their variables as { name, datatype, type: { name, typeid } }. Type names include arrays (INT[0..9]), strings (STRING[80]) and user types (MyStruct).
  • register(names) defines the variables on the PLC and returns their descriptors { name, id, datatype, type, length }. Names already registered on this connection are reused. read, write and subscribe register on demand, so calling register yourself is optional.
  • read(names) returns the values in the same order. readValue(name) returns one value.
  • write(name, value) and writeMany(entries) write values; entries may be an object { name: value }, a Map, or an array of [name, value] pairs. The promise resolves once the PLC acknowledged the telegram.
  • subscribe(names, { interval }) registers the variables and reads them every interval milliseconds (default 500). Returns a Subscription.
  • resolveAddress(name) returns a task-local variable's absolute RAM address from browse data (task data base plus the variable's offset), over pure INA. Supports array elements (Task:arr[5]); throws for struct members and PV/global variables.
  • readMemory(address, length) reads raw PLC memory (used by browse).

Events

  • connect ({ connectionId })
  • disconnect ({ reason }): reason is null after disconnect() and an Error when the PLC stopped answering keep-alive telegrams. The client is back in the disconnected state; call connect() again and re-subscribe.
  • error (err): emitted for a lost connection only when a listener exists.

Subscription

  • variables, interval, values (a Map of the last values), active
  • stop()
  • event data (values: Map) after every successful cycle
  • event change (variable, value, previous) for every changed value; the first cycle reports every variable with previous === undefined
  • event error (err) when a cycle failed (logged when nobody listens)

discover(options)

Broadcasts identify datagrams and resolves with the PLCs that answered: [{ address, port, node, raw }].

const { discover } = require('ina2000');
const found = await discover({ broadcast: '192.168.250.255', port: 11159, from: 1, to: 100 });

Options: broadcast (default 255.255.255.255), port, from/to node numbers (1..100), timeout (ms to wait after the last datagram, 1000), stagger (ms between datagrams, 10), bindAddress.

connect(options)

Shortcut that creates a client and connects it.

Errors

TimeoutError (no reply after the retries), ConnectionError (not connected), VariableError (a variable was not found; names lists them), ProtocolError (unexpected reply; frame holds the raw datagram).

Values and data types

| PLC type | JavaScript | notes | | --- | --- | --- | | BOOL | boolean | any non-zero byte reads as true | | SINT, INT, DINT, USINT, UINT, UDINT | number | range checked on write | | REAL | number | IEEE 754 single precision | | STRING | string | ends at the first NUL; truncated to the declared length on write | | arrays of the above (USINT[0..99]) | Array | whole array; missing elements are written as 0 | | other (structures, LREAL, TIME, ...) | Buffer | raw bytes of the registered length |

Variable names use Task:variable for task local variables and variable for global ones. Single array elements work (Program:values[3]). Whole structures cannot be registered through the variable-list service; read and write them by name with readStruct / writeStruct after a browse(), which resolve the address and go through memory instead.

Structures

Define a structure's layout once, then validate, pack and unpack values against it. defineStruct fills in member offsets by natural alignment (or use explicit offset values you have verified against the PLC).

const { defineStruct, validate } = require('ina2000');

const settings = defineStruct({
  name: 'SettingsType',
  members: [
    { name: 'setpoint', type: 'INT' },
    { name: 'enabled', type: 'BOOL' },
    { name: 'label', type: 'STRING', size: 21 },   // bytes including NUL
    { name: 'limits', type: 'INT', count: 2 },      // INT[0..1]
  ],
});

validate(settings, { setpoint: 70000 });
// throws ValidationError: setpoint: 70000 is out of range for INT [-32768..32767]

Every write is validated before any bytes are sent, with one issue per bad member and a dotted path (sub.limits[1]). Nested structures and arrays of structures are supported.

You often do not need to write a schema at all. After a browse(), the client reads each task's type table, so it knows a structure's members (name, type and offset) and builds the schema itself. Reading or subscribing to a struct by name returns a decoded object with named members, and writeStruct with a null schema builds one from browse data:

await plc.browse();
const s = await plc.readValue('IMA_Serv:struct_data');   // { a: 123, b: 456 }
await plc.writeStruct('IMA_Serv:struct_data', null, { a: 7, b: 8 });
plc.structSchema('IMA_Serv:struct_data');                // the built schema, or null

structSchema returns null when a member type is not a decodable base type (a nested struct, for example); pass your own schema for those. You can still supply a schema explicitly, which also sets the byte order (defaults to little-endian, the native order):

const value = await plc.readStruct('Program:settings', settings);
await plc.writeStruct('Program:settings', settings, { setpoint: 1500, enabled: true, label: 'run', limits: [0, 100] });

Resolving names to addresses

After a browse(), the client can turn a task-local variable name into its absolute PLC RAM address over pure INA, the way Automation Studio does: the owning task's data-segment base (from the module list) plus the variable's offset (from the variable table). No ANSL and no address from you.

await plc.browse();
plc.resolveAddress('IMA_Serv:struct_data');   // e.g. 0x04824db4
plc.resolveAddress('Program:hugeVariable[3]'); // array element address

This works for task-local variables and array elements. Struct members (Task:s.field) and PV/global variables (resolved by the PLC's own lookup service) are not resolvable this way yet and throw a VariableError.

Large arrays and structs

A large array such as INT[0..999] is bigger than one read reply. After a browse(), read and subscribe resolve its address and fetch it in one chunked memory read, returning a JavaScript array. Without browse data they fall back to reading it element by element (slower, but no address needed).

const all = await plc.readValue('Program:hugeVariable');   // 1000 numbers

Writing a whole large array by name is not supported (set elements individually, Program:hugeVariable[3], or use writeMemory).

Structs read and write by name too, once you have browsed. readStruct and writeStruct resolve the address over INA and read or write the structure in memory; you only supply the schema:

await plc.browse();
const value = await plc.readStruct('Program:settings', settings);
await plc.writeStruct('Program:settings', settings, { setpoint: 1500, enabled: true, label: 'run', limits: [0, 100] });

If a name cannot be resolved (before browse(), or a PV/global variable), these fall back to the variable-list service, which only works for the rare registerable struct. You can always read or write at an explicit address with readStructAt / writeStructAt, or raw bytes with readMemory / writeMemory:

const value = await plc.readStructAt(0x04824db4, settings);
await plc.writeStructAt(0x04824db4, settings, { setpoint: 1500, enabled: true, label: 'run', limits: [0, 100] });
const buf = await plc.readMemory(0x04824db4, 2000);   // e.g. a 2000-byte array
await plc.writeMemory(0x04824db4, buf);

Loggers and modules

Read a B&R logger module and decode its binary logbook into records, over pure INA using the FMS upload sequence. The module must exist in the module list, which is checked first.

const entries = await plc.readLogbook('System');   // $arlogsys
for (const e of entries) {
  console.log(e.time.toISOString(), e.severity, e.area, e.code, e.message);
}
// { recordId, severity: 'Info'|'Warning'|'Error'|'Success', time: Date, code, area, message }

const userLog = await plc.readLogbook('User');           // $arlogusr
const named = await plc.readLogbook('myUserLogger');     // a user logger by name
const raw = await plc.readLog('System');                 // undecoded bytes as text

The human readable description a tool shows beside each entry is a code lookup from B&R's error database and is not stored in the log, so readLogbook returns the record's own message text and code, not that description.

Logger types map the reserved names: System $arlogsys, User $arlogusr, Fieldbus $fieldbus, Safety $safety, Redundancy $redund, Connectivity $arlogconn, TextSystem $textsys, UnitSystem $unitsys, AccessAndSecurity $accsec, Visualization $visu. Not every logger exists on every PLC, so readLog throws if the module is not in the module list.

uploadModule(name) returns the raw bytes of any uploadable module, and getStatus() returns { version, cpu, raw } from the status report.

Limits

INA2000 telegrams are at most 255 bytes. The client splits requests accordingly: at most 29 variables and 220 value bytes per read telegram, 14 names per registration telegram, and memory reads in chunks of 226 bytes. A whole array larger than one reply (for example an INT[0..999]) or a struct is read from memory at its resolved address after a browse(), or, without browse data, an array is read element by element and returned as a JavaScript array. The variable service offers no partial reads. The variable tables read by browse() do carry each task-local variable's segment offset, so the client resolves names to addresses over INA (see resolveAddress); PV/global variables are resolved by a separate PLC lookup service that is not implemented yet.

readMemory() is meant for the task module images that browse() reads. Asking ARsim for an address outside its memory map produced no reply at all (the request times out and the connection is closed), so do not probe addresses blindly.

Upgrading from 1.x

The 1.x API is available unchanged from the legacy entry point:

const ina2000 = require('ina2000/legacy');
ina2000.connect('127.0.0.10', 11160).on('connected', (e) => {
  ina2000.registerVariable(['gVar', 'Program:localVar'], 1000).on('valueChanged', (variable, value) => {
    console.log(variable.name, value);
  });
});

| 1.x | 2.x | | --- | --- | | connect(ip, port).on('connected') | await plc.connect(); await plc.browse() | | registerVariable(names, cycle).on('valueChanged') | (await plc.subscribe(names, { interval })).on('change') | | registerDone event | subscription.variables | | writeValue(variable, value) | await plc.write(name, value) | | searchPLCs(broadcast, port, from, to).on('plcSearchFinished') | await discover({ broadcast, port, from, to }) | | disconnect() | await plc.disconnect() |

See CHANGELOG.md for the behaviour changes and the bugs fixed along the way.

Wireshark

tools/wireshark/ina2000.lua dissects INA2000 telegrams (FDL frame, LLI, FMS PDU and the B&R reports). Copy it into Wireshark's personal plugins folder or start with -X lua_script:tools/wireshark/ina2000.lua; tools/wireshark/README.md lists useful display filters, and sample-session.pcapng is a recorded session against ARsim.

Set NODE_DEBUG=ina2000 to print every telegram the client sends and receives as hex.

Protocol notes

A datagram is a 6 byte transport header (01, node, FDL length, 00 00) followed by a Profibus style FDL telegram: 68 LE LE 68, DA, SA, FC, DSAP, SSAP, DATA, FCS (sum of DA..DATA mod 256), 16. DATA holds an LLI-FC byte and an FMS PDU (ID-Info 1, invoke id, ID-Info 2, service data). Variable and module operations are B&R specific "informationReport" PDUs on object index fe 03. The frame control byte carries the frame count bit: it toggles with every new request, stays the same on a retry, and a PLC that receives the same bit twice repeats its previous reply instead of executing the request again. lib/constants.js and lib/requests.js document the byte layouts.

Development

npm test                                   # unit tests plus a fake PLC over UDP
node examples/browse.js 127.0.0.10 11160   # against ARsim
node examples/write.js 127.0.0.10 11160 Program:writeMe 42
node examples/subscribe.js 127.0.0.10 11160 Program:writeMe gVar
node examples/discover.js 192.168.250.255 11159 1 100

The original implementation is kept in legacy/index.js for reference; test/requests.test.js proves that the new telegram builders produce the same bytes.

Test environment

A runnable simulator can be downloaded from Google Drive: unzip it and run ar000loader; an icon appears in the tray. The development environment is available from the B&R download page and is only needed to change what runs in the simulator.

Contact

Issues, questions and feature requests: [email protected]. A short note if you end up using this package, even just for testing, is appreciated.

License

ISC