npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 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, the client automatically reconnects at reconnectInterval (default 5000ms) and resumes polling.
  • disconnect() is the only way to fully stop — after that, read/write will 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'));   // 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:

| 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 disconnects

Polling 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 needed

API 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: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 (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.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