feat: tag_browser — parse I/Q/M tags and DB/FB/OB interfaces from decompressed EXPLORE XML#780
Conversation
…RE XML Follow-up to gijzelaerr#776: wires find_and_decompress() into a small non-invasive tag layer. Parses the clean <IdentContainer> XML into Tag records with TIA addresses (%I0.0, %Q0.1, %MB100, %MW102, %MD104). Includes a regression test built from a real S7-1200 G2 (FW V4.1) M-area EXPLORE blob. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend tag_browser beyond I/Q/M symbols to data-block / FB / OB interfaces. - parse_block_interface(): parse the <BlockInterface> XML (DebugInfo_IntfDesc dict, Adler-32 0x66052b13) into Member records (name, type, LID, offset). A member's data type is a RID reference of the form 0x0200_00XX where XX is a Siemens SoftDataType id (mapping taken from thomas-v2/S7CommPlusDriver Core/Softdatatype.cs); complex types outside that namespace are kept as hex. - block_interface_from_explore(): a block EXPLORE response bundles several preset-dict streams (IdentES, interface, comments, ...), so target the interface dictionary explicitly via _find_preset_stream() instead of taking the first zlib stream. - datablocks_from_explore(): parse the DB listing from a 0x8A11FFFF wildcard EXPLORE. PlcContentInfo is a standard zlib stream (78 da) embedded in a larger BLOB, so it is raw-inflated to tolerate the trailing bytes. - I/Q/M path now targets the IntfDescTag dict (0xce9b821b) explicitly too. Validated end-to-end against a live S7-1200 G2 (FW V4.1): an optimized DB (Data_block_1, 11 members) resolves with correct names and Bool/Int/Real types. Tests use a real DB EXPLORE blob fixture plus a synthetic standard-zlib listing; 7/7 pass, ruff clean.
Generalize parse_block_interface() from the flat DB case to full FB/OB interfaces, validated against a live S7-1200 G2 FB (FB_Macchina): - Skip section-header and list-wrapper <Member> elements (those without a RID); previously they were emitted as bogus members (Input/Output/...). - Tag each member with its section (Input/Output/InOut/Static/Temp/Constant), resolved from the enclosing <Part Kind="XxxSection">. DB members stay "". - Resolve complex types from the ready-made ``Type`` attribute the PLC ships (e.g. ``Array[0..5] of IEC_TIMER``, ``Array[0..2] of Bool``); scalar members still resolve from the RID (arrays use 0x0201_00XX, hence the explicit Type). The raw RID stays available on Member.rid. Adds a Member.section field and an FB-interface test covering section tagging, header skipping and array/Type passthrough. 8/8 tests pass.
Validated against a live S7-1200 G2 DB that uses a user UDT (Data_block_1 with an "elettrovalvola" member). - A UDT-typed member carries a readable ``Type`` attribute (e.g. ``"elettrovalvola"``, TIA-quoted) and a RID in the UDT object namespace (0x91......), so it already resolves via the existing Type-attribute path. - When a block uses a UDT, its interface embeds the UDT's own definition under a nested ``<Part Kind="DataTypeSource">``. Those members describe the type, not the block, so keep a member only when its nearest enclosing ``<Part>`` is the block's primary part (flat DB/UDT) or a ``*Section`` part (FB/OB); members of a nested ``*Source`` part are skipped. Replaces the section-only ancestor walk with a nearest-Part check that drives both the keep decision and the section tag. Adds a DB-with-UDT test. 9/9 pass.
When a member is of a UDT (or other structured type), its definition is embedded under the primary part's <SubParts> and selected by the member's SubPartIndex. Expand it recursively into a new Member.fields list, validated against a live S7-1200 G2 DB whose member is of a user UDT (elettrovalvola -> 6 inner fields fc_apertura/stato_ev/...). - Member gains a `fields: list[Member]` (default empty). - parse_block_interface() now walks the primary part structurally via _parse_source_part()/_build_member(): flat DB/UDT members from <Payload><Root>, FB/OB members from <Part Kind="*Section">, and each member with a SubPartIndex into an inlined *Source subpart is expanded. Library types (IEC_TIMER) are referenced, not inlined, so they expand to no fields. Adds a real DB-with-UDT blob fixture and an inline expansion test. 10/10 pass.
gijzelaerr
left a comment
There was a problem hiding this comment.
Nice work — clean module, real hardware fixtures, well-scoped. Three things I noticed:
1. Off-by-one in _find_preset_stream (tag_browser.py:262)
end = len(data) - 6 should be len(data) - 5. The zlib header is 6 bytes (CMF + FLG + 4-byte DICTID), so valid start positions go up to len(data) - 6 inclusive, but the while i < end loop excludes that last position. Unlikely to bite in practice (a stream starting 6 bytes from the end would have no compressed data), but still a real off-by-one.
2. No cycle guard in recursive UDT expansion (tag_browser.py:231)
_build_member follows SubPartIndex → _parse_source_part → _build_member without tracking visited parts. A malformed BlockInterface with circular SubPartIndex references would hit RecursionError. A seen: set[int] threaded through the recursion would prevent it. Not a concern with real PLCs, but cheap to add.
3. DB listing only matches 78 da (tag_browser.py:281)
datablocks_from_explore hardcodes b"\x78\xda" (zlib best-compression), but standard zlib streams can use other FLG bytes (78 9c, 78 5e, 78 01). If a firmware version emits PlcContentInfo with default compression the function silently returns []. Scanning for any 0x78 byte and validating the header checksum (CMF*256 + FLG must be divisible by 31) would be more robust.
None of these block merging — just worth fixing while the code is fresh.
Fixes for @gijzelaerr's review on PR gijzelaerr#780: - _find_zlib_stream: match any standard zlib CMF 0x78 with a valid header checksum (CMF*256+FLG % 31 == 0) and no preset-dict flag, instead of hardcoding 78 da. Tolerates other compression levels (78 01/5e/9c) that other firmwares may emit for PlcContentInfo. - _build_member / _parse_source_part: thread a `seen` set of Part identities through the recursive UDT expansion to stop a malformed circular SubPartIndex from causing RecursionError. - _find_preset_stream: fix off-by-one (end = len(data) - 5) so the last valid 6-byte header start position is scanned. 10/10 tests pass, ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8vSkgn4ifGkRznTqNXmpj
|
Thanks for the careful review — all three fixed in 48c8659:
10/10 tests still pass, ruff clean. |
Summary
Follow-up to #776. That PR added the preset-dictionary decompressor; this one adds the small, non-invasive layer on top that turns a decompressed EXPLORE response into structured records — for I/Q/M symbols and DB / FB / OB / UDT block interfaces. This is the wiring flagged as TODO in #776 / #757.
All of it lives in
s7commplus/tag_browser.pyandtests/test_tag_browser.pyand touches no existing files, so you can merge it as-is, call the helpers frombrowse()yourself, or fold it into #776 — whatever is least noisy.I/Q/M symbolic tags
parse_ident_container()/tags_from_explore()— decompress the<IdentContainer>blob (IntfDescTagdict,0xce9b821b) and mapRange+Width+ByteNumber+BitNumberto TIA syntax (%I0.0,%Q0.1,%MB100,%MW102,%MD104) asTagrecords.DB / FB / OB / UDT interfaces
parse_block_interface()/block_interface_from_explore()— decompress the<BlockInterface>blob (DebugInfo_IntfDescdict,0x66052b13) intoMemberrecords (name, type, LID, offset, section). A block EXPLORE bundles several preset-dict streams, so it targets the interface dict explicitly rather than taking the first zlib stream.<Member>'s type is a RID reference0x0200_00XXwhereXXis a Siemens SoftDataType id (SOFTDATATYPEmap transcribed from thomas-v2/S7CommPlusDriverCore/Softdatatype.cs, the same source as the shipped dictionaries). Complex members (arrays, UDTs, strings) already carry a ready-made, human-readableTypeattribute (Array[0..5] of IEC_TIMER,"myUdt"), which is used directly; the raw RID stays onMember.rid.<Member>elements (no RID) are skipped. DB/UDT members are flat (empty section).<SubParts>(selected by the member'sSubPartIndex); it is expanded recursively intoMember.fields. Library types (e.g.IEC_TIMER) are referenced but not inlined, so they expand to no fields. Embedded type definitions are never surfaced as top-level members.DB listing
datablocks_from_explore()— parse the DB list from a0x8A11FFFFwildcard EXPLORE.PlcContentInfois a standard zlib stream (78 da) embedded in a larger BLOB, so it is raw-inflated to tolerate the trailing bytes.Honest scope / testing
feat/zlib-preset-dictionariesbranch, notmaster— it imports the decompressor, so it should merge after (or into) feat: zlib preset dictionary decompressor for S7CommPlus blobs #776.I/Q/M symbols (verified against TIA Portal):
input_1 %I0.0,output_1 %Q0.1,merker_1 %M0.2,mtag_byte %MB100,mtag_word %MW102,mtag_dword %MD104,mtag_bool %M108.0.Optimized DB
Data_block_1(11 elementary members: Bool/Int/Real, cross-checked against the physical<Offsets>), plus a live FB (FB_Macchina) with sections +Array[0..5] of IEC_TIMER/Array[0..2] of Bool, and a DB member of a user UDTelettrovalvolathat expands into its 6 inner fields (fc_apertura,stato_ev, …). Tests use real EXPLORE blob fixtures from the G2; 10/10 pass, ruff clean.This makes the oracle pipeline from #757 (
_build_fdict/ regex_extract_tags/ symbolic-READ width workaround) obsolete:<SimpleType>carries the real M widths directly, and block interfaces resolve straight to clean member names, types, sections and nested UDT fields.🤖 Generated with Claude Code