Skip to content

feat(s7commplus): full per-tag browse() — symbol tree with access-sequences#751

Merged
gijzelaerr merged 4 commits into
gijzelaerr:masterfrom
gridsociety:s7commplus-browse-typeinfo
Jun 19, 2026
Merged

feat(s7commplus): full per-tag browse() — symbol tree with access-sequences#751
gijzelaerr merged 4 commits into
gijzelaerr:masterfrom
gridsociety:s7commplus-browse-typeinfo

Conversation

@ale-rinaldi

@ale-rinaldi ale-rinaldi commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Full per-tag browse() — symbol tree with access-sequences

Returns the full per-tag symbol tree: each variable as {name, access_sequence (LID path), data_type, optimized/non-optimized byte+bit offsets}, convertible to a snap7.tags.Tag and readable via read_tag() / read_symbolic().

Rebased onto current master now that the prerequisite PRs (#745, #747, #749) have merged — the diff is the browse contribution only.

What's here

  • s7/typeinfo.py — a type-info parser + symbol-tree builder implemented independently from the S7CommPlus wire-protocol layout (PVartypeList / POffsetInfoType / the PObject tree / VarnameList): written against a protocol spec and the unit tests rather than by translating any existing driver; the module header credits the protocol. Handles scalars, structs/UDTs, 1-D / struct / multi-dimensional arrays (incl. BBOOL byte-aligned bit indexing) and multi-block vartype lists.
  • Sync browse() orchestration: enumerate DBs -> resolve each DB's type-info RID via a LID=1 read -> explore the type-info container -> recombine into the flat tree.
  • Async parity: async _send_request gains integrity_tail + multi-fragment reassembly, and async browse() returns the identical per-tag schema as sync.
  • Codec prerequisite: ValueDWord / ValueLWord are fixed-width big-endian, not VLQ.

Validation

Against a real S7-1500 (V2, TLS): sync and async browse() return a byte-for-byte identical symbol set (verified by hashing all names / access-sequences / offsets), and spot-reads via a browsed access-sequence match direct read_symbolic(). Full unit suite green.

(System datatypes — HW_*, CONN_*, ... — are intentionally not surfaced yet; tracked separately.)

@gijzelaerr gijzelaerr left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

The typeinfo.py module is well-structured: clean dataclass hierarchy, faithful port of the C# reference's type system, pure-function parsers, and a clear parse→tree-build→flatten pipeline. The test suite (449 lines) covers all offset-info variants, the tree builder (scalars, structs, basic/struct/mdim arrays, BBOOL alignment), and the EXPLORE response parsing. The codec DWORD/LWORD fix (fixed big-endian vs VLQ) is a genuine bug fix.

No malicious code. No network calls, no file I/O, no subprocess spawning in typeinfo.py — it's purely bytes-in/objects-out parsing.

PLC compatibility: All changes are in s7/ (S7CommPlus). No snap7/ files touched, so S7-300/400/1200 legacy PUT/GET is unaffected.

However, there are two blocking issues:

1. CRITICAL: Async browse() not updated — sync/async parity broken

The sync S7CommPlusClient.browse() is completely rewritten with the 5-phase orchestration (list DBs → read type-info RID → add native areas → explore type-info container → build tree), returning the new format with access_sequence, opt_address, etc. But S7CommPlusAsyncClient.browse() still uses the old _parse_explore_fields path and returns a different (inferior) dict shape. This means:

  • Sync and async browse() return incompatible result formats
  • Async users get the old, less complete symbol tree
  • The unified s7.AsyncClient wraps the async client, so it's also affected

This must be updated before merge.

2. LGPL-3.0 licensing question

python-snap7 is MIT-licensed. typeinfo.py header says it "ports the type-system structures from the thomas-v2/S7CommPlusDriver C# reference (LGPL-3.0)". Porting LGPL-3.0 code into an MIT project has licensing implications — the LGPL requires the derived work either remain LGPL-compatible or be structured so the LGPL portion can be replaced. A clean-room reimplementation from the protocol spec would be fine under MIT, but a direct "port" likely is not. This needs a licensing decision from the maintainer before merge.

Minor issues:

  • _add_subnodes mdim odometer: xx[dim + 1] += 1 when dim == 5 would index xx[6] (out of bounds). In practice this only triggers after n > array_element_count exits the while-loop, but it's brittle — add a guard.
  • The _parse_explore_fields function is now dead code for the sync path (only used by async browse()). Once async is updated, it can be removed.

Not ready to merge — needs async parity and a licensing decision.

Comment thread s7/typeinfo.py Outdated
Comment thread s7/_s7commplus_client.py Outdated
@ale-rinaldi ale-rinaldi force-pushed the s7commplus-browse-typeinfo branch from 2a10cb6 to adbdc0a Compare June 18, 2026 18:23
@ale-rinaldi ale-rinaldi marked this pull request as ready for review June 18, 2026 18:23
@ale-rinaldi ale-rinaldi force-pushed the s7commplus-browse-typeinfo branch from adbdc0a to 7c12e0f Compare June 18, 2026 18:38
@ale-rinaldi

Copy link
Copy Markdown
Contributor Author

Updated, and rebased onto current master (the prerequisite PRs #745 / #747 / #749 have merged) — the diff is the browse contribution only now.

  • Licensing: typeinfo.py reimplemented independently from the wire protocol (see thread).
  • Async parity: async browse() brought to full parity, identical result schema (see thread).
  • _parse_explore_fields (the old shallow browse path) is removed.
  • Re: the mdim odometer note — xx[dim + 1] cannot index xx[6]: the loop is for dim in range(5) (so dim ≤ 4), the highest write is xx[5], and xx has 6 slots. No out-of-bounds, so left as is.

Validation: sync + async browse() return a byte-for-byte identical symbol set on a live S7-1500 (verified by hashing all names / access-sequences / offsets); full unit suite green.

claude added 4 commits June 18, 2026 20:54
ValueDWord / ValueLWord are fixed 4-/8-byte big-endian values, unlike UDInt/ULInt
which are VLQ. They were wrongly grouped with the VLQ ints in decode_pvalue_to_bytes,
encode_typed_value and skip_typed_value; split them out as fixed-width. (16-bit
Word/UInt/Int were already fixed; only the 32/64-bit *Int* types are VLQ.) browse()
reads DWORD/LWORD tags, so this is a prerequisite.
Implemented independently from the S7CommPlus wire-protocol layout (PVartypeList /
POffsetInfoType / the PObject tree / VarnameList) — written against a protocol spec and
the unit tests rather than by translating any existing driver. Parses the EXPLORE
type-info container and flattens it into per-tag records: symbolic name, access-sequence
(LID path), datatype, and optimized/non-optimized byte+bit offsets. Handles scalars,
structs/UDTs, 1-D / struct / multi-dimensional arrays (incl. BBOOL byte-aligned bit
indexing) and multi-block vartype lists.
Enumerate DBs, resolve each DB's type-info RID via a LID=1 read, add the native process
areas, explore the type-info container, and recombine into the flat symbol tree. Returns
a list of {name, access_sequence, data_type, opt/nonopt byte+bit offsets}.
async _send_request gains integrity_tail + multi-fragment reassembly; async browse()
mirrors the sync per-tag tree with the identical result schema. Removes the old shallow
_parse_explore_fields path.
@ale-rinaldi ale-rinaldi force-pushed the s7commplus-browse-typeinfo branch from 7c12e0f to 017878b Compare June 18, 2026 18:54

@gijzelaerr gijzelaerr left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review after rebase (both prior blockers resolved)

Blocker #1: Async parity — RESOLVED

Async browse() now uses the full typeinfo-based orchestration (_read_typeinfo_rid, _explore_type_info_container, typeinfo.build_tree, typeinfo.build_flat_list). The old _parse_explore_fields import is removed. Async _send_request also gains integrity_tail + reassembly. Sync and async now return the identical schema.

Blocker #2: LGPL licensing — RESOLVED

typeinfo.py header no longer references "porting" from the C# driver. It now says: "follows the S7CommPlus wire protocol … as observed on an S7-1500 and documented publicly." The PR description confirms: "written against a protocol spec and the unit tests rather than by translating any existing driver."

Diff is clean after rebase

8 files (down from 11 before rebase). The #745/#747/#749 base changes (connection.py, _s7commplus_server.py, protocol.py, test_s7_tls.py) are gone — only browse-specific content remains.

DWORD/LWORD codec fix still present

ValueDWord (fixed 4-byte BE) and ValueLWord (fixed 8-byte BE) correctly split from VLQ-encoded UDINT/ULINT.

Security: ✅ No issues. typeinfo.py is pure parsing — no I/O, no network, no subprocess.

PLC compatibility: S7CommPlus only. No snap7/ touched.

Ready to merge.

@gijzelaerr gijzelaerr merged commit a871932 into gijzelaerr:master Jun 19, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants