Skip to content

Commit 1b0c380

Browse files
gijzelaerrclaude
andcommitted
Extract LIDs from browse() for symbolic access
Complete the symbolic access loop: browse() now captures the LID (from the object RID in EXPLORE responses) and symbol_crc for each variable. These can be fed into Tag objects for optimized-block access. New: - snap7.tags.from_browse(variables) — converts browse() results to dict[str, Tag], producing symbolic Tags when LIDs are present - _parse_explore_fields now captures "lid" and "symbol_crc" keys Workflow for optimized DBs: variables = client.browse() tags = from_browse(variables) value = client.read_tag(tags["Motor.Speed"]) # symbolic access Still experimental — the CRC handling in particular needs real PLC validation. If browse doesn't provide a CRC, the Tag uses CRC=0 (skip check) which the PLC may or may not accept depending on config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fa05269 commit 1b0c380

5 files changed

Lines changed: 84 additions & 6 deletions

File tree

s7/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from snap7.type import Area, Block, WordLen, SrvEvent, SrvArea
2222
from snap7.util.db import Row, DB
23-
from snap7.tags import Tag, load_csv, load_json, load_tia_xml
23+
from snap7.tags import Tag, load_csv, load_json, load_tia_xml, from_browse
2424

2525
__all__ = [
2626
"Client",
@@ -40,4 +40,5 @@
4040
"load_csv",
4141
"load_json",
4242
"load_tia_xml",
43+
"from_browse",
4344
]

s7/_s7commplus_client.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -868,15 +868,19 @@ def _parse_explore_fields(response: bytes, db_number: int, db_name: str) -> list
868868
"""Parse an EXPLORE response for a single DB to extract field layout.
869869
870870
Returns:
871-
List of dicts: ``{"name": str, "db_number": int, "db_name": str,
872-
"byte_offset": int, "data_type": str}``
871+
List of dicts with keys:
872+
``name``, ``db_number``, ``byte_offset``, ``data_type``, ``lid``,
873+
``symbol_crc``. ``lid`` and ``symbol_crc`` enable symbolic access
874+
for optimized DBs.
873875
"""
874876
from .vlq import decode_uint32_vlq as _vlq32
875877

876878
fields: list[dict[str, Any]] = []
877879
offset = 0
878880
field_name = ""
879881
byte_offset = 0
882+
field_lid = 0
883+
field_crc = 0
880884

881885
# Skip return code VLQ at start of response
882886
if offset < len(response):
@@ -890,6 +894,8 @@ def _parse_explore_fields(response: bytes, db_number: int, db_name: str) -> list
890894
if tag == 0xA1: # START_OF_OBJECT
891895
if offset + 4 > len(response):
892896
break
897+
# The RID bytes serve as the LID for symbolic access
898+
field_lid = struct.unpack(">I", response[offset : offset + 4])[0]
893899
offset += 4
894900
for _ in range(3):
895901
if offset >= len(response):
@@ -898,6 +904,7 @@ def _parse_explore_fields(response: bytes, db_number: int, db_name: str) -> list
898904
offset += consumed
899905
field_name = ""
900906
byte_offset = 0
907+
field_crc = 0
901908

902909
elif tag == 0xA2: # TERMINATING_OBJECT
903910
if field_name:
@@ -907,6 +914,8 @@ def _parse_explore_fields(response: bytes, db_number: int, db_name: str) -> list
907914
"db_number": db_number,
908915
"byte_offset": byte_offset,
909916
"data_type": "BYTE", # default; refined by type info
917+
"lid": field_lid,
918+
"symbol_crc": field_crc,
910919
}
911920
)
912921

snap7/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .partner import Partner
2222
from .logo import Logo
2323
from .util.db import Row, DB
24-
from .tags import Tag, load_csv, load_json, load_tia_xml
24+
from .tags import Tag, load_csv, load_json, load_tia_xml, from_browse
2525
from .type import Area, Block, WordLen, SrvEvent, SrvArea
2626

