logo
0
0
WeChat Login

snap7ts

A pure TypeScript implementation of the Siemens S7 communication protocol, fully compatible with the node-snap7 API.

Zero native dependencies. No compilation toolchain required. Works on any platform Node.js runs on.

Why snap7ts?

node-snap7 wraps the snap7 C++ library as a native addon, which requires a C++ compiler toolchain and platform-specific binaries. This causes friction in CI/CD pipelines, cross-platform deployments, and environments where native module compilation is restricted.

snap7ts reimplements the entire S7 protocol stack in pure TypeScript — from TPKT framing to S7 application-layer PDUs — while preserving the exact same API surface as node-snap7.

Zero runtime dependencies. Small footprint. The entire library is just the code you see — no hidden native modules, no platform-specific binaries, and no dependency tree to audit or break.

For production use, S7EnhancedClient offers lazy connection, automatic reconnection, batch read/write with DB merging, and polling with change detection — all built on top of the core S7Client. For testing, S7EnhancedServer provides declarative area management, type-aware read/write, data simulation, change watch, and snapshot/restore — all built on top of the core S7Server.

Protocol Stack

┌─────────────────────────┐ │ S7 Application Layer │ Read/Write, Control, System Info PDUs ├─────────────────────────┤ │ ISO COTP (Class 0) │ Connection Request/Confirm, TSAP negotiation ├─────────────────────────┤ │ ISO TPKT (RFC 1006) │ 4-byte header: version(0x03), reserved, length ├─────────────────────────┤ │ TCP │ Port 102 └─────────────────────────┘

Installation

npm install snap7ts

Quick Start

import { S7Client } from 'snap7ts'; const client = new S7Client(); // Callback style (node-snap7 compatible) client.ConnectTo('192.168.1.10', 0, 1, (err) => { if (err) { console.error('Connection failed:', client.ErrorText(err)); return; } client.DBRead(1, 0, 100, (err, data) => { if (err) { console.error('Read failed:', client.ErrorText(err)); return; } console.log('Data:', data.toString('hex')); client.Disconnect(); }); });

Promise Style

All async methods return a Promise when no callback is provided:

import { S7Client } from 'snap7ts'; const client = new S7Client(); client.SetConnectionType(3); // CONNTYPE_BASIC await client.ConnectTo('192.168.1.10', 0, 1); const data = await client.DBRead(1, 0, 100); console.log('Data:', data.toString('hex')); client.Disconnect();

Real-World Usage

Here are common patterns for working with S7 data in production:

Read a DB block and decode S7 data types

S7 addresses follow the format DB<n>.<type><m>.<b>:

  • DB<n> — Data Block number (e.g. DB1 = DB block 1)
  • <type> — Access size: DBX (bit), DBB (byte), DBW (word/2 bytes), DBD (double word/4 bytes)
  • <m> — Byte offset within the DB block
  • .<b> — Bit offset (only for DBX), range 0–7

Examples: DB1.DBX10.1 = DB1, byte 10, bit 1; DB1.DBB2 = DB1, byte 2; DB1.DBW20 = DB1, word at byte 20.

Read a block of bytes with DBRead, then decode by offset:

const client = new S7Client(); await client.ConnectTo('192.168.1.10', 0, 1); // To read DB1.DBB2 (string) and DB1.DBX0.0, DB1.DBX10.1 (bools), // read 20 bytes starting from offset 0 const buf = await client.DBRead(1, 0, 20); // DB1.DBB2 — S7 string: maxLen(1) + actualLen(1) + chars const actualLen = buf.readUInt8(3); // offset 2 + 1 = byte 3 const str = buf.toString('utf-8', 4, 4 + actualLen); // DB1.DBX0.0 — bool at byte 0, bit 0 const bool0_0 = (buf.readUInt8(0) & 0x01) !== 0; // DB1.DBX10.1 — bool at byte 10, bit 1 const bool10_1 = (buf.readUInt8(10) & 0x02) !== 0; console.log({ str, bool0_0, bool10_1 }); client.Disconnect();

S7EnhancedClient: batch read/write with auto-merge

S7EnhancedClient is a high-level client with lazy connection, auto-reconnect, batch operations, and polling. Constructor takes PLC connection parameters — no need to manually create an S7Client:

import { S7EnhancedClient } from 'snap7ts'; const client = new S7EnhancedClient({ ip: '192.168.1.10', port: 102, rack: 0, slot: 1, });

Lazy Connection & Auto-Reconnect

  • Lazy connection: read*, write*, startPolling auto-connect when not connected — no need to call connect() first.
  • Auto-reconnect: When PLC goes offline or is unavailable on first connect, the client automatically reconnects at reconnectInterval (default 5000ms) and resumes polling.
  • disconnect() is the only way to fully stop — after that, any read/write will throw Client is explicitly disconnected, and auto-reconnect is cancelled. You must explicitly call connect() or startPolling() again to re-establish the connection. Both connect() and startPolling() clear the disconnected flag, allowing auto-reconnect to resume.

Connection state machine:

connect() / startPolling() ┌──────────┐ ────────────────────────────► ┌───────────┐ │ Initial │ │ Connected │ └──────────┘ ◄──────────────────────────── └───────────┘ disconnect() │ ▲ PLC │ │ auto-reconnect offline │ │ (reconnectInterval) ▼ │ ┌──────────┐ │ Reconnect │ └──────────┘ │ disconnect() ▼ ┌────────────┐ │ Disconnected│ ← read/write throw, no auto-reconnect └────────────┘ │ connect() / startPolling() │ (clears _disconnected flag) ▼ back to Initial / Connected
Stateread/writeAuto-reconnectTransition out
Initialtriggers lazy connectyes (on failure)→ Connected or → Reconnect
Connectedworks normallyyes (on PLC offline)→ Reconnect
Reconnectthrows, waits for reconnectyes (retrying)→ Connected
Disconnectedthrows Client is explicitly disconnectednoconnect() or startPolling() clears _disconnected flag
// No connect() needed — readBool auto-connects const flag = await client.readBool('DB1.DBX10.1'); // PLC goes offline here... auto-reconnect kicks in // Next read/write works automatically after reconnect const temp = await client.readReal('DB1.DBD30');

Batch Read/Write

Automatically groups points by DB number — one DBRead per DB:

import { ReadPoint, WritePoint } from 'snap7ts'; // Batch read — points can span different DB blocks const points: ReadPoint[] = [ { address: 'DB1.DBB2', dataType: 'string' }, { address: 'DB1.DBX10.0', dataType: 'bool' }, { address: 'DB1.DBX10.1', dataType: 'bool' }, { address: 'DB1.DBB20', dataType: 'int' }, ]; const values = await client.batchRead(points); console.log(values.get('DB1.DBB2')); // "OP9999" console.log(values.get('DB1.DBX10.0')); // true / false console.log(values.get('DB1.DBB20')); // 42

Points from different DB blocks are merged — only 2 reads total (DB1 + DB2), not 5:

const points: ReadPoint[] = [ { address: 'DB1.DBX0.0', dataType: 'bool' }, { address: 'DB1.DBX0.1', dataType: 'bool' }, { address: 'DB2.DBB10', dataType: 'int' }, { address: 'DB2.DBX20.3', dataType: 'bool' }, { address: 'DB2.DBB30', dataType: 'real' }, ];

Batch write reads current data first, encodes values, then writes back (safe for bit operations):

const writes: WritePoint[] = [ { address: 'DB1.DBX10.0', dataType: 'bool', value: true }, { address: 'DB1.DBB20', dataType: 'int', value: 100 }, { address: 'DB1.DBB2', dataType: 'string', value: 'OP0001', stringLen: 10 }, ]; await client.batchWrite(writes);

Single-value Read/Write

