Skip to content

Commit 20071f7

Browse files
committed
feat: Improve the structure of the model to more closely match am L5X file.
1 parent df86656 commit 20071f7

File tree

6 files changed

+169
-16
lines changed

6 files changed

+169
-16
lines changed

acd/generated/comps/rx_generic.py

+22
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ def cip_data_type(self):
159159
self._io.seek(_pos)
160160
return getattr(self, '_m_cip_data_type', None)
161161

162+
@property
163+
def radix(self):
164+
if hasattr(self, '_m_radix'):
165+
return self._m_radix
166+
167+
_pos = self._io.pos()
168+
self._io.seek(32)
169+
self._m_radix = self._io.read_u2le()
170+
self._io.seek(_pos)
171+
return getattr(self, '_m_radix', None)
172+
162173
@property
163174
def data_type(self):
164175
if hasattr(self, '_m_data_type'):
@@ -200,6 +211,17 @@ def valid(self):
200211
self._m_valid = True
201212
return getattr(self, '_m_valid', None)
202213

214+
@property
215+
def external_access(self):
216+
if hasattr(self, '_m_external_access'):
217+
return self._m_external_access
218+
219+
_pos = self._io.pos()
220+
self._io.seek(34)
221+
self._m_external_access = self._io.read_u2le()
222+
self._io.seek(_pos)
223+
return getattr(self, '_m_external_access', None)
224+
203225
@property
204226
def dimension_1(self):
205227
if hasattr(self, '_m_dimension_1'):

acd/l5x/elements.py

+137-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os
22
import shutil
33
import struct
4-
from dataclasses import dataclass
4+
from dataclasses import dataclass, field
5+
from enum import Enum
56
from os import PathLike
67
from pathlib import Path
78
from sqlite3 import Cursor
@@ -38,7 +39,7 @@ def to_xml(self):
3839
if isinstance(attribute_value, L5xElement):
3940
child_list.append(attribute_value.to_xml())
4041
elif isinstance(attribute_value, list):
41-
if attribute == "tags":
42+
if attribute == "tags" or attribute == "data_types" or attribute == "members":
4243
new_child_list: List[str] = []
4344
for element in attribute_value:
4445
if isinstance(element, L5xElement):
@@ -48,16 +49,30 @@ def to_xml(self):
4849
child_list.append(f'<{attribute.title().replace("_", "")}>{" ".join(new_child_list)}</{attribute.title().replace("_", "")}>')
4950

5051
else:
52+
if attribute == "cls":
53+
attribute = "class"
5154
attribute_list.append(f'{attribute.title().replace("_", "")}="{attribute_value}"')
5255

5356
_export_name = self.__class__.__name__.title().replace("_", "")
5457
return f'<{_export_name} {" ".join(attribute_list)}>{" ".join(child_list)}</{_export_name}>'
5558

5659

60+
@dataclass
61+
class Member(L5xElement):
62+
name: str
63+
data_type: str
64+
dimension: int
65+
radix: str
66+
hidden: bool
67+
external_access: str
68+
69+
5770
@dataclass
5871
class DataType(L5xElement):
5972
name: str
60-
children: List[str]
73+
family: str
74+
cls: str
75+
members: List[Member]
6176

6277

6378
@dataclass
@@ -131,6 +146,85 @@ def __post_init__(self):
131146
self._name = "RSLogix5000Content"
132147

133148

149+
def radix_enum(i: int) -> str:
150+
if i == 0:
151+
return "NullType"
152+
if i == 1:
153+
return "General"
154+
if i == 2:
155+
return "Binary"
156+
if i == 3:
157+
return "Octal"
158+
if i == 4:
159+
return "Decimal"
160+
if i == 5:
161+
return "Hex"
162+
if i == 6:
163+
return "Exponential"
164+
if i == 7:
165+
return "Float"
166+
if i == 8:
167+
return "ASCII"
168+
if i == 9:
169+
return "Unicode"
170+
if i == 10:
171+
return "Date/Time"
172+
if i == 11:
173+
return "Date/Time (ns)"
174+
if i == 12:
175+
return "UseTypeStyle"
176+
return "General"
177+
178+
179+
def external_access_enum(i: int) -> str:
180+
if i == 0:
181+
return "Read/Write"
182+
if i == 1:
183+
return "Read Only"
184+
if i == 2:
185+
return "None"
186+
return "Read/Write"
187+
188+
@dataclass
189+
class MemberBuilder(L5xElementBuilder):
190+
record: List[int] = field(default_factory=[])
191+
192+
def build(self) -> Member:
193+
self._cur.execute(
194+
"SELECT comp_name, object_id, parent_id, record FROM comps WHERE object_id=" + str(
195+
self._object_id))
196+
results = self._cur.fetchall()
197+
198+
name = results[0][0]
199+
r = RxGeneric.from_bytes(results[0][3])
200+
try:
201+
r = RxGeneric.from_bytes(results[0][3])
202+
except Exception as e:
203+
return Member(name, name, "", 0, "Decimal", False, "Read/Write")
204+
205+
extended_records: Dict[int, List[int]] = {}
206+
for extended_record in r.extended_records:
207+
extended_records[extended_record.attribute_id] = extended_record.value
208+
extended_records[r.last_extended_record.attribute_id] = r.last_extended_record.value
209+
210+
cip_data_typoe = struct.unpack_from("<I", self.record, 0x78)[0]
211+
dimension = struct.unpack_from("<I", self.record, 0x5C)[0]
212+
radix = radix_enum(struct.unpack_from("<I", self.record, 0x54)[0])
213+
data_type_id = struct.unpack_from("<I", self.record, 0x58)[0]
214+
hidden = bool(struct.unpack_from("<I", self.record, 0x70)[0])
215+
external_access = external_access_enum(struct.unpack_from("<I", self.record, 0x74)[0])
216+
217+
218+
self._cur.execute(
219+
"SELECT comp_name, object_id, parent_id, record FROM comps WHERE object_id=" + str(
220+
data_type_id))
221+
data_type_results = self._cur.fetchall()
222+
data_type = data_type_results[0][0]
223+
224+
225+
return Member(name, name, data_type, dimension, radix, hidden, external_access)
226+
227+
134228
@dataclass
135229
class DataTypeBuilder(L5xElementBuilder):
136230

