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

node-s7-client

v1.0.0

Published

Type-safe Node.js wrapper for node-snap7 — event-driven S7 PLC client with automatic reconnection, adaptive DB polling and full TypeScript support.

Readme

node-s7-client — S7 PLC Client for Node.js

A high-level, event-driven TypeScript client for communicating with Siemens S7 PLCs via the node-snap7 native library.

It wraps the low-level snap7 API with:

  • Automatic connection management with exponential-backoff reconnection
  • Typed DataBlock polling with adaptive interval (slows down when idle, backs off on errors)
  • Strongly-typed S7 data types (Bool, Byte, Word, DWord, Int, DInt, Real, Char, String)
  • Fully-typed EventEmitter events for connection state, PLC status, and tag changes

Table of Contents


Quick Start

import { Client } from 'node-s7-client';

// 1. Create a client pointing to your PLC
const client = new Client({
  ip: '192.168.1.10',
  rack: 0,
  slot: 1,
  reconnect: true,
  reconnectInterval: 3000,
  reconnectMaxInterval: 60_000,
});

// 2. Register a DataBlock with its tags
// (auto-starts polling when the client connects)
const db = client.register({
  dbNumber: 10,
  pollInterval: 500,
  tags: [
    { name: 'speed', type: 'Int', offset: [0, 0], initialValue: 0 },
    { name: 'alarm', type: 'Bool', offset: [4, 2], initialValue: false },
    { name: 'setpoint', type: 'Real', offset: [6, 0], initialValue: 0.0 },
    { name: 'label', type: 'String', offset: [10, 0], dimension: 16, initialValue: '' },
  ],
});

// 3. Listen for tag changes
db.on('changed', (tag, value) => {
  console.log(`[DB${db.dbNumber}] ${tag.name} (${tag.type}) = ${value}`);
});

// 4. Subscribe to client events
client.on('connected', () => {
  console.log('PLC connected');
});

client.on('disconnected', () => {
  console.log('PLC disconnected');
});

client.on('reconnecting', ({ interval, attempt }) => {
  console.log(`Reconnecting in ${interval} ms — attempt ${attempt}`);
});

client.on('status', (status) => {
  console.log(`PLC status: ${status}`);
});

client.on('error', (e) => {
  console.error('Client error:', e.message);
});

// 5. Connect
await client.connect();

// 6. Read a tag value
const { value } = db.get('speed');

console.log('Current speed:', value);

// 7. Write a tag value
await db.set('alarm', true);

// 8. Snapshot of all tags
console.log(db.dump());

// 9. Disconnect cleanly
client.disconnect();

Architecture

┌───────────────────────────────────────────────────────────────┐
│  Client (EventEmitter)                                        │
│                                                               │
│  • Manages one TCP/S7 connection (node-snap7 S7Client)        │
│  • Connection state machine:                                  │
│    disconnected → connecting → connected → disconnecting      │
│    connected → reconnecting → connected (on drop)             │
│  • Periodic connection health-check (connectionCheckInterval) │
│  • Periodic PLC status poll (statusCheckInterval)             │
│  • Exponential-backoff reconnect loop                         │
│  • Owns a Set<DataBlock> — subscribes/unsubscribes them       │
│    automatically on connect/disconnect/reconnect              │
└────────────────────────┬──────────────────────────────────────┘
                         │ client.client (snap7.S7Client)
              ┌──────────▼──────────────────────────┐
              │  DataBlock (EventEmitter)           │
              │                                     │
              │  • Maps to one S7 DB number         │
              │  • Holds a local byte Buffer for    │
              │    the exact range covered by tags  │
              │  • Polls ReadArea on a timer        │
              │    (adaptive: slows down when       │
              │    idle, backs off on error)        │
              │  • Emits 'changed' on value diff    │
              │  • WriteArea on db.set()            │
              └─────────────────────────────────────┘

Connection State Machine

| From state | Event / action | To state | | -------------- | --------------------- | -------------------------------- | | disconnected | connect() called | connecting | | connecting | Handshake success | connected | | connecting | Handshake failure | disconnected | | connected | disconnect() called | disconnectingdisconnected | | connected | Unexpected drop | disconnectedreconnecting | | reconnecting | Reconnect success | connected | | reconnecting | disconnect() called | disconnected |


Polling Strategy

