diff --git a/examples/somersaultecu.py b/examples/somersaultecu.py index a26059a1..b0b13a87 100755 --- a/examples/somersaultecu.py +++ b/examples/somersaultecu.py @@ -2067,6 +2067,7 @@ class SomersaultSID(IntEnum): env_data_descs=NamedItemList(somersault_env_data_descs.values()), dtc_dops=NamedItemList(), structures=NamedItemList(), + static_fields=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), sdgs=[], @@ -2327,6 +2328,7 @@ class SomersaultSID(IntEnum): diag_data_dictionary_spec=DiagDataDictionarySpec( dtc_dops=NamedItemList(), data_object_props=NamedItemList(), + static_fields=NamedItemList(), structures=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), diff --git a/odxtools/diagdatadictionaryspec.py b/odxtools/diagdatadictionaryspec.py index b59c0520..e1a1ede5 100644 --- a/odxtools/diagdatadictionaryspec.py +++ b/odxtools/diagdatadictionaryspec.py @@ -14,11 +14,11 @@ from .environmentdata import EnvironmentData from .environmentdatadescription import EnvironmentDataDescription from .exceptions import odxraise -from .globals import logger from .multiplexer import Multiplexer from .nameditemlist import NamedItemList from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId from .specialdatagroup import SpecialDataGroup +from .staticfield import StaticField from .structure import Structure from .table import Table from .unitspec import UnitSpec @@ -33,6 +33,7 @@ class DiagDataDictionarySpec: data_object_props: NamedItemList[DataObjectProperty] structures: NamedItemList[BasicStructure] end_of_pdu_fields: NamedItemList[EndOfPduField] + static_fields: NamedItemList[StaticField] dynamic_length_fields: NamedItemList[DynamicLengthField] tables: NamedItemList[Table] env_data_descs: NamedItemList[EnvironmentDataDescription] @@ -48,6 +49,7 @@ def __post_init__(self) -> None: self.data_object_props, self.structures, self.end_of_pdu_fields, + self.static_fields, self.dynamic_length_fields, self.env_data_descs, self.env_datas, @@ -73,6 +75,11 @@ def from_et(et_element: ElementTree.Element, for eofp_element in et_element.iterfind("END-OF-PDU-FIELDS/END-OF-PDU-FIELD") ] + static_fields = [ + StaticField.from_et(dl_element, doc_frags) + for dl_element in et_element.iterfind("STATIC-FIELDS/STATIC-FIELD") + ] + dynamic_length_fields = [ DynamicLengthField.from_et(dl_element, doc_frags) for dl_element in et_element.iterfind("DYNAMIC-LENGTH-FIELDS/DYNAMIC-LENGTH-FIELD") @@ -115,25 +122,13 @@ def from_et(et_element: ElementTree.Element, else: unit_spec = None - # TODO: Parse different specs.. Which of them are needed? - for (path, name) in [ - ("STATIC-FIELDS", "static fields"), - ("DYNAMIC-LENGTH-FIELDS/DYNAMIC-LENGTH-FIELD", "dynamic length fields"), - ( - "DYNAMIC-ENDMARKER-FIELDS/DYNAMIC-ENDMARKER-FIELD", - "dynamic endmarker fields", - ), - ]: - num = len(list(et_element.iterfind(path))) - if num > 0: - logger.info(f"Not implemented: Did not parse {num} {name}.") - sdgs = create_sdgs_from_et(et_element.find("SDGS"), doc_frags) return DiagDataDictionarySpec( data_object_props=NamedItemList(data_object_props), structures=NamedItemList(structures), end_of_pdu_fields=NamedItemList(end_of_pdu_fields), + static_fields=NamedItemList(static_fields), dynamic_length_fields=NamedItemList(dynamic_length_fields), dtc_dops=NamedItemList(dtc_dops), unit_spec=unit_spec, @@ -162,6 +157,8 @@ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: odxlinks.update(sdg._build_odxlinks()) for structure in self.structures: odxlinks.update(structure._build_odxlinks()) + for static_field in self.static_fields: + odxlinks.update(static_field._build_odxlinks()) for dynamic_length_field in self.dynamic_length_fields: odxlinks.update(dynamic_length_field._build_odxlinks()) for end_of_pdu_field in self.end_of_pdu_fields: @@ -179,6 +176,8 @@ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: data_object_prop._resolve_odxlinks(odxlinks) for dtc_dop in self.dtc_dops: dtc_dop._resolve_odxlinks(odxlinks) + for static_field in self.static_fields: + static_field._resolve_odxlinks(odxlinks) for dynamic_length_field in self.dynamic_length_fields: dynamic_length_field._resolve_odxlinks(odxlinks) for end_of_pdu_field in self.end_of_pdu_fields: @@ -204,6 +203,8 @@ def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None: data_object_prop._resolve_snrefs(diag_layer) for dtc_dop in self.dtc_dops: dtc_dop._resolve_snrefs(diag_layer) + for static_field in self.static_fields: + static_field._resolve_snrefs(diag_layer) for dynamic_length_field in self.dynamic_length_fields: dynamic_length_field._resolve_snrefs(diag_layer) for end_of_pdu_field in self.end_of_pdu_fields: diff --git a/odxtools/diaglayer.py b/odxtools/diaglayer.py index a92b10b3..aaef5791 100644 --- a/odxtools/diaglayer.py +++ b/odxtools/diaglayer.py @@ -195,6 +195,7 @@ def _finalize_init(self, odxlinks: OdxLinkDatabase) -> None: data_object_props=dops, dtc_dops=dtc_dops, structures=structures, + static_fields=NamedItemList(), end_of_pdu_fields=end_of_pdu_fields, dynamic_length_fields=dynamic_length_fields, tables=tables, diff --git a/odxtools/dynamiclengthfield.py b/odxtools/dynamiclengthfield.py index 7001a0df..fc7057af 100644 --- a/odxtools/dynamiclengthfield.py +++ b/odxtools/dynamiclengthfield.py @@ -99,6 +99,7 @@ def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue: decode_state.cursor_bit_position = det_num_items.bit_position or 0 n = det_num_items.dop.decode_from_pdu(decode_state) + result: List[ParameterValue] = [] if not isinstance(n, int): odxraise(f"Number of items specified by a dynamic length field {self.short_name} " @@ -107,11 +108,10 @@ def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue: odxraise( f"Number of items specified by a dynamic length field {self.short_name} " f"must be positive (is: {n})", DecodeError) - else: - decode_state.cursor_byte_position = decode_state.origin_byte_position + self.offset - result: List[ParameterValue] = [] - for _ in range(n): - result.append(self.structure.decode_from_pdu(decode_state)) + + decode_state.cursor_byte_position = decode_state.origin_byte_position + self.offset + for _ in range(n): + result.append(self.structure.decode_from_pdu(decode_state)) decode_state.origin_byte_position = orig_origin decode_state.cursor_byte_position = max(orig_cursor, decode_state.cursor_byte_position) diff --git a/odxtools/staticfield.py b/odxtools/staticfield.py new file mode 100644 index 00000000..260b54a8 --- /dev/null +++ b/odxtools/staticfield.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: MIT +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Dict, List +from xml.etree import ElementTree + +from typing_extensions import override + +from .decodestate import DecodeState +from .encodestate import EncodeState +from .exceptions import odxassert, odxraise, odxrequire +from .field import Field +from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId +from .odxtypes import ParameterValue +from .utils import dataclass_fields_asdict + +if TYPE_CHECKING: + from .diaglayer import DiagLayer + + +@dataclass +class StaticField(Field): + """Array of a fixed number of structure objects""" + fixed_number_of_items: int + item_byte_size: int + + @staticmethod + @override + def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> "StaticField": + kwargs = dataclass_fields_asdict(Field.from_et(et_element, doc_frags)) + + fixed_number_of_items = int(odxrequire(et_element.findtext('FIXED-NUMBER-OF-ITEMS'))) + item_byte_size = int(odxrequire(et_element.findtext('ITEM-BYTE-SIZE'))) + + return StaticField( + fixed_number_of_items=fixed_number_of_items, item_byte_size=item_byte_size, **kwargs) + + @override + def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: + odxlinks = super()._build_odxlinks() + return odxlinks + + @override + def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: + super()._resolve_odxlinks(odxlinks) + + @override + def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None: + super()._resolve_snrefs(diag_layer) + + @override + def convert_physical_to_bytes( + self, + physical_value: ParameterValue, + encode_state: EncodeState, + bit_position: int = 0, + ) -> bytes: + if not isinstance(physical_value, + (tuple, list)) or len(physical_value) != self.fixed_number_of_items: + odxraise(f"Value for static field '{self.short_name}' " + f"must be a list of size {self.fixed_number_of_items}") + + result = bytearray() + for val in physical_value: + if not isinstance(val, dict): + odxraise(f"The individual parameter values for static field '{self.short_name}' " + f"must be dictionaries for structure '{self.structure.short_name}'") + + data = self.structure.convert_physical_to_bytes(val, encode_state) + + if len(data) > self.item_byte_size: + odxraise(f"Insufficient item byte size for static field {self.short_name}: " + f"Is {self.item_byte_size} bytes, but need at least {len(data)} bytes") + data = data[:self.item_byte_size] + elif len(data) < self.item_byte_size: + # add some padding bytes + data = data.ljust(self.item_byte_size, b'\x00') + + result += data + + return result + + @override + def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue: + + odxassert(decode_state.cursor_bit_position == 0, + "No bit position can be specified for static length fields!") + + result: List[ParameterValue] = [] + for _ in range(self.fixed_number_of_items): + orig_cursor = decode_state.cursor_byte_position + + if decode_state.cursor_byte_position - orig_cursor > self.item_byte_size: + odxraise(f"Insufficient item byte size for static field {self.short_name}: " + f"Is {self.item_byte_size} bytes, but need at least " + f"{decode_state.cursor_byte_position - orig_cursor} bytes") + + result.append(self.structure.decode_from_pdu(decode_state)) + + decode_state.cursor_byte_position = orig_cursor + self.item_byte_size + + return result diff --git a/odxtools/templates/macros/printStaticField.xml.jinja2 b/odxtools/templates/macros/printStaticField.xml.jinja2 new file mode 100644 index 00000000..675b04ac --- /dev/null +++ b/odxtools/templates/macros/printStaticField.xml.jinja2 @@ -0,0 +1,15 @@ +{#- -*- mode: sgml; tab-width: 1; indent-tabs-mode: nil -*- + # + # SPDX-License-Identifier: MIT +-#} + +{%- import('macros/printElementId.xml.jinja2') as peid %} + +{%- macro printStaticField(sf) -%} + + {{ peid.printElementIdSubtags(sf)|indent(1) }} + + {{sf.fixed_number_of_items}} + {{sf.item_byte_size}} + +{%- endmacro -%} diff --git a/odxtools/templates/macros/printVariant.xml.jinja2 b/odxtools/templates/macros/printVariant.xml.jinja2 index 6bdac9aa..dcfbe1d6 100644 --- a/odxtools/templates/macros/printVariant.xml.jinja2 +++ b/odxtools/templates/macros/printVariant.xml.jinja2 @@ -9,6 +9,7 @@ {%- import('macros/printFunctionalClass.xml.jinja2') as pfc %} {%- import('macros/printStructure.xml.jinja2') as pst %} {%- import('macros/printEndOfPdu.xml.jinja2') as peopdu %} +{%- import('macros/printStaticField.xml.jinja2') as psf %} {%- import('macros/printDynamicLengthField.xml.jinja2') as pdlf %} {%- import('macros/printMux.xml.jinja2') as pm %} {%- import('macros/printEnvData.xml.jinja2') as ped %} @@ -67,6 +68,13 @@ {%- endfor %} {%- endif %} + {%- if ddds.static_fields %} + + {%- for sf in ddds.static_fields %} + {{ psf.printStaticField(sf)|indent(3) }} + {%- endfor %} + + {%- endif %} {%- if ddds.dynamic_length_fields %} {%- for dlf in ddds.dynamic_length_fields %} diff --git a/tests/test_decoding.py b/tests/test_decoding.py index 62df2ace..922ca312 100644 --- a/tests/test_decoding.py +++ b/tests/test_decoding.py @@ -30,6 +30,7 @@ from odxtools.request import Request from odxtools.response import Response, ResponseType from odxtools.standardlengthtype import StandardLengthType +from odxtools.staticfield import StaticField from odxtools.structure import Structure doc_frags = [OdxDocFragment("UnitTest", "WinneThePoh")] @@ -654,6 +655,7 @@ def test_decode_request_structure(self) -> None: dtc_dops=NamedItemList(), data_object_props=NamedItemList([dop]), structures=NamedItemList([struct]), + static_fields=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), tables=NamedItemList(), @@ -702,8 +704,221 @@ def test_decode_request_structure(self) -> None: self.assertEqual(expected_message.coding_object, decoded_message.coding_object) self.assertEqual(expected_message.param_dict, decoded_message.param_dict) + def test_static_field_coding(self) -> None: + """Test en- and decoding of static fields.""" + diag_coded_type = StandardLengthType( + base_data_type=DataType.A_UINT32, + base_type_encoding=None, + bit_length=8, + bit_mask=None, + is_condensed_raw=None, + is_highlow_byte_order_raw=None, + ) + + compu_method = IdenticalCompuMethod( + internal_type=DataType.A_INT32, physical_type=DataType.A_INT32) + dop = DataObjectProperty( + odx_id=OdxLinkId("static_field.dop.id", doc_frags), + short_name="static_field_dop_sn", + long_name=None, + description=None, + admin_data=None, + diag_coded_type=diag_coded_type, + physical_type=PhysicalType(DataType.A_UINT32, display_radix=None, precision=None), + compu_method=compu_method, + unit_ref=None, + sdgs=[], + internal_constr=None, + physical_constr=None, + ) + + req_param1 = CodedConstParameter( + short_name="SID", + long_name=None, + description=None, + semantic=None, + diag_coded_type=diag_coded_type, + coded_value=0x12, + byte_position=0, + bit_position=None, + sdgs=[], + ) + + struct_param1 = ValueParameter( + short_name="static_field_struct_param_1", + long_name=None, + description=None, + semantic=None, + dop_ref=OdxLinkRef.from_id(dop.odx_id), + dop_snref=None, + physical_default_value_raw=None, + byte_position=None, + bit_position=None, + sdgs=[], + ) + struct_param2 = ValueParameter( + short_name="static_field_struct_param_2", + long_name=None, + description=None, + semantic=None, + dop_ref=OdxLinkRef.from_id(dop.odx_id), + dop_snref=None, + physical_default_value_raw=None, + byte_position=None, + bit_position=None, + sdgs=[], + ) + struct = Structure( + odx_id=OdxLinkId("static_field_struct.id", doc_frags), + short_name="static_field_struct", + long_name=None, + description=None, + admin_data=None, + sdgs=[], + parameters=NamedItemList([struct_param1, struct_param2]), + byte_size=None, + ) + static_field = StaticField( + odx_id=OdxLinkId("static_field.id", doc_frags), + short_name="static_field_sn", + long_name=None, + description=None, + admin_data=None, + sdgs=[], + structure_ref=OdxLinkRef.from_id(struct.odx_id), + structure_snref=None, + env_data_desc_ref=None, + env_data_desc_snref=None, + is_visible_raw=True, + fixed_number_of_items=2, + item_byte_size=3, + ) + req_param2 = ValueParameter( + short_name="static_field_param", + long_name=None, + description=None, + semantic=None, + dop_ref=OdxLinkRef.from_id(static_field.odx_id), + dop_snref=None, + physical_default_value_raw=None, + byte_position=None, + bit_position=None, + sdgs=[], + ) + + req = Request( + odx_id=OdxLinkId("static_field.request.id", doc_frags), + short_name="static_field_request_sn", + long_name=None, + description=None, + admin_data=None, + sdgs=[], + parameters=NamedItemList([req_param1, req_param2]), + byte_size=None, + ) + service = DiagService( + odx_id=OdxLinkId("static_field.service.id", doc_frags), + short_name="static_field_service_sn", + long_name=None, + description=None, + protocol_snrefs=[], + related_diag_comm_refs=[], + diagnostic_class=None, + is_mandatory_raw=None, + is_executable_raw=None, + is_final_raw=None, + admin_data=None, + semantic=None, + comparam_refs=[], + is_cyclic_raw=None, + is_multiple_raw=None, + addressing_raw=None, + transmission_mode_raw=None, + audience=None, + functional_class_refs=[], + pre_condition_state_refs=[], + state_transition_refs=[], + request_ref=OdxLinkRef.from_id(req.odx_id), + pos_response_refs=[], + neg_response_refs=[], + sdgs=[], + ) + diag_layer_raw = DiagLayerRaw( + variant_type=DiagLayerType.BASE_VARIANT, + odx_id=OdxLinkId("dl.id", doc_frags), + short_name="dl_sn", + long_name=None, + description=None, + admin_data=None, + company_datas=NamedItemList(), + functional_classes=NamedItemList(), + diag_data_dictionary_spec=DiagDataDictionarySpec( + dtc_dops=NamedItemList(), + data_object_props=NamedItemList([dop]), + structures=NamedItemList([struct]), + end_of_pdu_fields=NamedItemList(), + dynamic_length_fields=NamedItemList(), + static_fields=NamedItemList([static_field]), + tables=NamedItemList(), + env_data_descs=NamedItemList(), + env_datas=NamedItemList(), + muxs=NamedItemList(), + unit_spec=None, + sdgs=[]), + diag_comms=[service], + requests=NamedItemList([req]), + positive_responses=NamedItemList(), + negative_responses=NamedItemList(), + global_negative_responses=NamedItemList(), + additional_audiences=NamedItemList(), + import_refs=[], + state_charts=NamedItemList(), + sdgs=[], + parent_refs=[], + comparams=[], + ecu_variant_patterns=[], + comparam_spec_ref=None, + prot_stack_snref=None, + ) + diag_layer = DiagLayer(diag_layer_raw=diag_layer_raw) + odxlinks = OdxLinkDatabase() + odxlinks.update(diag_layer._build_odxlinks()) + diag_layer._resolve_odxlinks(odxlinks) + diag_layer._finalize_init(odxlinks) + + expected_message = Message( + coded_message=bytes([0x12, 0x34, 0x56, 0x00, 0x78, 0x9a, 0x00]), + service=service, + coding_object=req, + param_dict={ + "SID": + 0x12, + "static_field_param": [ + { + "static_field_struct_param_1": 0x34, + "static_field_struct_param_2": 0x56 + }, + { + "static_field_struct_param_1": 0x78, + "static_field_struct_param_2": 0x9a + }, + ], + }, + ) + + # test encoding + encoded_message = diag_layer.services.static_field_service_sn(**expected_message.param_dict) + self.assertEqual(encoded_message, expected_message.coded_message) + + # test decoding + decoded_message = diag_layer.decode(expected_message.coded_message)[0] + self.assertEqual(expected_message.coded_message, decoded_message.coded_message) + self.assertEqual(expected_message.service, decoded_message.service) + self.assertEqual(expected_message.coding_object, decoded_message.coding_object) + self.assertEqual(expected_message.param_dict, decoded_message.param_dict) + def test_dynamic_length_field_coding(self) -> None: - """Test en- and decoding of a dynamic length fields.""" + """Test en- and decoding of dynamic length fields.""" diag_coded_type = StandardLengthType( base_data_type=DataType.A_UINT32, base_type_encoding=None, @@ -865,6 +1080,7 @@ def test_dynamic_length_field_coding(self) -> None: structures=NamedItemList([struct]), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList([dlf]), + static_fields=NamedItemList(), tables=NamedItemList(), env_data_descs=NamedItemList(), env_datas=NamedItemList(), @@ -1086,6 +1302,7 @@ def test_decode_request_end_of_pdu_field(self) -> None: dtc_dops=NamedItemList(), data_object_props=NamedItemList([dop]), structures=NamedItemList([struct]), + static_fields=NamedItemList(), end_of_pdu_fields=NamedItemList([eopf]), dynamic_length_fields=NamedItemList(), tables=NamedItemList(), @@ -1247,6 +1464,7 @@ def test_decode_request_linear_compu_method(self) -> None: dtc_dops=NamedItemList(), data_object_props=NamedItemList([dop]), structures=NamedItemList(), + static_fields=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), tables=NamedItemList(), diff --git a/tests/test_diag_coded_types.py b/tests/test_diag_coded_types.py index e11e01cb..6ff0de34 100644 --- a/tests/test_diag_coded_types.py +++ b/tests/test_diag_coded_types.py @@ -249,6 +249,7 @@ def test_end_to_end(self) -> None: data_object_props=NamedItemList(dops.values()), dtc_dops=NamedItemList(), structures=NamedItemList(), + static_fields=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), tables=NamedItemList(), @@ -548,6 +549,7 @@ def test_end_to_end(self) -> None: data_object_props=NamedItemList(dops.values()), dtc_dops=NamedItemList(), structures=NamedItemList(), + static_fields=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), tables=NamedItemList(), @@ -854,6 +856,7 @@ def test_end_to_end(self) -> None: data_object_props=NamedItemList(dops.values()), dtc_dops=NamedItemList(), structures=NamedItemList(), + static_fields=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), tables=NamedItemList(), diff --git a/tests/test_diag_data_dictionary_spec.py b/tests/test_diag_data_dictionary_spec.py index 0ca265cc..f9d06542 100644 --- a/tests/test_diag_data_dictionary_spec.py +++ b/tests/test_diag_data_dictionary_spec.py @@ -275,6 +275,7 @@ def test_initialization(self) -> None: env_datas=NamedItemList([env_data]), muxs=NamedItemList([mux]), structures=NamedItemList(), + static_fields=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), unit_spec=None, diff --git a/tests/test_unit_spec.py b/tests/test_unit_spec.py index 6f478517..67772529 100644 --- a/tests/test_unit_spec.py +++ b/tests/test_unit_spec.py @@ -143,6 +143,7 @@ def test_resolve_odxlinks(self) -> None: structures=NamedItemList(), end_of_pdu_fields=NamedItemList(), dynamic_length_fields=NamedItemList(), + static_fields=NamedItemList(), tables=NamedItemList(), env_data_descs=NamedItemList(), env_datas=NamedItemList(),