S7-1200 Q/I/M symbolic reads: findings from real hardware testing (FW V4.5)
Hardware / software setup
- PLC: Siemens S7-1200 CPU 1214C DC/DC/DC, firmware V4.5
- Module: DI8/DQ6 (digital inputs 0–7, digital outputs Q0.0–Q0.5)
- Config: no password, no Put/Get, port 102
- Library: python-snap7 3.0.0 (
s7 S7CommPlus package)
- Reference capture: Wireshark capture of EXOR HMI communicating via AGLink
(confirms wire-level S7CommPlus V1 protocol details)
We tested read_symbolic / write_symbolic against a live PLC and compared
results with captured EXOR HMI traffic. Below are our findings. Some bugs were
already fixed locally; one unfixed bug remains. All findings are from empirical
testing, not reverse-engineering documentation.
Bug 1 — _build_symbolic_write_payload uses wrong sub_area for I/Q/M writes
Status: still present in 3.0.0 — needs a fix
_build_symbolic_read_payload correctly uses sub_area=3736 (0xE98) for
non-DB areas (I/Q/M). But the write counterpart uses CONTROLLER_AREA_VALUE_ACTUAL
(2551), which causes the PLC to reject the write with error 0x4D0013.
# _build_symbolic_write_payload — CURRENT (broken for I/Q/M)
if access_area >= 0x8A0E0000:
access_sub_area = Ids.DB_VALUE_ACTUAL
else:
access_sub_area = Ids.CONTROLLER_AREA_VALUE_ACTUAL # wrong: 2551
# SHOULD BE (matching read side)
if access_area >= 0x8A0E0000:
access_sub_area = Ids.DB_VALUE_ACTUAL
else:
access_sub_area = 3736 # 0xE98 — confirmed from EXOR AGLink capture
The value 3736 was confirmed by byte-comparing captured EXOR HMI
GetMultiVariables / SetMultiVariables frames with the library's output.
Bug 2 — _parse_read_response misreads single-byte scalar as PLC error
Status: fixed locally — sharing for upstream
For a single BOOL or USINT variable, the PLC returns an 8-byte response:
[value] [0x00] [0x00] [0x04] [0x00] [0x00] [0x00] [0x00]
The parser reads the first VLQ as the return_value. When value=1
(BOOL TRUE), the VLQ decodes as 1, which is non-zero → the parser
logs "PLC returned error: 1" and returns []. Every BOOL TRUE read
raises RuntimeError("Symbolic read failed").
Fix (already applied locally):
def _parse_read_response(response: bytes) -> list[Optional[bytes]]:
# Special-case: single 1-byte scalar + fixed 6-byte trailer
suffix = bytes.fromhex("000400000000")
if response.endswith(suffix):
body = response[:-len(suffix)]
if len(body) == 2 and body[1] == 0x00:
return [bytes(body[:1])]
# ... rest of parser unchanged
Bug 3 — SequenceNumber field missing in GetMultiVariables payloads
Status: fixed locally — sharing for upstream
All GetMultiVariables / SetMultiVariables payloads require a VLQ-encoded
SequenceNumber field immediately after the ObjectQualifier. Without it
the PLC rejects the request with error 11862009.
Fix (already applied locally — add after encode_object_qualifier() in
all 6 payload-builder functions):
payload += encode_object_qualifier()
payload += encode_uint32_vlq(1) # SequenceNumber — required, PLC rejects without it
payload += struct.pack(">I", 0)
Finding 1 — Physical PAQ LIDs return error 5046291 when value = 0x00
When reading a physical Q-byte LID (e.g. QB0 via LID=20) and all outputs are
OFF, the PLC returns error code 5046291 (0x4D3013) instead of 0x00.
_parse_read_response returns [] and read_symbolic raises RuntimeError.
Implication for callers: catch the exception and treat it as 0x00:
try:
result = client.read_symbolic(Q_RID, [QB0_LID])
value = result[0] if result else 0
except Exception:
value = 0 # ERR from PLC = byte is 0x00 (all outputs OFF)
This behaviour was confirmed for both physical PAQ bytes (QB0, QB100) and
physical bit LIDs. Symbolic BOOL LIDs return 0x00 normally (no error).
Finding 2 — PLC sends RST after first GetMultiVariables per TCP connection (I/Q/M only)
After the first successful GetMultiVariables read of any I/Q/M LID, the PLC
immediately sends a TCP RST. A second read on the same connection fails.
DB reads are unaffected: multiple reads per connection work fine.
Workaround: open a new TCP connection for every I/Q/M read call.
This is safe but slow (~0.3–0.5 s per read due to connection overhead).
The root cause is likely that the PLC expects a GetVarSubStreamed subscribe
for live HMI monitoring, not repeated one-shot GetMultiVariables. We have not
yet implemented subscribe.
Finding 3 — Symbolic tag LIDs can be stale vs. physical PAQ
When an HMI writes a symbolic BOOL tag (e.g. Tag_3 = %Q0.0) via
SetMultiVariables, the S7CommPlus object model caches the written value.
A subsequent forced write to the physical address %Q0.0 via TIA Portal
Watch table updates the PAQ byte (readable via the physical byte LID), but
does not update the symbolic BOOL LID. The two diverge.
Example (observed):
- Physical Q0.0 LED: OFF
- LID=20 (QB0 physical byte):
0x04 (Q0.2 ON, Q0.0 correctly OFF)
- LID=17 (Tag_3 symbolic BOOL):
0x01 (stale TRUE from last EXOR write)
Implication: for reliable physical output state, read the physical byte
LID (QB0, QB100, …) rather than individual symbolic BOOL LIDs.
Finding 4 — Empirical LID map for S7-1200 DI8/DQ6 (Q area, RID=81, sub_area=3736)
Discovered by scanning LIDs 1–30 with specific Q outputs toggled ON/OFF.
| LID |
Tag in TIA |
Type |
Notes |
| 9 |
Tag_3 (Q0.0) alias |
BOOL symbolic |
stale from EXOR write |
| 17 |
Tag_3 (Q0.0) |
BOOL symbolic |
stale from EXOR write |
| 18 |
unknown |
BOOL symbolic |
always FALSE in our tests |
| 20 |
QB0 |
BYTE physical (PAQ) |
live; ERR when 0x00 |
| 21 |
100_output (QB100) |
BYTE tag |
live; ERR when 0x00 |
| 22 |
Tag_24 (Q100.4) |
BOOL symbolic |
live (never EXOR-written) |
| 25 |
Q0.4 |
BIT physical |
live |
LIDs were identified by setting one or two specific outputs ON and finding
which LID changed value. Multi-byte tags (QB0, QB100) were disambiguated by
setting two bits simultaneously (e.g. Q0.0+Q0.2 → 0x05, not power-of-2) to
distinguish them from single-BOOL LIDs.
Note: LID=21 was initially misidentified as a static module attribute because
with only Q100.4 ON the byte value is 0x10, which matches a common static
attribute pattern. Setting two bits (Q100.0+Q100.4 → 0x11) revealed it is
live.
Example scripts
Three working scripts are attached to help reproduce our results:
read_q_byte.py — reads any Q byte by index using the LID cache
find_q_lid.py — scans Q area LIDs and reports TRUE/FALSE/ERR per LID
monitor_q.py — live monitor of QB0, refreshes every 2 seconds
All scripts use a workaround for the RST issue (one fresh TCP connection per
read) and treat ERR responses as 0x00 for physical LIDs.
We hope these findings are useful for improving S7CommPlus support.
Happy to test any proposed fixes against our hardware.
find_q_lid.py
monitor_q.py
read_q_byte.py
S7-1200 Q/I/M symbolic reads: findings from real hardware testing (FW V4.5)
Hardware / software setup
s7S7CommPlus package)(confirms wire-level S7CommPlus V1 protocol details)
We tested
read_symbolic/write_symbolicagainst a live PLC and comparedresults with captured EXOR HMI traffic. Below are our findings. Some bugs were
already fixed locally; one unfixed bug remains. All findings are from empirical
testing, not reverse-engineering documentation.
Bug 1 —
_build_symbolic_write_payloaduses wrongsub_areafor I/Q/M writesStatus: still present in 3.0.0 — needs a fix
_build_symbolic_read_payloadcorrectly usessub_area=3736(0xE98) fornon-DB areas (I/Q/M). But the write counterpart uses
CONTROLLER_AREA_VALUE_ACTUAL(2551), which causes the PLC to reject the write with error
0x4D0013.The value
3736was confirmed by byte-comparing captured EXOR HMIGetMultiVariables / SetMultiVariables frames with the library's output.
Bug 2 —
_parse_read_responsemisreads single-byte scalar as PLC errorStatus: fixed locally — sharing for upstream
For a single BOOL or USINT variable, the PLC returns an 8-byte response:
The parser reads the first VLQ as the
return_value. Whenvalue=1(BOOL TRUE), the VLQ decodes as
1, which is non-zero → the parserlogs
"PLC returned error: 1"and returns[]. Every BOOL TRUE readraises
RuntimeError("Symbolic read failed").Fix (already applied locally):
Bug 3 —
SequenceNumberfield missing in GetMultiVariables payloadsStatus: fixed locally — sharing for upstream
All GetMultiVariables / SetMultiVariables payloads require a VLQ-encoded
SequenceNumberfield immediately after theObjectQualifier. Without itthe PLC rejects the request with error
11862009.Fix (already applied locally — add after
encode_object_qualifier()inall 6 payload-builder functions):
Finding 1 — Physical PAQ LIDs return error 5046291 when value = 0x00
When reading a physical Q-byte LID (e.g. QB0 via LID=20) and all outputs are
OFF, the PLC returns error code
5046291(0x4D3013) instead of0x00._parse_read_responsereturns[]andread_symbolicraisesRuntimeError.Implication for callers: catch the exception and treat it as
0x00:This behaviour was confirmed for both physical PAQ bytes (QB0, QB100) and
physical bit LIDs. Symbolic BOOL LIDs return
0x00normally (no error).Finding 2 — PLC sends RST after first GetMultiVariables per TCP connection (I/Q/M only)
After the first successful GetMultiVariables read of any I/Q/M LID, the PLC
immediately sends a TCP RST. A second read on the same connection fails.
DB reads are unaffected: multiple reads per connection work fine.
Workaround: open a new TCP connection for every I/Q/M read call.
This is safe but slow (~0.3–0.5 s per read due to connection overhead).
The root cause is likely that the PLC expects a
GetVarSubStreamedsubscribefor live HMI monitoring, not repeated one-shot GetMultiVariables. We have not
yet implemented subscribe.
Finding 3 — Symbolic tag LIDs can be stale vs. physical PAQ
When an HMI writes a symbolic BOOL tag (e.g.
Tag_3 = %Q0.0) viaSetMultiVariables, the S7CommPlus object model caches the written value.A subsequent forced write to the physical address
%Q0.0via TIA PortalWatch table updates the PAQ byte (readable via the physical byte LID), but
does not update the symbolic BOOL LID. The two diverge.
Example (observed):
0x04(Q0.2 ON, Q0.0 correctly OFF)0x01(stale TRUE from last EXOR write)Implication: for reliable physical output state, read the physical byte
LID (QB0, QB100, …) rather than individual symbolic BOOL LIDs.
Finding 4 — Empirical LID map for S7-1200 DI8/DQ6 (Q area, RID=81, sub_area=3736)
Discovered by scanning LIDs 1–30 with specific Q outputs toggled ON/OFF.
LIDs were identified by setting one or two specific outputs ON and finding
which LID changed value. Multi-byte tags (QB0, QB100) were disambiguated by
setting two bits simultaneously (e.g. Q0.0+Q0.2 → 0x05, not power-of-2) to
distinguish them from single-BOOL LIDs.
Note: LID=21 was initially misidentified as a static module attribute because
with only Q100.4 ON the byte value is
0x10, which matches a common staticattribute pattern. Setting two bits (Q100.0+Q100.4 →
0x11) revealed it islive.
Example scripts
Three working scripts are attached to help reproduce our results:
read_q_byte.py— reads any Q byte by index using the LID cachefind_q_lid.py— scans Q area LIDs and reports TRUE/FALSE/ERR per LIDmonitor_q.py— live monitor of QB0, refreshes every 2 secondsAll scripts use a workaround for the RST issue (one fresh TCP connection per
read) and treat ERR responses as 0x00 for physical LIDs.
We hope these findings are useful for improving S7CommPlus support.
Happy to test any proposed fixes against our hardware.
find_q_lid.py
monitor_q.py
read_q_byte.py