@@ -140,25 +234,53 @@ def build(self) -> DataType:
140234
self._object_id))
141235
results = self._cur.fetchall()
142236

143-
record = results[0][3]
144237
name = results[0][0]
145238

239+
try:
240+
r = RxGeneric.from_bytes(results[0][3])
241+
except Exception as e:
242+
return DataType(name, name, "NoFamily", "User", [])
243+
244+
extended_records: Dict[int, List[int]] = {}
245+
for extended_record in r.extended_records:
246+
extended_records[extended_record.attribute_id] = extended_record.value
247+
extended_records[r.last_extended_record.attribute_id] = r.last_extended_record.value
248+
249+
string_family_int = struct.unpack("<I", extended_records[0x6C])[0]
250+
string_family = "StringFamily" if string_family_int == 1 else "NoFamily"
251+
252+
built_in = struct.unpack("<I", extended_records[0x67])[0]
253+
module_defined = struct.unpack("<I", extended_records[0x69])[0]
254+
255+
class_type = "User"
256+
if module_defined > 0:
257+
class_type = "IO"
258+
if built_in > 0:
259+
class_type = "ProductDefined"
260+
if len(extended_records[0x64]) == 0x04:
261+
member_count = struct.unpack("<I", extended_records[0x64])[0]
262+
else:
263+
member_count = 0
264+
146265
self._cur.execute(
147266
"SELECT comp_name, object_id, parent_id, record FROM comps WHERE parent_id=" + str(
148267
self._object_id))
149268
member_results = self._cur.fetchall()
269+
children: List[Member] = []
150270
if len(member_results) == 1:
151271
member_collection_id = member_results[0][1]
152272

153273
self._cur.execute(
154-
"SELECT comp_name, object_id, parent_id, record FROM comps WHERE parent_id=" + str(
155-
member_collection_id))
274+
f"SELECT comp_name, object_id, parent_id, seq_number, record FROM comps WHERE parent_id={member_collection_id} ORDER BY seq_number")
156275
children_results = self._cur.fetchall()
157-
children = []
158-
for child in children_results:
159-
children.append(child[0])
160-
return DataType(name, name, children)
161-
return DataType(name, name, [])
276+
277+
if member_count != len(children_results):
278+
raise Exception("Member and children list arent the same length")
279+
280+
for idx, child in enumerate(children_results):
281+
children.append(MemberBuilder(self._cur, child[1], extended_records[0x6E + idx]).build())
282+
283+
return DataType(name, name, string_family, class_type, children)
162284

163285

164286
@dataclass
@@ -236,13 +358,16 @@ def build(self) -> Tag:
236358
name_length = struct.unpack("<H", extended_records[0x01][0:2])[0]
237359
name = bytes(extended_records[0x01][2:name_length+2]).decode('utf-8')
238360

361+
radix = radix_enum(r.main_record.radix)
362+
external_access = external_access_enum(r.main_record.external_access)
363+
239364
if r.main_record.dimension_1 != 0:
240365
data_type = data_type + "[" + str(r.main_record.dimension_1) + "]"
241366
if r.main_record.dimension_2 != 0:
242367
data_type = data_type + "[" + str(r.main_record.dimension_2) + "]"
243368
if r.main_record.dimension_3 != 0:
244369
data_type = data_type + "[" + str(r.main_record.dimension_3) + "]"
245-
return Tag(name, name, "Base", data_type, "Decimal", "Read/Write", r.main_record.data_table_instance, comment_results)
370+
return Tag(name, name, "Base", data_type, radix, external_access, r.main_record.data_table_instance, comment_results)
246371

247372

248373
@dataclass

resources/templates/Comps/RxGeneric.ksy

+6
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ types:
6464
data_type:
6565
pos: 0x1C
6666
type: u4
67+
radix:
68+
pos: 0x20
69+
type: u2
70+
external_access:
71+
pos: 0x22
72+
type: u2
6773
data_table_instance:
6874
pos: 0x24
6975
type: u4

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def run(self):
4848

4949
setup(
5050
name="acd-tools",
51-
version="0.2a4",
51+
version="0.2a5",
5252
description="Rockwell ACD File Tools",
5353
classifiers=[
5454
"Development Status :: 3 - Alpha",

test/test_api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_dump_to_files():
2525
DumpCompsRecords(export._cur, 0).dump(0)
2626

2727

28-
def manual_test_to_xml():
28+
def test_to_xml():
2929
importer = ImportProjectFromFile(Path(os.path.join("..", "resources", "CuteLogix.ACD")))
3030
project: RSLogix5000Content = importer.import_project()
3131
ss = project.to_xml()

test/test_database.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ def test_parse_rungs_dat(controller):
4545

4646
def test_parse_datatypes_dat(controller):
4747
data_type = controller.data_types[-1].name
48-
child = controller.data_types[-1].children[-1]
48+
child = controller.data_types[-1].members[-1]
4949
assert data_type == 'STRING20'
50-
assert child == 'DATA'
50+
assert child.name == 'DATA'
5151

5252

5353
def test_parse_tags_dat(controller):

0 commit comments

Comments
 (0)