2727
__all__ = [
@@ -36,6 +36,7 @@
3636
"load_csv",
3737
"load_json",
3838
"load_tia_xml",
39+
"from_browse",
3940
"Area",
4041
"Block",
4142
"WordLen",

snap7/tags.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import re
3535
from dataclasses import dataclass, field
3636
from pathlib import Path
37-
from typing import Union
37+
from typing import Any, Union
3838

3939
from .type import Area
4040

@@ -403,6 +403,42 @@ def load_json(source: Union[str, Path]) -> dict[str, Tag]:
403403
return tags
404404

405405

406+
def from_browse(variables: list[dict[str, Any]]) -> dict[str, Tag]:
407+
"""Build a dict of Tags from :meth:`s7.Client.browse` results.
408+
409+
.. warning:: This function is **experimental** and may change.
410+
411+
When the browse result includes an ``lid`` key, the resulting Tag
412+
is configured for symbolic (LID-based) access suitable for
413+
optimized DBs. Otherwise it uses byte-offset access.
414+
415+
Args:
416+
variables: List of variable-info dicts from ``client.browse()``.
417+
418+
Returns:
419+
Dictionary mapping variable names to :class:`Tag` objects.
420+
"""
421+
tags: dict[str, Tag] = {}
422+
for var in variables:
423+
name = var.get("name", "")
424+
if not name:
425+
continue
426+
lid = var.get("lid", 0)
427+
crc = var.get("symbol_crc", 0)
428+
access_sequence = [lid] if lid else []
429+
tags[name] = Tag(
430+
area=Area.DB,
431+
db_number=int(var.get("db_number", 0)),
432+
byte_offset=int(var.get("byte_offset", 0)),
433+
datatype=str(var.get("data_type", "BYTE")),
434+
count=int(var.get("count", 1)),
435+
name=name,
436+
access_sequence=access_sequence,
437+
symbol_crc=int(crc),
438+
)
439+
return tags
440+
441+
406442
def load_tia_xml(source: Union[str, Path]) -> dict[str, Tag]:
407443
"""Load tags from a TIA Portal DB source XML export.
408444

tests/test_tags.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77

8-
from snap7.tags import Tag, load_csv, load_json, load_tia_xml
8+
from snap7.tags import Tag, from_browse, load_csv, load_json, load_tia_xml
99
from snap7.type import Area
1010

1111

@@ -197,6 +197,37 @@ def test_from_file(self, tmp_path: Path) -> None:
197197
assert "Tank.Level" in tags
198198

199199

200+
class TestFromBrowse:
201+
"""from_browse converts browse results to Tag dicts."""
202+
203+
def test_classic_browse_result(self) -> None:
204+
variables = [
205+
{"name": "Motor.Speed", "db_number": 1, "byte_offset": 0, "data_type": "REAL"},
206+
{"name": "Motor.Running", "db_number": 1, "byte_offset": 4, "data_type": "BOOL"},
207+
]
208+
tags = from_browse(variables)
209+
assert "Motor.Speed" in tags
210+
assert tags["Motor.Speed"].byte_offset == 0
211+
assert tags["Motor.Speed"].datatype == "REAL"
212+
assert tags["Motor.Speed"].is_symbolic is False
213+
214+
def test_symbolic_browse_result(self) -> None:
215+
"""When browse includes LID, produce symbolic Tags."""
216+
variables = [
217+
{"name": "Motor.Speed", "db_number": 1, "byte_offset": 0,
218+
"data_type": "REAL", "lid": 0xA, "symbol_crc": 0x12345678},
219+
]
220+
tags = from_browse(variables)
221+
assert tags["Motor.Speed"].is_symbolic is True
222+
assert tags["Motor.Speed"].access_sequence == [0xA]
223+
assert tags["Motor.Speed"].symbol_crc == 0x12345678
224+
225+
def test_skips_unnamed(self) -> None:
226+
variables = [{"name": "", "db_number": 1, "byte_offset": 0, "data_type": "BYTE"}]
227+
tags = from_browse(variables)
228+
assert len(tags) == 0
229+
230+
200231
class TestSymbolicAccess:
201232
"""Tag.from_access_string and symbolic access fields."""
202233

0 commit comments

Comments
 (0)