Skip to content

Commit

Permalink
Merge pull request #37 from KLBa/feature/internalize-param-nrc-const
Browse files Browse the repository at this point in the history
  • Loading branch information
andlaus authored Apr 21, 2022
2 parents 9d9bf87 + 518aacf commit 063cdc0
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 24 deletions.
Binary file modified examples/somersault.pdx
Binary file not shown.
16 changes: 9 additions & 7 deletions examples/somersaultecu.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from odxtools.parameters import ReservedParameter
from odxtools.parameters import ValueParameter
from odxtools.parameters import MatchingRequestParameter
from odxtools.parameters import NrcConstParameter

from odxtools.communicationparameter import CommunicationParameterRef

Expand Down Expand Up @@ -86,7 +87,7 @@ class SID(IntEnum):
TeamMember(id="TM.Doggy",
short_name="Doggy",
long_name="Doggy the dog",
description="Dog is man's best friend",
description="<p>Dog is man's best friend</p>",
roles=["gymnast", "tracker"],
department="sniffers",
address="Some road",
Expand All @@ -100,7 +101,7 @@ class SID(IntEnum):
TeamMember(id="TM.Horsey",
short_name="Horsey",
long_name="Horsey the horse",
description="Trustworthy worker",
description="<p>Trustworthy worker</p>",
roles=["gymnast" ],
department="haulers",
address="Some road",
Expand All @@ -120,7 +121,7 @@ class SID(IntEnum):
CompanyData(id="CD.Suncus",
short_name="Suncus",
long_name="Circus of the sun",
description="Prestigious group of performers",
description="<p>Prestigious group of performers</p>",
roles=["circus", "gym"],
team_members=NamedItemList(lambda x: x.short_name,
[
Expand All @@ -130,10 +131,10 @@ class SID(IntEnum):
company_specific_info=CompanySpecificInfo(
related_docs=[
RelatedDoc(
description="We are the best!",
description="<p>We are the best!</p>",
xdoc=XDoc(short_name="best",
long_name="suncus is the best",
description="great propaganda...",
description="<p>great propaganda...</p>",
number="1",
state="published",
date="2015-01-15T20:15:20+05:00",
Expand Down Expand Up @@ -719,10 +720,11 @@ class SID(IntEnum):
request_byte_position=0,
byte_position=1,
byte_length=1),
ValueParameter(
NrcConstParameter(
short_name="reason",
dop_ref="somersault.DOP.error_code",
diag_coded_type=somersault_diagcodedtypes["uint8"],
byte_position=2,
coded_values=[0, 1, 2]
# possible values (TODO: make this an enum parameter):
# 0 -> not sober
# 1 -> too dizzy
Expand Down
1 change: 1 addition & 0 deletions odxtools/parameters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .dynamicparameter import DynamicParameter
from .lengthkeyparameter import LengthKeyParameter
from .matchingrequestparameter import MatchingRequestParameter
from .nrcconstparameter import NrcConstParameter
from .physicalconstantparameter import PhysicalConstantParameter
from .reservedparameter import ReservedParameter
from .systemparameter import SystemParameter
Expand Down
119 changes: 119 additions & 0 deletions odxtools/parameters/nrcconstparameter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2022 MBition GmbH

from typing import List
from ..decodestate import DecodeState
from ..encodestate import EncodeState
from ..diagcodedtypes import DiagCodedType
from ..odxtypes import DataType
from ..exceptions import DecodeError, EncodeError

from .parameterbase import Parameter


class NrcConstParameter(Parameter):
"""A param of type NRC-CONST defines a set of values to be matched.
An NRC-CONST can only be used in a negative response.
Its encoding behaviour is similar to a VALUE parameter with a TEXTTABLE.
However, an NRC-CONST is used for matching a response (similar to a CODED-CONST).
See ASAM MCD-2 D (ODX), p. 77-79.
"""

def __init__(self, short_name, diag_coded_type: DiagCodedType, coded_values: List[int], **kwargs):
super().__init__(short_name,
parameter_type="NRC-CONST", **kwargs)

self._diag_coded_type = diag_coded_type
# TODO: Does it have to be an integer or is that just common practice?
assert all(isinstance(coded_value, int)
for coded_value in coded_values)
self.coded_values = coded_values

@property
def diag_coded_type(self):
return self._diag_coded_type

@property
def bit_length(self):
return self.diag_coded_type.bit_length

@property
def internal_data_type(self) -> DataType:
return self.diag_coded_type.base_data_type

def is_required(self):
return False

def is_optional(self):
return False

def get_coded_value(self):
return self.coded_value

def get_coded_value_as_bytes(self, encode_state: EncodeState):
if self.short_name in encode_state.parameter_values:
if encode_state.parameter_values[self.short_name] not in self.coded_values:
raise EncodeError(f"The parameter '{self.short_name}' must have"
f" one of the constant values {self.coded_values}")
else:
coded_value = encode_state.parameter_values[self.short_name]
else:
# If the user does not select one, just select any.
# I think it does not matter ...
coded_value = self.coded_values[0]
return self.diag_coded_type.convert_internal_to_bytes(coded_value, encode_state, bit_position=self.bit_position)

def decode_from_pdu(self, decode_state: DecodeState):
if self.byte_position is not None and self.byte_position != decode_state.next_byte_position:
# Update byte position
decode_state = decode_state._replace(
next_byte_position=self.byte_position)

# Extract coded values
coded_value, next_byte_position = self.diag_coded_type.convert_bytes_to_internal(decode_state,
bit_position=self.bit_position)

# Check if the coded value in the message is correct.
if coded_value not in self.coded_values:
raise DecodeError(
f"Coded constant parameter does not match! "
f"The parameter {self.short_name} expected a coded value in {self.coded_values} but got {coded_value} "
f"at byte position {decode_state.next_byte_position} "
f"in coded message {decode_state.coded_message.hex()}."
)

return coded_value, next_byte_position

def _as_dict(self):
d = super()._as_dict()
if self.bit_length is not None:
d["bit_length"] = self.bit_length
d["coded_values"] = self.coded_values
return d

def __repr__(self):
repr_str = f"CodedConstParameter(short_name='{self.short_name}', coded_values={self.coded_values}"
if self.long_name is not None:
repr_str += f", long_name='{self.long_name}'"
if self.byte_position is not None:
repr_str += f", byte_position='{self.byte_position}'"
if self.bit_position:
repr_str += f", bit_position='{self.bit_position}'"
if self.semantic is not None:
repr_str += f", semantic='{self.semantic}'"
repr_str += f", diag_coded_type={repr(self.diag_coded_type)}"
if self.description is not None:
repr_str += f", description='{' '.join(self.description.split())}'"
return repr_str + ")"

def __str__(self):
lines = [
super().__str__(),
]
return "\n".join(lines)

def get_description_of_valid_values(self) -> str:
"""return a human-understandable description of valid physical values"""
return f"One of the constant internal values: {self.coded_values}"
28 changes: 22 additions & 6 deletions odxtools/parameters/readparameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

from ..diagcodedtypes import read_diag_coded_type_from_odx
from ..globals import xsi
from ..utils import read_description_from_odx
from ..utils import read_element_id

from .codedconstparameter import CodedConstParameter
from .dynamicparameter import DynamicParameter
from .lengthkeyparameter import LengthKeyParameter
from .matchingrequestparameter import MatchingRequestParameter
from .nrcconstparameter import NrcConstParameter
from .physicalconstantparameter import PhysicalConstantParameter
from .reservedparameter import ReservedParameter
from .systemparameter import SystemParameter
Expand All @@ -20,14 +21,13 @@


def read_parameter_from_odx(et_element):
short_name = et_element.find("SHORT-NAME").text
element_id = read_element_id(et_element)
short_name = element_id["short_name"]
long_name = element_id.get("long_name")
description = element_id.get("description")

long_name = et_element.find(
"LONG-NAME").text if et_element.find("LONG-NAME") is not None else None
semantic = et_element.get("SEMANTIC")

description = read_description_from_odx(et_element.find("DESC"))

byte_position = int(et_element.find(
"BYTE-POSITION").text) if et_element.find("BYTE-POSITION") is not None else None
bit_position = int(et_element.find(
Expand Down Expand Up @@ -89,6 +89,21 @@ def read_parameter_from_odx(et_element):
bit_position=bit_position,
description=description)

elif parameter_type == "NRC-CONST":
diag_coded_type = read_diag_coded_type_from_odx(
et_element.find("DIAG-CODED-TYPE"))
coded_values = [diag_coded_type.base_data_type.cast_string(val.text)
for val in et_element.iterfind("CODED-VALUES/CODED-VALUE")]

return NrcConstParameter(short_name,
long_name=long_name,
semantic=semantic,
diag_coded_type=diag_coded_type,
coded_values=coded_values,
byte_position=byte_position,
bit_position=bit_position,
description=description)

elif parameter_type == "RESERVED":
bit_length = int(et_element.find("BIT-LENGTH").text)

Expand Down Expand Up @@ -152,6 +167,7 @@ def read_parameter_from_odx(et_element):
"TABLE-KEY-REF").get("ID-REF") if et_element.find("TABLE-KEY-REF") is not None else None
key_snref = et_element.find(
"TABLE-KEY-SNREF").get("SHORT-NAME") if et_element.find("TABLE-KEY-SNREF") is not None else None

return TableStructParameter(short_name=short_name,
table_key_ref=key_ref,
table_key_snref=key_snref,
Expand Down
2 changes: 1 addition & 1 deletion odxtools/pdx_stub/macros/printAdminData.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
{%- if doc_revision.tool is not none %}
<TOOL>{{doc_revision.tool|e}}</TOOL>
{%- endif %}
{%- if doc_revision.modifications is not none %}
{%- if doc_revision.modifications %}
<MODIFICATIONS>
{%- for mod in doc_revision.modifications %}
<MODIFICATION>
Expand Down
2 changes: 2 additions & 0 deletions odxtools/pdx_stub/macros/printDOP.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
{%- if dct.dct_type in ("STANDARD-LENGTH-TYPE", "LEADING-LENGTH-INFO-TYPE") %}
<BIT-LENGTH>{{dct.bit_length}}</BIT-LENGTH>
{%- else %}
{%- if dct.max_length is not none %}
<MAX-LENGTH>{{dct.max_length}}</MAX-LENGTH>
{%- endif %}
<MIN-LENGTH>{{dct.min_length}}</MIN-LENGTH>
{%- endif %}
</DIAG-CODED-TYPE>
Expand Down
6 changes: 6 additions & 0 deletions odxtools/pdx_stub/macros/printParam.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
<CODED-VALUE>{{param.coded_value}}</CODED-VALUE>
{%- elif param.physical_constant_value is defined %}
<PHYS-CONSTANT-VALUE>{{param.physical_constant_value}}</PHYS-CONSTANT-VALUE>
{%- elif param.coded_values is defined %}
<CODED-VALUES>
{%- for coded_value in param.coded_values %}
<CODED-VALUE>{{coded_value}}</CODED-VALUE>
{%- endfor %}
</CODED-VALUES>
{%- endif %}
{%- if param.dop_ref %}
<DOP-REF ID-REF="{{param.dop_ref}}"/>
Expand Down
9 changes: 7 additions & 2 deletions odxtools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@

def read_description_from_odx(et_element: ElementTree.Element):
"""Read a DESCRIPTION element. The element usually has the name DESC."""
# TODO: Invent a better representation of a DESC element.
# This just represents it as XHTML string.
if et_element is None:
return None

raw_string = ElementTree.tostring(et_element,
encoding="unicode",
method="xml")
method="xml").strip()
# Remove DESC start and end tag.
raw_string = "\n".join(raw_string.split("\n")[1:-2])
assert raw_string.startswith("<DESC>"), raw_string
assert raw_string.endswith("</DESC>"), raw_string
# Remove starting and ending tag
raw_string = raw_string[len("<DESC>"):-len("</DESC>")]
return raw_string.strip()


Expand Down
2 changes: 1 addition & 1 deletion odxtools/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2022 MBition GmbH

__version__ = '1.1.0'
__version__ = '1.2.0'
17 changes: 14 additions & 3 deletions tests/test_encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from odxtools.dataobjectproperty import DataObjectProperty
from odxtools.compumethods import LinearCompuMethod
from odxtools.diagcodedtypes import StandardLengthType
from odxtools.parameters import CodedConstParameter, ValueParameter
from odxtools.exceptions import EncodeError
from odxtools.parameters import CodedConstParameter, ValueParameter, NrcConstParameter
from odxtools.physicaltype import PhysicalType
from odxtools.structures import Request
from odxtools.structures import Request, Response

class TestEncodeRequest(unittest.TestCase):
def test_encode_coded_const_infer_order(self):
Expand All @@ -21,7 +22,7 @@ def test_encode_coded_const_infer_order(self):
req = Request("request_id", "request_sn", [param1, param2])
self.assertEqual(req.encode(), bytearray([0x7d, 0xab]))

def test_encode_coded_const_rerder(self):
def test_encode_coded_const_reorder(self):
diag_coded_type = StandardLengthType("A_UINT32", 8)
param1 = CodedConstParameter(
"param1", diag_coded_type, coded_value=0x34, byte_position=1)
Expand All @@ -48,6 +49,16 @@ def test_encode_linear(self):
bytearray([0x3]) # encode(14) = (14-8)/2 = 3
)

def test_encode_nrc_const(self):
diag_coded_type = StandardLengthType("A_UINT32", 8)
param1 = CodedConstParameter(
"param1", diag_coded_type, coded_value=0x12, byte_position=0)
param2 = NrcConstParameter(
"param2", diag_coded_type, coded_values=[0x34, 0xab], byte_position=1)
resp = Response("response_id", "response_sn", [param1, param2])
self.assertEqual(resp.encode(), bytearray([0x12, 0x34]))
self.assertEqual(resp.encode(param2=0xab), bytearray([0x12, 0xab]))
self.assertRaises(EncodeError, resp.encode, param2=0xef)

if __name__ == '__main__':
unittest.main()
38 changes: 38 additions & 0 deletions tests/test_readparameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2022 MBition GmbH

import unittest
from xml.etree import ElementTree

from odxtools.odxtypes import DataType
from odxtools.parameters import NrcConstParameter, read_parameter_from_odx


class TestReadNrcParam(unittest.TestCase):
def test_read_nrcconst_from_odx(self):
ODX = """
<PARAM SEMANTIC="SUBFUNCTION-ID" xsi:type="NRC-CONST" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SHORT-NAME>NR_identifier</SHORT-NAME>
<LONG-NAME>Identifier for the negative response</LONG-NAME>
<BYTE-POSITION>0</BYTE-POSITION>
<CODED-VALUES>
<CODED-VALUE>16</CODED-VALUE>
<CODED-VALUE>10</CODED-VALUE>
</CODED-VALUES>
<DIAG-CODED-TYPE BASE-DATA-TYPE="A_UINT32" xsi:type="STANDARD-LENGTH-TYPE">
<BIT-LENGTH>8</BIT-LENGTH>
</DIAG-CODED-TYPE>
</PARAM>
"""
root = ElementTree.fromstring(ODX)
param = read_parameter_from_odx(root)

self.assertIsInstance(param, NrcConstParameter)
self.assertEqual("SUBFUNCTION-ID", param.semantic)
self.assertEqual("NR_identifier", param.short_name)
self.assertEqual("Identifier for the negative response",
param.long_name)
self.assertEqual([16, 10], param.coded_values)
self.assertEqual(DataType.A_UINT32,
param.diag_coded_type.base_data_type)
self.assertEqual(8, param.diag_coded_type.bit_length)
Loading

0 comments on commit 063cdc0

Please sign in to comment.