From 625ff12af654a112d94f966103213d04c3265162 Mon Sep 17 00:00:00 2001 From: Ben Hutcheson Date: Sun, 7 Jul 2024 16:28:31 +0200 Subject: [PATCH] fix: created generic parsing of fields --- acd/api.py | 18 +- acd/export_l5x.py | 12 +- acd/generated/comps/rx_generic.py | 239 ++++++++++++++++++ acd/l5x/elements.py | 199 +++++++++++---- resources/templates/Comps/RxGeneric.ksy | 98 +++++++ .../templates/Controller/RxController.ksy | 8 +- resources/templates/MapDevice/RxMapDevice.ksy | 15 +- setup.py | 9 +- test/test_api.py | 16 +- 9 files changed, 528 insertions(+), 86 deletions(-) create mode 100644 acd/generated/comps/rx_generic.py create mode 100644 resources/templates/Comps/RxGeneric.ksy diff --git a/acd/api.py b/acd/api.py index 19eaacb..4d1b222 100644 --- a/acd/api.py +++ b/acd/api.py @@ -1,25 +1,23 @@ from dataclasses import dataclass from os import PathLike +from typing import List from acd.database.acd_database import AcdDatabase -from acd.l5x.elements import Controller, DumpCompsRecords +from acd.l5x.elements import Controller, DumpCompsRecords, RSLogix5000Content from acd.export_l5x import ExportL5x from acd.unzip import Unzip # Returned Project Structures -@dataclass -class Project: - """Controller Project""" - controller: Controller + # Import Export Interfaces class ImportProject: """"Interface to import an PLC project""" - def import_project(self) -> Project: + def import_project(self) -> RSLogix5000Content: # Import Project Interface pass @@ -27,7 +25,7 @@ def import_project(self) -> Project: class ExportProject: """"Interface to export an PLC project""" - def export_project(self, project: Project): + def export_project(self, project: RSLogix5000Content): # Export Project Interface pass @@ -38,10 +36,10 @@ class ImportProjectFromFile(ImportProject): """Import a Controller from an ACD stored on file""" filename: PathLike - def import_project(self) -> Project: + def import_project(self) -> RSLogix5000Content: # Import Project Interface export = ExportL5x(self.filename) - return Project(export.controller) + return export.project @dataclass @@ -49,7 +47,7 @@ class ExportProjectToFile(ExportProject): """Export a Controller to an ACD file""" filename: PathLike - def export_project(self, project: Project): + def export_project(self, project: RSLogix5000Content): # Concreate example of exporting a Project Object to an ACD file raise NotImplementedError diff --git a/acd/export_l5x.py b/acd/export_l5x.py index dbdbda4..eccba06 100644 --- a/acd/export_l5x.py +++ b/acd/export_l5x.py @@ -4,12 +4,14 @@ import struct import tempfile from dataclasses import dataclass, field +from datetime import datetime from sqlite3 import Cursor +import xml.etree.ElementTree as ET from acd.comments import CommentsRecord from acd.comps import CompsRecord from acd.dbextract import DbExtract -from acd.l5x.elements import Controller, ControllerBuilder +from acd.l5x.elements import Controller, ControllerBuilder, ProjectBuilder, RSLogix5000Content from acd.nameless import NamelessRecord from acd.sbregion import SbRegionRecord from acd.unzip import Unzip @@ -21,6 +23,7 @@ class ExportL5x: input_filename: os.PathLike _temp_dir: str = "build" #tempfile.mkdtemp() _controller: Controller = None + _project: RSLogix5000Content = None def __post_init__(self): log.info("Creating temporary directory (if it doesn't exist to store ACD database files - " + self._temp_dir) @@ -98,6 +101,13 @@ def controller(self): self._controller = ControllerBuilder(self._cur).build() return self._controller + @property + def project(self): + if self._project is None: + self._project = ProjectBuilder(os.path.join(self._temp_dir, 'QuickInfo.XML')).build() + self._project.controller = self.controller + return self._project + def populate_region_map(self): self._cur.execute( diff --git a/acd/generated/comps/rx_generic.py b/acd/generated/comps/rx_generic.py new file mode 100644 index 0000000..0ce0226 --- /dev/null +++ b/acd/generated/comps/rx_generic.py @@ -0,0 +1,239 @@ +# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild + +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO + + +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) + +class RxGeneric(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.parent_id = self._io.read_u4le() + self.unique_tag_identifier = self._io.read_u4le() + self.record_format_version = self._io.read_u2le() + self.cip_type = self._io.read_u2le() + self.comment_id = self._io.read_u2le() + _on = self.cip_type + if _on == 107: + self._raw_main_record = self._io.read_bytes(60) + _io__raw_main_record = KaitaiStream(BytesIO(self._raw_main_record)) + self.main_record = RxGeneric.RxTag(_io__raw_main_record, self, self._root) + else: + self._raw_main_record = self._io.read_bytes(60) + _io__raw_main_record = KaitaiStream(BytesIO(self._raw_main_record)) + self.main_record = RxGeneric.Unknown(_io__raw_main_record, self, self._root) + self.len_record = self._io.read_u4le() + self.count_record = self._io.read_u4le() + self.extended_records = [] + for i in range((self.count_record - 1)): + self.extended_records.append(RxGeneric.AttributeRecord(self._io, self, self._root)) + + self.last_extended_record = RxGeneric.LastAttributeRecord(self._io, self, self._root) + + class Unknown(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.body = self._io.read_bytes(60) + + + class LastAttributeRecord(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.attribute_id = self._io.read_u4le() + self.record_length = self._io.read_u4le() + self.value = self._io.read_bytes((self.record_length - 4)) + + + class RxMapDevice(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + pass + + @property + def module_id(self): + if hasattr(self, '_m_module_id'): + return self._m_module_id + + _pos = self._io.pos() + self._io.seek(36) + self._m_module_id = self._io.read_u4le() + self._io.seek(_pos) + return getattr(self, '_m_module_id', None) + + @property + def product_type(self): + if hasattr(self, '_m_product_type'): + return self._m_product_type + + _pos = self._io.pos() + self._io.seek(4) + self._m_product_type = self._io.read_u2le() + self._io.seek(_pos) + return getattr(self, '_m_product_type', None) + + @property + def vendor_id(self): + if hasattr(self, '_m_vendor_id'): + return self._m_vendor_id + + _pos = self._io.pos() + self._io.seek(2) + self._m_vendor_id = self._io.read_u2le() + self._io.seek(_pos) + return getattr(self, '_m_vendor_id', None) + + @property + def slot_no(self): + if hasattr(self, '_m_slot_no'): + return self._m_slot_no + + _pos = self._io.pos() + self._io.seek(32) + self._m_slot_no = self._io.read_u4le() + self._io.seek(_pos) + return getattr(self, '_m_slot_no', None) + + @property + def product_code(self): + if hasattr(self, '_m_product_code'): + return self._m_product_code + + _pos = self._io.pos() + self._io.seek(6) + self._m_product_code = self._io.read_u2le() + self._io.seek(_pos) + return getattr(self, '_m_product_code', None) + + @property + def parent_module(self): + if hasattr(self, '_m_parent_module'): + return self._m_parent_module + + _pos = self._io.pos() + self._io.seek(22) + self._m_parent_module = self._io.read_u4le() + self._io.seek(_pos) + return getattr(self, '_m_parent_module', None) + + + class RxTag(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + pass + + @property + def cip_data_type(self): + if hasattr(self, '_m_cip_data_type'): + return self._m_cip_data_type + + _pos = self._io.pos() + self._io.seek(52) + self._m_cip_data_type = self._io.read_u2le() + self._io.seek(_pos) + return getattr(self, '_m_cip_data_type', None) + + @property + def data_type(self): + if hasattr(self, '_m_data_type'): + return self._m_data_type + + _pos = self._io.pos() + self._io.seek(28) + self._m_data_type = self._io.read_u4le() + self._io.seek(_pos) + return getattr(self, '_m_data_type', None) + + @property + def dimension_2(self): + if hasattr(self, '_m_dimension_2'): + return self._m_dimension_2 + + _pos = self._io.pos() + self._io.seek(16) + self._m_dimension_2 = self._io.read_u4le() + self._io.seek(_pos) + return getattr(self, '_m_dimension_2', None) + + @property + def dimension_3(self): + if hasattr(self, '_m_dimension_3'): + return self._m_dimension_3 + + _pos = self._io.pos() + self._io.seek(20) + self._m_dimension_3 = self._io.read_u4le() + self._io.seek(_pos) + return getattr(self, '_m_dimension_3', None) + + @property + def valid(self): + if hasattr(self, '_m_valid'): + return self._m_valid + + self._m_valid = True + return getattr(self, '_m_valid', None) + + @property + def dimension_1(self): + if hasattr(self, '_m_dimension_1'): + return self._m_dimension_1 + + _pos = self._io.pos() + self._io.seek(12) + self._m_dimension_1 = self._io.read_u4le() + self._io.seek(_pos) + return getattr(self, '_m_dimension_1', None) + + @property + def data_table_instance(self): + if hasattr(self, '_m_data_table_instance'): + return self._m_data_table_instance + + _pos = self._io.pos() + self._io.seek(36) + self._m_data_table_instance = self._io.read_u4le() + self._io.seek(_pos) + return getattr(self, '_m_data_table_instance', None) + + + class AttributeRecord(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.attribute_id = self._io.read_u4le() + self.record_length = self._io.read_u4le() + self.value = self._io.read_bytes(self.record_length) + + + diff --git a/acd/l5x/elements.py b/acd/l5x/elements.py index 5753274..325e69b 100644 --- a/acd/l5x/elements.py +++ b/acd/l5x/elements.py @@ -6,8 +6,11 @@ from pathlib import Path from sqlite3 import Cursor from typing import List, Tuple, Dict +from datetime import datetime, timedelta +import xml.etree.ElementTree as ET from acd.exceptions.CompsRecordException import UnknownRxTagVersion +from acd.generated.comps.rx_generic import RxGeneric from acd.generated.comps.rx_tag import RxTag from acd.generated.controller.rx_controller import RxController from acd.generated.map_device.rx_map_device import RxMapDevice @@ -21,16 +24,42 @@ class L5xElementBuilder: @dataclass class L5xElement: - name: str + _name: str + + def __post_init__(self): + self._export_name = "" + + def to_xml(self): + attribute_list: List[str] = [] + child_list: List[str] = [] + for attribute in self.__dict__: + if attribute[0] != "_": + attribute_value = self.__getattribute__(attribute) + if isinstance(attribute_value, L5xElement): + child_list.append(attribute_value.to_xml()) + elif isinstance(attribute_value, list): + pass + #for element in attribute_value: + # if isinstance(element, L5xElement): + # child_list.append(element.to_xml()) + # else: + # child_list.append(f"<{element}/>") + else: + attribute_list.append(f'{attribute.title().replace("_", "")}="{attribute_value}"') + + _export_name = self.__class__.__name__.title().replace("_", "") + return f'<{_export_name} {" ".join(attribute_list)}>{" ".join(child_list)}' @dataclass class DataType(L5xElement): + name: str children: List[str] @dataclass class Tag(L5xElement): + name: str data_table_instance: int data_type: str comments: List[Tuple[str, str]] @@ -68,6 +97,11 @@ class Program(L5xElement): class Controller(L5xElement): serial_number: str path: str + sfc_execution_control: str + sfc_restart_position: str + sfc_last_scan: str + created_date: str + modified_date: str data_types: List[DataType] tags: List[Tag] programs: List[Program] @@ -75,6 +109,22 @@ class Controller(L5xElement): map_devices: List[MapDevice] +@dataclass +class RSLogix5000Content(L5xElement): + """Controller Project""" + controller: Controller + schema_revision: str + software_revision: str + target_name: str + target_name: str + contains_context: str + export_date: str + export_options: str + + def __post_init__(self): + self._name = "RSLogix5000Content" + + @dataclass class DataTypeBuilder(L5xElementBuilder): @@ -101,8 +151,8 @@ def build(self) -> DataType: children = [] for child in children_results: children.append(child[0]) - return DataType(name, children) - return DataType(name, []) + return DataType(name, name, children) + return DataType(name, name, []) @dataclass @@ -113,21 +163,33 @@ def build(self) -> MapDevice: "SELECT comp_name, object_id, parent_id, record FROM comps WHERE object_id=" + str( self._object_id)) results = self._cur.fetchall() + try: + r = RxGeneric.from_bytes(results[0][3]) + except Exception as e: + return MapDevice(results[0][0], 0, 0, 0, 0, 0, 0, []) - r = RxMapDevice.from_bytes(results[0][3]) - - if r.record_format_version == 0x00: + if r.cip_type != 0x69: return MapDevice(results[0][0], 0, 0, 0, 0, 0, 0, []) - elif not r.body.valid: - return MapDevice("", 0, 0, 0, 0, 0, 0, []) self._cur.execute( "SELECT tag_reference, record_string FROM comments WHERE parent=" + str( - r.comment_id)) + (r.cip_type * 0x0100) + r.comment_id)) comment_results = self._cur.fetchall() + extended_records: Dict[int, List[int]] = {} + for extended_record in r.extended_records: + extended_records[extended_record.attribute_id] = extended_record.value + extended_records[r.last_extended_record.attribute_id] = r.last_extended_record.value + + vendor_id = struct.unpack(" Tag: self._object_id)) results = self._cur.fetchall() - r = RxTag.from_bytes(results[0][3]) + r = RxGeneric.from_bytes(results[0][3]) - if r.record_format_version == 0x00: - return Tag(results[0][0], 0, "", []) - elif not r.body.valid: - raise UnknownRxTagVersion(r.record_format_version) + if r.cip_type != 0x6B: + return Tag(results[0][0], results[0][0], 0, "", []) + if r.main_record.data_type == 0xFFFFFFFF: + return Tag(results[0][0], results[0][0], r.main_record.data_table_instance, "", []) - if r.body.data_type == 4294967295: - data_type = "" - name = r.body.name - comment_results = [] - else: - self._cur.execute( - "SELECT comp_name, object_id, parent_id, record FROM comps WHERE object_id=" + str( - r.body.data_type)) - data_type_results = self._cur.fetchall() - data_type = data_type_results[0][0] + self._cur.execute( + "SELECT comp_name, object_id, parent_id, record FROM comps WHERE object_id=" + str( + r.main_record.data_type)) + data_type_results = self._cur.fetchall() + data_type = data_type_results[0][0] - self._cur.execute( - "SELECT tag_reference, record_string FROM comments WHERE parent=" + str( - r.comment_id)) - comment_results = self._cur.fetchall() - try: - name = r.body.name - except Exception as e: - name = "" - if len(comment_results) > 0: - pass - if r.body.dimension_1 != 0: - data_type = data_type + "[" + str(r.body.dimension_1) + "]" - if r.body.dimension_2 != 0: - data_type = data_type + "[" + str(r.body.dimension_2) + "]" - if r.body.dimension_3 != 0: - data_type = data_type + "[" + str(r.body.dimension_3) + "]" - return Tag(name, r.body.data_table_instance, data_type, comment_results) + self._cur.execute( + "SELECT tag_reference, record_string FROM comments WHERE parent=" + str( + (r.cip_type * 0x0100) + r.comment_id)) + comment_results = self._cur.fetchall() + + extended_records: Dict[int, List[int]] = {} + for extended_record in r.extended_records: + extended_records[extended_record.attribute_id] = extended_record.value + extended_records[r.last_extended_record.attribute_id] = r.last_extended_record.value + + name_length = struct.unpack(" Controller: results = self._cur.fetchall() if len(results) != 1: raise Exception("Does not contain exactly one root controller node") - serial_number = "" - path = "" - try: - r = RxController.from_bytes(results[0][4]) - serial_number = hex(r.body.serial_number) - if r.record_format_version == 103: - path = r.body.path - except: - pass + + r = RxGeneric.from_bytes(results[0][4]) + self._cur.execute( + "SELECT tag_reference, record_string FROM comments WHERE parent=" + str( + (r.cip_type * 0x0100) + r.comment_id)) + comment_results = self._cur.fetchall() + + extended_records: Dict[int, List[int]] = {} + for extended_record in r.extended_records: + extended_records[extended_record.attribute_id] = extended_record.value + extended_records[r.last_extended_record.attribute_id] = r.last_extended_record.value + + path = bytes(extended_records[0x6a][:-2]).decode('utf-16') + sfc_execution_control = bytes(extended_records[0x6F][:-2]).decode('utf-16') + sfc_restart_position = bytes(extended_records[0x70][:-2]).decode('utf-16') + sfc_last_scan = bytes(extended_records[0x71][:-2]).decode('utf-16') + + serial_number = hex(struct.unpack(" Controller: _map_device_object_id = result[1] map_devices.append(MapDeviceBuilder(self._cur, _map_device_object_id).build()) - return Controller(controller_name, serial_number, path, data_types, tags, programs, aois, map_devices) + return Controller(controller_name, serial_number, path, sfc_execution_control, sfc_restart_position, sfc_last_scan, created_date, modified_date, data_types, tags, programs, aois, map_devices) + +@dataclass +class ProjectBuilder: + quick_info_filename: PathLike + + def build(self) -> RSLogix5000Content: + element = ET.parse(self.quick_info_filename) + target_name = element.find(".").attrib["Name"] + schema_revision = str(element.find("SchemaVersion").attrib["Major"]) + "." + str( + element.find("SchemaVersion").attrib["Minor"]) + software_revision = str(element.find("DeviceIdentity").attrib["MajorRevision"]) + "." + str( + element.find("DeviceIdentity").attrib["MinorRevision"]) + target_type = "Controller" + contains_context = "false" + now = datetime.now() + export_date = now.strftime("%a %b %d %H:%M:%S %Y") + export_options = "NoRawData L5KData DecoratedData ForceProtectedEncoding AllProjDocTrans" + return RSLogix5000Content(None, schema_revision, software_revision, target_name, target_type, contains_context, export_date, export_options) + @dataclass class DumpCompsRecords(L5xElementBuilder): diff --git a/resources/templates/Comps/RxGeneric.ksy b/resources/templates/Comps/RxGeneric.ksy new file mode 100644 index 0000000..483bb93 --- /dev/null +++ b/resources/templates/Comps/RxGeneric.ksy @@ -0,0 +1,98 @@ +meta: + id: rx_generic + endian: le + +seq: + - id: parent_id + type: u4 + - id: unique_tag_identifier + type: u4 + - id: record_format_version + type: u2 + - id: cip_type + type: u2 + - id: comment_id + type: u2 + - id: main_record + size: 0x3C + type: + switch-on: cip_type + cases: + 0x6B: rx_tag + _: unknown + - id: len_record + type: u4 + - id: count_record + type: u4 + - id: extended_records + type: attribute_record + repeat: expr + repeat-expr: count_record - 1 + - id: last_extended_record + type: last_attribute_record + +types: + attribute_record: + seq: + - id: attribute_id + type: u4 + - id: record_length + type: u4 + - id: value + size: record_length + last_attribute_record: + seq: + - id: attribute_id + type: u4 + - id: record_length + type: u4 + - id: value + size: record_length - 4 + rx_tag: + instances: + valid: + value: true + dimension_1: + pos: 0x0C + type: u4 + dimension_2: + pos: 0x10 + type: u4 + dimension_3: + pos: 0x14 + type: u4 + data_type: + pos: 0x1C + type: u4 + data_table_instance: + pos: 0x24 + type: u4 + cip_data_type: + pos: 0x34 + type: u2 + + unknown: + seq: + - id: body + size: 0x3C + + rx_map_device: + instances: + vendor_id: + pos: 0x02 + type: u2 + product_type: + pos: 0x04 + type: u2 + product_code: + pos: 0x06 + type: u2 + parent_module: + pos: 0x16 + type: u4 + slot_no: + pos: 0x20 + type: u4 + module_id: + pos: 0x24 + type: u4 diff --git a/resources/templates/Controller/RxController.ksy b/resources/templates/Controller/RxController.ksy index 93526c6..3910aee 100644 --- a/resources/templates/Controller/RxController.ksy +++ b/resources/templates/Controller/RxController.ksy @@ -63,18 +63,18 @@ types: record: pos: 0x4A size: len_record - len_current_active: + len_sfc_execution_control: type: u4 pos: 0xC4 - current_acive: + sfc_execution_control: pos: 0xC8 type: str size: len_current_active encoding: utf-16 - len_most_recent: + len_sfc_restart_position: type: u4 pos: 0xE8 - most_recent: + sfc_restart_position: pos: 0xEC type: str size: len_most_recent diff --git a/resources/templates/MapDevice/RxMapDevice.ksy b/resources/templates/MapDevice/RxMapDevice.ksy index 4183333..7bd934d 100644 --- a/resources/templates/MapDevice/RxMapDevice.ksy +++ b/resources/templates/MapDevice/RxMapDevice.ksy @@ -33,26 +33,23 @@ types: instances: valid: value: true - record_length: - pos: 0x4A - type: u4 vendor_id: - pos: 0x5C + pos: 0x02 type: u2 product_type: - pos: 0x5E + pos: 0x04 type: u2 product_code: - pos: 0x60 + pos: 0x06 type: u2 parent_module: - pos: 0x70 + pos: 0x16 type: u4 slot_no: - pos: 0x7A + pos: 0x20 type: u4 module_id: - pos: 0x7E + pos: 0x24 type: u4 v173: instances: diff --git a/setup.py b/setup.py index f5f0fc9..7287847 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ class install(_install): def run(self): subprocess.run(["ksc", "-t", "python", "--outdir", "acd/generated/", "--python-package", "acd.generated", "resources/templates/Dat/Dat.ksy"]) - subprocess.run(["ksc", "-t", "python", "--outdir", "acd/generated/comps/", "--python-package", "acd.generated.comps", "resources/templates/Comps/RxTag.ksy"]) subprocess.run( ["ksc", "-t", "python", "--outdir", "acd/generated/comps/", "--python-package", "acd.generated.comps", "resources/templates/Comps/FAFA_Comps.ksy"]) @@ -42,12 +41,8 @@ def run(self): ["ksc", "-t", "python", "--outdir", "acd/generated/comments/", "--python-package", "acd.generated.comments", "resources/templates/Comments/FAFA_Comments.ksy"]) subprocess.run( - ["ksc", "-t", "python", "--outdir", "acd/generated/controller/", "--python-package", "acd.generated.controller", - "resources/templates/Controller/RxController.ksy"]) - subprocess.run( - ["ksc", "-t", "python", "--outdir", "acd/generated/map_device/", "--python-package", - "acd.generated.map_device", - "resources/templates/MapDevice/RxMapDevice.ksy"]) + ["ksc", "-t", "python", "--outdir", "acd/generated/comps/", "--python-package", "acd.generated.comps", + "resources/templates/Comps/RxGeneric.ksy"]) _install.run(self) print("--------------------------------------------------------------------") diff --git a/test/test_api.py b/test/test_api.py index eb4b75b..5b3b5be 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,16 +1,17 @@ import os from pathlib import Path -from acd.api import ImportProjectFromFile, Project, Extract, ExtractAcdDatabase +from acd.api import ImportProjectFromFile, RSLogix5000Content, Extract, ExtractAcdDatabase from acd.export_l5x import ExportL5x from acd.l5x.elements import DumpCompsRecords +import xml.etree.ElementTree as ET def test_import_from_file(): importer = ImportProjectFromFile(Path(os.path.join("..", "resources", "CuteLogix.ACD"))) - project: Project = importer.import_project() + project: RSLogix5000Content = importer.import_project() assert project is not None @@ -20,5 +21,14 @@ def test_extract_database_files(): def test_dump_to_files(): - export = ExportL5x(Path(os.path.join("..", "resources", "CuteLogix.ACD"))) + export = ExportL5x(Path(os.path.join("..", "resources", "C05.ACD"))) DumpCompsRecords(export._cur, 0).dump(0) + + +def manual_test_to_xml(): + importer = ImportProjectFromFile(Path(os.path.join("..", "resources", "CuteLogix.ACD"))) + project: RSLogix5000Content = importer.import_project() + ss = project.to_xml() + element = ET.XML(ss) + ET.indent(element) + print(ET.tostring(element, encoding='unicode')) \ No newline at end of file