Optimizer

Warning

The read optimizer is experimental and its API may change in future versions. Disable it with client.use_optimizer = False if you encounter issues.

The multi-variable read optimizer merges adjacent or overlapping read requests and packs them into minimal PDU-sized S7 exchanges. This significantly reduces the number of round-trips when reading many scattered variables.

How it works

The optimizer uses a three-stage pipeline inspired by nodeS7:

  1. Sort — items are sorted by area, DB number, and byte offset so that adjacent reads end up next to each other.

  2. Merge — sorted items in the same area/DB with a small gap between them (configurable via multi_read_max_gap) are merged into contiguous read blocks. This avoids issuing many small reads when a single larger read covers them all.

  3. Packetize — merged blocks are packed into PDU-sized packets, respecting both the request and reply size budgets of the negotiated PDU length.

Parallel dispatch

When there are multiple packets to send, the optimizer can fire them back-to-back on the same TCP connection and collect responses by sequence number (pipelining). This avoids paying a full round-trip per packet.

The number of in-flight packets is controlled by max_parallel, which is auto-tuned based on the negotiated PDU size after connecting:

PDU size

max_parallel

>= 960

8

>= 480

4

>= 240

2

< 240

1 (sequential)

You can override it manually:

client.max_parallel = 2   # limit to 2 in-flight packets

Configuration

client.use_optimizer = False          # disable optimizer entirely
client.multi_read_max_gap = 10        # merge reads up to 10 bytes apart (default 5)
client.max_parallel = 1               # disable parallel dispatch (sequential only)

Plan caching

The optimizer caches the merge/packetize plan for repeated calls with the same item layout. If you always read the same set of variables in a loop (a common pattern in PLC polling), the planning overhead is paid only on the first call.

API reference

Multi-variable read optimizer for S7 communication.

Optimizes multiple scattered read requests into minimal PDU-packed S7 exchanges by merging adjacent/overlapping reads and packing them into PDU-sized packets.

Warning

This module is experimental and its API may change in future versions.

class snap7.optimizer.ReadBlock(area: int, db_number: int, start_offset: int, byte_length: int, items: list[ReadItem] = <factory>, buffer: bytearray = <factory>)[source]

A merged contiguous block of bytes to read in one address spec.

area

S7Area value.

Type:

int

db_number

DB number.

Type:

int

start_offset

Start byte offset of the block.

Type:

int

byte_length

Total bytes to read.

Type:

int

items

The ReadItems contained in this block.

Type:

list[snap7.optimizer.ReadItem]

class snap7.optimizer.ReadItem(area: int, db_number: int, byte_offset: int, bit_offset: int, byte_length: int, index: int)[source]

A single read request from the caller.

area

S7Area value (e.g. 0x84 for DB).

Type:

int

db_number

DB number (0 for non-DB areas).

Type:

int

byte_offset

Start byte offset in the area.

Type:

int

bit_offset

Bit offset within the byte (0 for byte-level reads).

Type:

int

byte_length

Number of bytes to read.

Type:

int

index

Original ordering position so results can be returned in order.

Type:

int

class snap7.optimizer.ReadPacket(blocks: list[ReadBlock] = <factory>)[source]

A group of ReadBlocks that fit in a single S7 PDU exchange.

blocks

The blocks in this packet.

Type:

list[snap7.optimizer.ReadBlock]

snap7.optimizer.extract_results(packets: list[ReadPacket], original_count: int) list[bytearray][source]

Map block buffers back to original items using offset math.

Each block must have its buffer attribute set (a bytearray of the block’s data as returned by the PLC) before calling this function. The buffer is stored as a dynamic attribute on the ReadBlock dataclass.

Parameters:
  • packets – Packets with block buffers populated.

  • original_count – Number of original read items.

Returns:

List of bytearrays indexed by original ReadItem.index.

snap7.optimizer.merge_items(sorted_items: list[ReadItem], max_gap: int = 5, max_block_size: int = 462) list[ReadBlock][source]

Merge sorted read items into contiguous blocks.

Adjacent or overlapping items in the same area/db are merged when the gap between them is at most max_gap bytes and the resulting block does not exceed max_block_size bytes.

Parameters:
  • sorted_items – Items pre-sorted by sort_items().

  • max_gap – Maximum byte gap between items to still merge them.

  • max_block_size – Maximum byte length of a single merged block.

Returns:

List of merged ReadBlocks.

snap7.optimizer.packetize(blocks: list[ReadBlock], pdu_size: int) list[ReadPacket][source]

Pack blocks into PDU-sized packets.

Two budgets are enforced per packet:
  • Request budget: 12 (header) + 2 (func+count) + 12*N (address specs) <= pdu_size

  • Reply budget: 12 (header) + 2 (func+count) + sum(4 + ceil_even(length)) <= pdu_size

Oversized blocks are first split at item boundaries, then blocks are greedily packed into packets.

Parameters:
  • blocks – Merged read blocks.

  • pdu_size – Negotiated PDU size in bytes.

Returns:

List of ReadPackets.

snap7.optimizer.sort_items(items: list[ReadItem]) list[ReadItem][source]

Sort read items for optimal merging.

Items are sorted by (area, db_number, byte_offset, bit_offset, -byte_length). Sorting by descending byte_length ensures that when two items start at the same offset, the larger one comes first, which simplifies overlap handling.

Parameters:

items – List of read items to sort.

Returns:

New sorted list (original is not modified).