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.
Maintainers
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 | disconnecting → disconnected |
| connected | Unexpected drop | disconnected → reconnecting |
| 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') returnsPromise.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-
Booltypes, thebitcomponent ofoffsetis ignored by the library and should always be0as 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