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
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();┌───────────────────────────────────────────────────────────────┐
│ 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() │
└─────────────────────────────────────┘
| 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 |
Runs every connectionCheckInterval ms (default 2000). Calls snap7.Connected() and triggers #onDisconnected() if it returns false.
Runs every statusCheckInterval ms (default 5000). Calls PlcStatus() and emits the status event only when the PLC mode changes. Set statusCheckInterval: 0 to disable.
| 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.
new Client(options?: 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 |
| 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) |
| 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.
| 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.) |
Normally created via client.register(). Can also be instantiated directly for advanced use:
new DataBlock(client: snap7.S7Client, options: 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 |
| 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 |
| 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 |
| 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.) |
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.
| 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 |
| 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. |
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.
// 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: '' }byte[offset + 0] → max allocated length (= dimension)
byte[offset + 1] → current actual length
byte[offset + 2 … offset + 1 + dimension] → ASCII character data