const flag = await client.readBool('DB1.DBX10.1'); const count = await client.readInt('DB1.DBW20'); const temp = await client.readReal('DB1.DBD30'); const name = await client.readString('DB1.DBB50', 20); // stringLen=20 await client.writeBool('DB1.DBX10.1', true); await client.writeInt('DB1.DBW20', 100); await client.writeReal('DB1.DBD30', 36.5); await client.writeString('DB1.DBB50', 'hello', 20);

Available methods:

ReadWriteReturns / Value type
readBool(addr)writeBool(addr, val)boolean
readByte(addr)writeByte(addr, val)number (uint8)
readInt(addr)writeInt(addr, val)number (int16)
readWord(addr)writeWord(addr, val)number (uint16)
readDInt(addr)writeDInt(addr, val)number (int32)
readDWord(addr)writeDWord(addr, val)number (uint32)
readLInt(addr)writeLInt(addr, val)bigint (int64)
readULInt(addr)writeULInt(addr, val)bigint (uint64)
readReal(addr)writeReal(addr, val)number (float32)
readString(addr, len?)writeString(addr, val, len?)string
readWString(addr, len?)writeWString(addr, val, len?)string

dataType is case-insensitive ('BOOL', 'Bool', 'bool' all work). Invalid types throw an error.

Polling with Change Detection

S7EnhancedClient can poll points at a fixed interval and fire callbacks when values change:

const client = new S7EnhancedClient({ ip: '192.168.1.10', port: 102, rack: 0, slot: 1, interval: 500, // poll every 500ms reconnectInterval: 3000, // reconnect every 3s after disconnect }); client.setPollPoints([ { address: 'DB1.DBB2', dataType: 'string', name: 'StationCode' }, { address: 'DB1.DBX10.1', dataType: 'bool' }, ]); client.onChange(changes => { for (const c of changes) { const label = c.name || c.address; console.log(`[${new Date().toISOString()}] ${label}: ${c.oldValue}${c.newValue}`); } }); client.onConnect(() => console.log('PLC connected')); client.onDisconnect(() => console.log('PLC disconnected')); client.onError(err => console.error('Error:', err.message || err)); // startPolling auto-connects — no need to call connect() first await client.startPolling(); // ... later client.stopPolling(); // stops polling, connection stays alive client.disconnect(); // fully disconnects

Polling methods:

MethodDescription
setPollPoints(points)Set points to poll. Each point can have an optional name for easy identification in change callbacks.
setPollInterval(ms)Set poll interval (default 1000ms, or use interval in constructor)
startPolling()Start polling. Auto-connects if not connected. Resumes after reconnect.
stopPolling()Stop polling. Connection remains active — read/write still work.

Event callbacks:

CallbackDescription
onChange(handler)Fired with ChangeRecord[] when any polled value changes. First poll always fires (old value is undefined).
onError(handler)Fired on any error (poll failure, connection lost).
onConnect(handler)Fired when connection is established (including auto-reconnect).
onDisconnect(handler)Fired when connection is lost.

ChangeRecord fields: address, dataType, oldValue, newValue, name? (carried from PollPoint).

Complete Demo: Data Collector with Heartbeat

const { S7EnhancedClient } = require('snap7ts'); const client = new S7EnhancedClient({ ip: '127.0.0.1', port: 102, rack: 0, slot: 1, interval: 500, }); const POLL_POINTS = [ { address: 'DB9002.DBB2', dataType: 'string', name: 'StationCode' }, { address: 'DB9002.DBX10.1', dataType: 'bool' }, ]; const HEARTBEAT_ADDR = 'DB9002.DBX0.0'; client.onChange(changes => { for (const c of changes) { const label = c.name || c.address; console.log(`[${new Date().toISOString()}] ${label}: ${c.oldValue}${c.newValue}`); } }); client.onConnect(() => console.log('PLC connected')); client.onDisconnect(() => console.log('PLC disconnected')); client.onError(err => console.error('Error:', err.message || err)); client.setPollPoints(POLL_POINTS); client.startPolling().catch(err => console.error('Error:', err.message || err)); let heartbeat = false; setInterval(() => { heartbeat = !heartbeat; client.writeBool(HEARTBEAT_ADDR, heartbeat) .catch(err => console.error('Write heartbeat failed:', err.message || err)); }, 1000);

Data Type Reference

S7DataTypePLC TypeBytesAddressDescription
boolBOOL1 bitDBXBit access with bit offset, e.g. DB1.DBX10.1
byteBYTE / CHAR1DBBUnsigned 8-bit integer
intINT2DBWSigned 16-bit integer (BigEndian)
wordWORD2DBWUnsigned 16-bit integer (BigEndian)
dintDINT4DBDSigned 32-bit integer (BigEndian)
dwordDWORD4DBDUnsigned 32-bit integer (BigEndian)
lintLINT8DBDSigned 64-bit integer (BigEndian), S7-1500 only
ulintULINT8DBDUnsigned 64-bit integer (BigEndian), S7-1500 only
realREAL4DBDIEEE 754 32-bit float (BigEndian)
stringSTRING2+NDBBS7 format: maxLen(1) + actualLen(1) + ASCII chars
wstringWSTRING4+2NDBBS7 format: maxLen(2) + actualLen(2) + UTF-16LE chars

For string, stringLen defaults to 254. For wstring, defaults to 512. Override if your PLC uses different lengths.

Read/write a single bit

Use ReadArea/WriteArea with S7WLBit:

// Read one byte containing the target bit const data = await client.ReadArea(0x84, dbNumber, byteOffset, 1, 0x01); const bitSet = (data.readUInt8(0) & (1 << bitOffset)) !== 0; // Write: read-modify-write the byte const current = data.readUInt8(0); const buf = Buffer.alloc(1); buf.writeUInt8(bitSet ? (1 << bitOffset) | current : ~(1 << bitOffset) & current); await client.WriteArea(0x84, dbNumber, byteOffset, 1, 0x01, buf);

Connection setup with parameters

const client = new S7Client(); client.SetConnectionType(3); // CONNTYPE_BASIC client.SetParam(2, 102); // RemotePort client.SetParam(5, 5000); // RecvTimeout client.SetParam(4, 5000); // SendTimeout await client.ConnectTo('192.168.1.10', 0, 1);

Error handling pattern

try { const data = await client.DBRead(1, 0, 100); // process data... } catch (err) { const code = typeof err === 'number' ? err : 0; console.error('PLC error:', client.ErrorText(code)); // Reconnect on connection errors if (code === 0x00030000 || code === 0x00020000) { client.Disconnect(); await client.ConnectTo(ip, rack, slot); } }

Drop-in Replacement for node-snap7

// Before: const snap7 = require('node-snap7'); // After: const snap7 = require('snap7ts').default; const client = new snap7.S7Client(); // Same API, no code changes needed

API Reference

Connection Management

MethodDescription
ConnectTo(ip, rack, slot, callback?)Connect to a Siemens PLC
Connect(callback?)Connect using preset parameters
Disconnect()Disconnect from PLC
SetConnectionParams(ip, localTSAP, remoteTSAP)Set connection parameters
SetConnectionType(type)Set connection type (PG/OP/Basic)
GetParam(paramNumber)Get internal parameter value
SetParam(paramNumber, value)Set internal parameter value

Data I/O

MethodDescription
ReadArea(area, db, start, size, wordLen, callback?)Read from any PLC area
WriteArea(area, db, start, size, wordLen, buffer, callback?)Write to any PLC area
ReadMultiVars(items, callback?)Batch read up to 20 variables
WriteMultiVars(items, callback?)Batch write up to 20 variables
DBRead(db, start, size, callback?)Read DB area
DBWrite(db, start, size, buffer, callback?)Write DB area
ABRead/Write, EBRead/Write, MBRead/WriteConvenience wrappers for I/O/Merker areas
TMRead/Write, CTRead/WriteTimer/Counter area access

Directory & Block Operations

MethodDescription
ListBlocks(callback?)List AG block counts
ListBlocksOfType(type, callback?)List blocks of a specific type
GetAgBlockInfo(type, num, callback?)Get AG block info
Upload/FullUpload(type, num, callback?)Upload block from PLC
Download(num, buffer, callback?)Download block to PLC
Delete(type, num, callback?)Delete a block
DBGet(db, callback?)Get entire DB block
DBFill(db, fillChar, callback?)Fill DB with a byte

System Info & Control

MethodDescription
ReadSZL(id, index, callback?)Read System Status List
GetOrderCode(callback?)Get PLC order code
GetCpuInfo(callback?)Get CPU module info
GetCpInfo(callback?)Get CP module info
GetPlcDateTime(callback?)Read PLC date/time
SetPlcDateTime(dateTime, callback?)Set PLC date/time
PlcHotStart/PlcColdStart/PlcStopPLC run control
SetSessionPassword/ClearSessionPasswordSecurity management
GetProtection(callback?)Read protection level
PlcStatus(callback?)Get PLC run status
ErrorText(errNum)Error code to text

S7Server

snap7ts also includes a fully functional S7Server for simulating PLCs in testing:

import { S7Server } from 'snap7ts'; const server = new S7Server(); const db = Buffer.alloc(1024); server.RegisterArea(server.srvAreaDB, 1, db); server.StartTo('127.0.0.1'); // Now S7Client can connect to 127.0.0.1:102

Architecture

Concurrency Safety

The S7 protocol is inherently request-response over a single TCP connection. snap7ts enforces this with an internal mutex (withLock) that serializes all send+recv cycles. This matches node-snap7's C++ synchronous blocking behavior and prevents the response-mixing race condition that would otherwise occur with JavaScript's async I/O model.

Automatic Chunking

Read/Write operations that exceed the negotiated PDU size are automatically split into chunks:

  • Read: max bytes per chunk = PDU - 18
  • Write: max bytes per chunk = PDU - 34

Each chunk is a separate request-response cycle, and results are assembled into a contiguous buffer.

Error Detection

S7 error responses (MsgType 0x02 with non-zero error class/code) are explicitly detected and surfaced as error codes, rather than being silently treated as successful empty responses.

Project Structure

snap7ts/ ├── src/ │ ├── index.ts # Public API exports │ ├── client.ts # S7Client class │ ├── server.ts # S7Server class │ ├── enhanced/ # S7EnhancedClient + S7EnhancedServer │ │ ├── index.ts # Module re-exports │ │ ├── shared/ # Shared types and codec │ │ │ ├── types.ts # Shared type definitions │ │ │ └── codec.ts # Address parsing, type sizing, encode/decode │ │ ├── client/ # Enhanced client │ │ │ ├── client.ts # S7EnhancedClient (lazy connect, auto-reconnect, polling) │ │ │ ├── polling.ts # PollingRunner (interval-based change detection) │ │ │ └── batch.ts # DB grouping and byte range calculation │ │ └── server/ # Enhanced server │ │ ├── server.ts # S7EnhancedServer (area manager, simulator, watch) │ │ ├── area-manager.ts # AreaManager (type-aware read/write, snapshot) │ │ └── simulator.ts # Simulator (sine/random/toggle/increment/sequence) │ ├── protocol/ │ │ ├── constants.ts # Protocol constants (Area, WordLen, Error codes, etc.) │ │ ├── tpkt.ts # TPKT frame encoding/decoding │ │ ├── cotp.ts # COTP connection management (CR/CC/DR/DT) │ │ └── s7-protocol.ts # S7 PDU building/parsing │ ├── transport/ │ │ └── tcp-transport.ts # TCP transport (net.Socket + frame buffering) │ └── utils/ │ ├── error-text.ts # Error code → text mapping │ └── buffer-utils.ts # Buffer read/write helpers ├── test/ # 192 unit & integration tests ├── doc/ # Documentation ├── dist/ # Compiled output ├── package.json ├── tsconfig.json └── vitest.config.ts

Development

npm install npm run build # Compile TypeScript npm test # Run tests npm run test:watch # Watch mode npm run test:coverage # Coverage report

Requirements

  • Node.js >= 16
  • No native dependencies

License

MIT