snap7ts
v1.0.0
Published
Pure TypeScript implementation of S7 communication protocol (node-snap7 compatible)
Readme
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.
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 snap7tsQuick 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 forDBX), 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*,startPollingauto-connect when not connected — no need to callconnect()first. - Auto-reconnect: When PLC goes offline, the client automatically reconnects at
reconnectInterval(default 5000ms) and resumes polling. disconnect()is the only way to fully stop — after that,read/writewill throw. Calling them again re-triggers lazy connection.
// 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')); // 42Points 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:
| Read | Write | Returns / 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 disconnectsPolling methods:
| Method | Description |
|--------|-------------|
| 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:
| Callback | Description |
|----------|-------------|
| 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
| S7DataType | PLC Type | Bytes | Address | Description |
|------------|----------|-------|---------|-------------|
| bool | BOOL | 1 bit | DBX | Bit access with bit offset, e.g. DB1.DBX10.1 |
| byte | BYTE / CHAR | 1 | DBB | Unsigned 8-bit integer |
| int | INT | 2 | DBW | Signed 16-bit integer (BigEndian) |
| word | WORD | 2 | DBW | Unsigned 16-bit integer (BigEndian) |
| dint | DINT | 4 | DBD | Signed 32-bit integer (BigEndian) |
| dword | DWORD | 4 | DBD | Unsigned 32-bit integer (BigEndian) |
| lint | LINT | 8 | DBD | Signed 64-bit integer (BigEndian), S7-1500 only |
| ulint | ULINT | 8 | DBD | Unsigned 64-bit integer (BigEndian), S7-1500 only |
| real | REAL | 4 | DBD | IEEE 754 32-bit float (BigEndian) |
| string | STRING | 2+N | DBB | S7 format: maxLen(1) + actualLen(1) + ASCII chars |
| wstring | WSTRING | 4+2N | DBB | S7 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 neededAPI Reference
Connection Management
| Method | Description |
|--------|-------------|
| 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
| Method | Description |
|--------|-------------|
| 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/Write | Convenience wrappers for I/O/Merker areas |
| TMRead/Write, CTRead/Write | Timer/Counter area access |
Directory & Block Operations
| Method | Description |
|--------|-------------|
| 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
| Method | Description |
|--------|-------------|
| 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/PlcStop | PLC run control |
| SetSessionPassword/ClearSessionPassword | Security 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:102Architecture
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 (batch, polling, auto-reconnect)
│ │ ├── types.ts # Shared type definitions
│ │ ├── codec.ts # Address parsing, type sizing, encode/decode
│ │ ├── batch.ts # DB grouping and byte range calculation
│ │ ├── polling.ts # PollingRunner (interval-based change detection)
│ │ ├── client.ts # S7EnhancedClient (unified entry point)
│ │ └── index.ts # Module re-exports
│ ├── 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.tsDevelopment
npm install
npm run build # Compile TypeScript
npm test # Run tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage reportRequirements
- Node.js >= 16
- No native dependencies
License
MIT