Connection health-check (#connectionPoll)

Runs every connectionCheckInterval ms (default 2000). Calls snap7.Connected() and triggers #onDisconnected() if it returns false.

PLC status poll (#statusPoll)

Runs every statusCheckInterval ms (default 5000). Calls PlcStatus() and emits the status event only when the PLC mode changes. Set statusCheckInterval: 0 to disable.

DataBlock poll (#poll)

| Outcome of last poll | Next interval | | -------------------- | ------------------------------------------- | | Value changed | Reset to pollInterval (default 1000 ms) | | No change | Grow ×1.5, capped at 30 000 ms | | Read error | Grow ×2, capped at 60 000 ms |

After an error, the interval resets to pollInterval on the next successful read.


API Reference

Client

new Client(options?: ClientOptions)

ClientOptions

| Option | Type | Default | Description | | ------------------------- | --------- | -------------- | --------------------------------------------------- | | name | string | 'local:<ip>' | Friendly name for logging | | ip | string | '127.0.0.1' | PLC IP address | | port | number | 102 | S7 TCP port | | rack | number | 0 | PLC rack number | | slot | number | 1 | PLC slot number | | reconnect | boolean | true | Automatically reconnect on unexpected disconnection | | reconnectInterval | number | 3000 | Initial reconnect delay in ms | | reconnectMaxInterval | number | 60_000 | Maximum reconnect delay in ms | | connectionCheckInterval | number | 2000 | How often to check TCP health in ms | | statusCheckInterval | number | 5000 | How often to poll PLC status in ms; 0 to disable |

Properties

| Property | Type | Description | | ----------- | ----------------- | ------------------------------------------------------------------------------------------------------- | | name | string | Friendly name | | state | ConnectionState | Current state: 'disconnected' / 'connecting' / 'connected' / 'disconnecting' / 'reconnecting' | | connected | boolean | true when state is 'connected' | | plcStatus | PlcStatus | Last known PLC mode: 'run' / 'stop' / 'unknown' | | client | snap7.S7Client | Underlying snap7 client (for advanced use) |

Methods

| Method | Returns | Description | | ------------------ | --------------- | ------------------------------------------------------------------------------------ | | connect() | Promise<void> | Establishes the TCP/S7 connection. Rejects on failure and emits 'error'. | | disconnect() | void | Closes the connection intentionally. Does not emit 'disconnected' or reconnect. | | register(opts) | DataBlock | Creates and attaches a DataBlock; starts polling immediately if connected. | | unregister(db) | void | Stops polling and detaches the DataBlock. | | Client.status(v) | PlcStatus | Static helper: converts a raw snap7 status code to 'run' / 'stop' / 'unknown'. |

Note: Calling connect() while the client is in any state other than 'disconnected' (including 'reconnecting') returns Promise.resolve() immediately without starting a new handshake. The active reconnect loop will handle reconnection automatically.

Events

| Event | Args | When fired | | -------------- | --------------------------------------- | ---------------------------------------------------------------------------- | | connected | — | Every successful connection, including after reconnects | | disconnected | — | Unexpected drops only — not fired when disconnect() is called | | reconnecting | { interval: number; attempt: number } | Before each reconnect delay. The first attempt waits reconnectInterval ms. | | status | status: PlcStatus | When PLC operating mode changes | | error | e: Error | Non-fatal errors (failed status poll, initial connect failure, etc.) |


DataBlock

Normally created via client.register(). Can also be instantiated directly for advanced use:

new DataBlock(client: snap7.S7Client, options: DataBlockOptions)

DataBlockOptions

| Option | Type | Default | Description | | -------------- | ----------------- | -------- | ------------------------------------ | | dbNumber | number | required | S7 DB number | | tags | TagDefinition[] | required | Initial tag list (must have ≥ 1 tag) | | pollInterval | number | 1000 | Base polling interval in ms |

Properties

| Property | Type | Description | | ---------- | -------- | ------------------------------------------------- | | dbNumber | number | The DB number | | size | number | Total byte size of the internal buffer | | offset | number | Byte offset of the first (lowest-addressed) tag | | buffer | Buffer | Immutable snapshot of the current internal buffer |

Methods

| Method | Returns | Description | | ------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | subscribe() | void | Starts the polling loop (idempotent) | | unsubscribe() | void | Stops the polling loop | | get(name) | { tag: TagDefinition, value: bool\|num\|str } | Reads the current value from the local buffer. Throws if tag not found. | | set(name, value) | Promise<void> | Writes to the PLC via WriteArea and updates the local buffer. Rejects if tag not found or write fails. | | add(tag) | void | Dynamically adds a tag; grows the buffer if needed | | addMany(tags) | void | Bulk-adds tags; recomputes layout once | | delete(name) | void | Removes a tag; shrinks the buffer if at boundary. No-op if not found. Stops polling if last tag is removed. | | deleteMany(names) | void | Bulk-removes tags; recomputes layout once | | dump() | Record<string, boolean\|number\|string> | Snapshot of all tag values decoded from the local buffer | | DataBlock.tagSize(tag) | number | Returns the byte size of a tag | | DataBlock.computeLayout(tags) | { start: number, size: number } | Computes the minimal byte range covering all tags |

Events

| Event | Args | When fired | | --------- | ---------------------------------------------------- | --------------------------------------------------- | | changed | tag: TagDefinition, value: boolean\|number\|string | Fired when a tag value changes during a poll cycle | | error | e: Error | Non-fatal errors (connection lost, PLC error, etc.) |


Datatype

A frozen collection of S7 data-type descriptors. Each descriptor exposes:

{
  key: string; // e.g. 'Bool', 'Int', 'String'
  size: number; // byte size in the PLC
  wordSize: number; // snap7 WordSize constant for ReadArea / WriteArea
  get(buffer: Buffer, offset: Offset): boolean | number | string;
  set(buffer: Buffer, offset: Offset, value: ..., dimension?: number): void;
}

Available descriptors: Bool, Byte, Word, DWord, Int, DInt, Real, Char, String.


Constants

| Export | Values | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | ConnectionType | Pg (0x01) / Op (0x02) / Basic (0x03) | S7 connection type | | Area | Pe (0x81) / Pa (0x82) / Merker (0x83) / DataBlock (0x84) / Counter (0x1c) / Timer (0x1d) | S7 memory areas | | WordSize | Bit (0x01) / Byte (0x02) / Word (0x04) / DWord (0x06) / Real (0x08) / Counter (0x1c) / Timer (0x1d) | S7 word sizes | | Status | Unknown (0x00) / Stop (0x04) / Run (0x08) | Raw PLC status codes | | Parameter | RemotePort (2) / PingTimeout (3) / SendTimeout (4) / ReceiveTimeout (5) / SourceRef (7) / DestinationRef (8) / SourceTSap (9) / PDURequest (10) | Numeric identifiers for SetParam / GetParam |


Supported S7 Data Types

| Type | S7 Type | Byte Size | JS Type | Notes | | -------- | -------- | --------------- | --------- | ------------------------------------------------------------------------- | | Bool | BOOL | 1 | boolean | Bit-addressed. offset[1] (bit index 0–7) is significant. | | Byte | BYTE | 1 | number | Unsigned 8-bit. Values above 255 are masked. | | Word | WORD | 2 | number | Unsigned 16-bit big-endian. Values above 65 535 are masked. | | DWord | DWORD | 4 | number | Unsigned 32-bit big-endian. Coerced with >>> 0. | | Int | INT | 2 | number | Signed 16-bit big-endian. Range: −32 768 … 32 767. | | DInt | DINT | 4 | number | Signed 32-bit big-endian. Range: −2 147 483 648 … 2 147 483 647. | | Real | REAL | 4 | number | IEEE 754 single-precision float. Rounded to 7 significant digits on read. | | Char | CHAR | 1 | string | Single ASCII character. | | String | STRING | 2 + dimension | string | S7 STRING: [maxLen:1][actualLen:1][chars…]. Default dimension = 254. |


Tag Definition

Every tag shares this base structure:

type TagDefinition = {
  name: string; // Unique identifier within the DataBlock
  type: string; // One of the Datatype keys (e.g. 'Bool', 'Int')
  offset: [byteOffset, bit]; // [byte address, bit index (0–7)]
  initialValue: boolean | number | string;
};

For String tags, an optional dimension (1–254) overrides the default 254-character allocation:

{ name: 'label', type: 'String', offset: [10, 0], dimension: 16, initialValue: '' }

Important: For all non-Bool types, the bit component of offset is ignored by the library and should always be 0 as a convention.

Offset examples

// Bool at DB byte 4, bit 2
{ name: 'alarm', type: 'Bool', offset: [4, 2], initialValue: false }

// Int at DB byte 0 (occupies bytes 0–1)
{ name: 'speed', type: 'Int', offset: [0, 0], initialValue: 0 }

// Real at DB byte 6 (occupies bytes 6–9)
{ name: 'setpoint', type: 'Real', offset: [6, 0], initialValue: 0.0 }

// String at DB byte 10, max 16 chars (occupies bytes 10–27: 2 header + 16 data)
{ name: 'label', type: 'String', offset: [10, 0], dimension: 16, initialValue: '' }

S7 STRING memory layout

byte[offset + 0] → max allocated length (= dimension)
byte[offset + 1] → current actual length
byte[offset + 2 … offset + 1 + dimension] → ASCII character data