Skip to content

tommy-may/node-s7-client

Repository files navigation

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

About

High-level TypeScript client for Siemens S7 PLCs built on node-snap7. Features auto-reconnect with exponential backoff, adaptive polling, dynamic tag management and a strongly-typed EventEmitter API.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors