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:
Sort — items are sorted by area, DB number, and byte offset so that adjacent reads end up next to each other.
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.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:
- 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
bufferattribute 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_sizeReply 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).