From 4f172205c60925f746cd657f98ee0d9c86d49aa9 Mon Sep 17 00:00:00 2001 From: Andreas Lauser Date: Fri, 26 Apr 2024 08:53:07 +0200 Subject: [PATCH 1/5] improve detection of overlapping parameters during encoding this code should now be bullet-proof: instead of complaing only if at least of the bits which are supposed to be set by a parameter in the coded message is already set, used bits are now explicitly tracked via a bit mask in the `EncodeState`. As a result, if e.g. an all-zero integer parameter overrides another all-zero integer parameer we will now get an overlapping objects warning as we are supposed to. Signed-off-by: Andreas Lauser Signed-off-by: Alexander Walz --- odxtools/basicstructure.py | 4 +- odxtools/dataobjectproperty.py | 4 +- odxtools/encodestate.py | 124 +++++++++++++++++++++--------- odxtools/leadinglengthinfotype.py | 2 + odxtools/minmaxlengthtype.py | 1 + odxtools/parameterinfo.py | 2 +- odxtools/paramlengthinfotype.py | 1 + odxtools/standardlengthtype.py | 45 +++++++++-- tests/test_encoding.py | 18 +++-- 9 files changed, 148 insertions(+), 53 deletions(-) diff --git a/odxtools/basicstructure.py b/odxtools/basicstructure.py index f558a00d..335ddbb7 100644 --- a/odxtools/basicstructure.py +++ b/odxtools/basicstructure.py @@ -208,7 +208,9 @@ def encode_into_pdu(self, physical_value: Optional[ParameterValue], # position directly after the structure and let # EncodeState add the padding as needed. encode_state.cursor_byte_position = encode_state.origin_byte_position + self.byte_size - encode_state.emplace_bytes(b'', "") + # Padding bytes needed. these count as "used". + encode_state.coded_message += b"\x00" * (self.byte_size - actual_len) + encode_state.used_mask += b"\xff" * (self.byte_size - actual_len) # encode the length- and table keys. This cannot be done above # because we allow these to be defined implicitly (i.e. they diff --git a/odxtools/dataobjectproperty.py b/odxtools/dataobjectproperty.py index 3dceaa76..608f39e2 100644 --- a/odxtools/dataobjectproperty.py +++ b/odxtools/dataobjectproperty.py @@ -129,8 +129,8 @@ def encode_into_pdu(self, physical_value: ParameterValue, encode_state: EncodeSt f"The value {repr(physical_value)} of type {type(physical_value).__name__}" f" is not a valid.") - internal_val = self.convert_physical_to_internal(physical_value) - self.diag_coded_type.encode_into_pdu(internal_val, encode_state) + internal_value = self.convert_physical_to_internal(physical_value) + self.diag_coded_type.encode_into_pdu(internal_value, encode_state) def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue: """ diff --git a/odxtools/encodestate.py b/odxtools/encodestate.py index 1ba58d21..0eb8dffe 100644 --- a/odxtools/encodestate.py +++ b/odxtools/encodestate.py @@ -20,6 +20,9 @@ class EncodeState: #: payload that has been constructed so far coded_message: bytearray + #: the bits of the payload that are used + used_mask: bytearray = field(default_factory=bytearray) + #: The absolute position in bytes from the beginning of the PDU to #: which relative positions refer to, e.g., the beginning of the #: structure. @@ -53,6 +56,18 @@ class EncodeState: #: (needed for MinMaxLengthType, EndOfPduField, etc.) is_end_of_pdu: bool = True + def __post_init__(self) -> None: + # if a coded message has been specified, but no used_mask, we + # assume that all of the bits of the coded message are + # currently used. + if len(self.coded_message) > len(self.used_mask): + self.used_mask += b'\xff' * (len(self.coded_message) - len(self.used_mask)) + if len(self.coded_message) < len(self.used_mask): + odxraise(f"The specified bit mask 0x{self.used_mask.hex()} for used bits " + f"is not suitable for representing the coded_message " + f"0x{self.coded_message.hex()}") + self.used_mask = self.used_mask[:len(self.coded_message)] + def emplace_atomic_value( self, *, @@ -60,19 +75,22 @@ def emplace_atomic_value( bit_length: int, base_data_type: DataType, is_highlow_byte_order: bool, + used_mask: Optional[bytes], ) -> None: """Convert the internal_value to bytes and emplace this into the PDU""" + + raw_value: AtomicOdxType + # Check that bytes and strings actually fit into the bit length if base_data_type == DataType.A_BYTEFIELD: - if isinstance(internal_value, bytearray): - internal_value = bytes(internal_value) if not isinstance(internal_value, bytes): odxraise() if 8 * len(internal_value) > bit_length: raise EncodeError(f"The bytefield {internal_value.hex()} is too large " f"({len(internal_value)} bytes)." f" The maximum length is {bit_length//8}.") - if base_data_type == DataType.A_ASCIISTRING: + raw_value = internal_value + elif base_data_type == DataType.A_ASCIISTRING: if not isinstance(internal_value, str): odxraise() @@ -80,18 +98,18 @@ def emplace_atomic_value( # But in practice, vendors use iso-8859-1, aka latin-1 # reason being iso-8859-1 never fails since it has a valid # character mapping for every possible byte sequence. - internal_value = internal_value.encode("iso-8859-1") + raw_value = internal_value.encode("iso-8859-1") - if 8 * len(internal_value) > bit_length: + if 8 * len(raw_value) > bit_length: raise EncodeError(f"The string {repr(internal_value)} is too large." f" The maximum number of characters is {bit_length//8}.") elif base_data_type == DataType.A_UTF8STRING: if not isinstance(internal_value, str): odxraise() - internal_value = internal_value.encode("utf-8") + raw_value = internal_value.encode("utf-8") - if 8 * len(internal_value) > bit_length: + if 8 * len(raw_value) > bit_length: raise EncodeError(f"The string {repr(internal_value)} is too large." f" The maximum number of bytes is {bit_length//8}.") @@ -100,11 +118,13 @@ def emplace_atomic_value( odxraise() text_encoding = "utf-16-be" if is_highlow_byte_order else "utf-16-le" - internal_value = internal_value.encode(text_encoding) + raw_value = internal_value.encode(text_encoding) - if 8 * len(internal_value) > bit_length: + if 8 * len(raw_value) > bit_length: raise EncodeError(f"The string {repr(internal_value)} is too large." f" The maximum number of characters is {bit_length//16}.") + else: + raw_value = internal_value # If the bit length is zero, return empty bytes if bit_length == 0: @@ -125,46 +145,78 @@ def emplace_atomic_value( left_pad = f"p{padding}" if padding > 0 else "" # actually encode the value - coded = bitstruct.pack(f"{left_pad}{char}{bit_length}", internal_value) - - # apply byte order for numeric objects + coded = bitstruct.pack(f"{left_pad}{char}{bit_length}", raw_value) + + # create the raw mask of used bits for numeric objects + used_mask_raw = used_mask + if base_data_type in [DataType.A_INT32, DataType.A_UINT32 + ] and (self.cursor_bit_position != 0 or + (self.cursor_bit_position + bit_length) % 8 != 0): + if used_mask is None: + tmp = (1 << bit_length) - 1 + else: + tmp = int.from_bytes(used_mask, "big") + tmp <<= self.cursor_bit_position + + used_mask_raw = tmp.to_bytes((self.cursor_bit_position + bit_length + 7) // 8, "big") + + # apply byte order to numeric objects if not is_highlow_byte_order and base_data_type in [ - DataType.A_INT32, - DataType.A_UINT32, - DataType.A_FLOAT32, - DataType.A_FLOAT64, + DataType.A_INT32, DataType.A_UINT32, DataType.A_FLOAT32, DataType.A_FLOAT64 ]: coded = coded[::-1] - self.emplace_bytes(coded) + if used_mask_raw is not None: + used_mask_raw = used_mask_raw[::-1] + + self.cursor_bit_position = 0 + self.emplace_bytes(coded, obj_used_mask=used_mask_raw) + + def emplace_bytes(self, + new_data: bytes, + obj_name: Optional[str] = None, + obj_used_mask: Optional[bytes] = None) -> None: + if self.cursor_bit_position != 0: + odxraise("EncodeState.emplace_bytes can only be called " + "for a bit position of 0!", RuntimeError) - def emplace_bytes(self, new_data: bytes, obj_name: Optional[str] = None) -> None: pos = self.cursor_byte_position # Make blob longer if necessary min_length = pos + len(new_data) if len(self.coded_message) < min_length: - self.coded_message.extend([0] * (min_length - len(self.coded_message))) - - for i in range(len(new_data)): - # insert new byte. this is pretty hacky: it will fail if - # the value to be inserted is bitwise "disjoint" from the - # value which is already in the PDU... - if self.coded_message[pos + i] & new_data[i] != 0: - if obj_name is not None: - warnings.warn( - f"'{obj_name}' overlaps with another object (bits to be set are already set)", - OdxWarning, - stacklevel=1, - ) - else: + pad = b'\x00' * (min_length - len(self.coded_message)) + self.coded_message += pad + self.used_mask += pad + + if obj_used_mask is None: + # Happy path for when no obj_used_mask has been + # specified. In this case we assume that all bits of the + # new data to be emplaced are used. + n = len(new_data) + + if self.used_mask[pos:pos + n] != b'\x00' * n: + warnings.warn( + f"Overlapping objects detected in between bytes {pos} and " + f"{pos+n}", + OdxWarning, + stacklevel=1, + ) + self.coded_message[pos:pos + n] = new_data + self.used_mask[pos:pos + n] = b'\xff' * n + else: + # insert data the hard way, i.e. we have to look at each + # individual byte to determine if it has already been used + # somewhere else (it would be nice if bytearrays supported + # bitwise operations!) + for i in range(len(new_data)): + if self.used_mask[pos + i] & obj_used_mask[i] != 0: warnings.warn( - "Object overlap (bits to be set are already set)", + "Overlapping objects detected", OdxWarning, stacklevel=1, ) - - self.coded_message[pos + i] |= new_data[i] + self.coded_message[pos + i] |= new_data[i] + self.used_mask[pos + i] |= obj_used_mask[i] self.cursor_byte_position += len(new_data) - self.cursor_bit_position = 0 diff --git a/odxtools/leadinglengthinfotype.py b/odxtools/leadinglengthinfotype.py index 70d1b3ec..d702539f 100644 --- a/odxtools/leadinglengthinfotype.py +++ b/odxtools/leadinglengthinfotype.py @@ -57,6 +57,7 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta encode_state.emplace_atomic_value( internal_value=byte_length, + used_mask=None, bit_length=self.bit_length, base_data_type=DataType.A_UINT32, is_highlow_byte_order=self.is_highlow_byte_order, @@ -64,6 +65,7 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta encode_state.emplace_atomic_value( internal_value=internal_value, + used_mask=None, bit_length=8 * byte_length, base_data_type=self.base_data_type, is_highlow_byte_order=self.is_highlow_byte_order, diff --git a/odxtools/minmaxlengthtype.py b/odxtools/minmaxlengthtype.py index 39027c9e..83688c3d 100644 --- a/odxtools/minmaxlengthtype.py +++ b/odxtools/minmaxlengthtype.py @@ -68,6 +68,7 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta orig_cursor = encode_state.cursor_byte_position encode_state.emplace_atomic_value( internal_value=internal_value, + used_mask=None, bit_length=8 * data_length, base_data_type=self.base_data_type, is_highlow_byte_order=self.is_highlow_byte_order, diff --git a/odxtools/parameterinfo.py b/odxtools/parameterinfo.py index 5135c93c..d22bc2be 100644 --- a/odxtools/parameterinfo.py +++ b/odxtools/parameterinfo.py @@ -37,7 +37,7 @@ def parameter_info(param_list: Iterable[Parameter], quoted_names: bool = False) of.write(f"{q}{param.short_name}{q}: \n") continue elif isinstance(param, NrcConstParameter): - of.write(f"{q}{param.short_name}{q}: NRC_const; choices = {param.coded_values}\n") + of.write(f"{q}{param.short_name}{q}: const; choices = {param.coded_values}\n") continue elif isinstance(param, ReservedParameter): of.write(f"{q}{param.short_name}{q}: \n") diff --git a/odxtools/paramlengthinfotype.py b/odxtools/paramlengthinfotype.py index c7e4c1cd..1056249e 100644 --- a/odxtools/paramlengthinfotype.py +++ b/odxtools/paramlengthinfotype.py @@ -81,6 +81,7 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta encode_state.emplace_atomic_value( internal_value=internal_value, + used_mask=None, bit_length=bit_length, base_data_type=self.base_data_type, is_highlow_byte_order=self.is_highlow_byte_order, diff --git a/odxtools/standardlengthtype.py b/odxtools/standardlengthtype.py index 273b1820..8b7373a0 100644 --- a/odxtools/standardlengthtype.py +++ b/odxtools/standardlengthtype.py @@ -1,13 +1,13 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass -from typing import Optional +from typing import Literal, Optional from typing_extensions import override from .decodestate import DecodeState from .diagcodedtype import DctType, DiagCodedType from .encodestate import EncodeState -from .exceptions import odxassert, odxraise +from .exceptions import odxassert, odxraise, odxrequire from .odxtypes import AtomicOdxType, DataType @@ -22,6 +22,10 @@ class StandardLengthType(DiagCodedType): def dct_type(self) -> DctType: return "STANDARD-LENGTH-TYPE" + @property + def is_condensed(self) -> bool: + return self.is_condensed_raw is True + def __post_init__(self) -> None: if self.bit_mask is not None: maskable_types = (DataType.A_UINT32, DataType.A_INT32, DataType.A_BYTEFIELD) @@ -30,10 +34,41 @@ def __post_init__(self) -> None: 'Can not apply a bit_mask on a value of type {self.base_data_type}', ) + def __get_raw_mask(self, internal_value: AtomicOdxType) -> Optional[bytes]: + """Returns a byte field where all bits that are used by the + DiagCoded type are set and all unused ones are not set. + + If `None` is returned, all bits are used. + """ + if self.bit_mask is None: + return None + + if self.is_condensed: + odxraise("Condensed bit masks are not yet supported", NotImplementedError) + return + + endianness: Literal["little", "big"] = "big" + if not self.is_highlow_byte_order and self.base_data_type in [ + DataType.A_INT32, DataType.A_UINT32, DataType.A_FLOAT32, DataType.A_FLOAT64 + ]: + # TODO (?): Technically, little endian A_UNICODE2STRING + # objects require a byte swap for each 16 bit letter, and + # thus also for the mask. I somehow doubt that this has + # been anticipated by the standard, though... + endianness = "little" + + sz: int + if isinstance(internal_value, (bytes, bytearray)): + sz = len(internal_value) + else: + sz = (odxrequire(self.get_static_bit_length()) + 7) // 8 + + return self.bit_mask.to_bytes(sz, endianness) + def __apply_mask(self, internal_value: AtomicOdxType) -> AtomicOdxType: if self.bit_mask is None: return internal_value - if self.is_condensed_raw is True: + if self.is_condensed: odxraise("Serialization of condensed bit mask is not supported", NotImplementedError) return if isinstance(internal_value, int): @@ -53,10 +88,10 @@ def get_static_bit_length(self) -> Optional[int]: def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeState) -> None: encode_state.emplace_atomic_value( internal_value=self.__apply_mask(internal_value), + used_mask=self.__get_raw_mask(internal_value), bit_length=self.bit_length, base_data_type=self.base_data_type, - is_highlow_byte_order=self.is_highlow_byte_order, - ) + is_highlow_byte_order=self.is_highlow_byte_order) @override def decode_from_pdu(self, decode_state: DecodeState) -> AtomicOdxType: diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 8eebe829..6a4c217e 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -319,7 +319,7 @@ def test_encode_overlapping(self) -> None: parameters=NamedItemList([param1, param2, param3]), byte_size=None, ) - self.assertEqual(req.encode(), bytearray([0x12, 0x34, 0x56])) + self.assertEqual(req.encode().hex(), "123456") self.assertEqual(req.get_static_bit_length(), 24) def _create_request(self, parameters: List[Parameter]) -> Request: @@ -336,12 +336,12 @@ def _create_request(self, parameters: List[Parameter]) -> Request: def test_bit_mask(self) -> None: inner_dct = StandardLengthType( - bit_mask=0x0ff0, + bit_mask=0x3fc, base_data_type=DataType.A_UINT32, base_type_encoding=None, is_highlow_byte_order_raw=None, is_condensed_raw=None, - bit_length=16) + bit_length=14) outer_dct = StandardLengthType( bit_mask=0xf00f, base_data_type=DataType.A_UINT32, @@ -395,7 +395,7 @@ def test_bit_mask(self) -> None: long_name=None, description=None, byte_position=0, - bit_position=None, + bit_position=2, dop_ref=OdxLinkRef.from_id(inner_dop.odx_id), dop_snref=None, semantic=None, @@ -421,11 +421,13 @@ def test_bit_mask(self) -> None: req = self._create_request([inner_param, outer_param]) - self.assertEqual(req.encode(inner_param=0x1111, outer_param=0x2222).hex(), "2112") + # the bit shifts here stem from the fact that we placed the + # inner parameter at bit position 2... + self.assertEqual(req.encode(inner_param=0x1234 >> 2, outer_param=0x4568).hex(), "4238") self.assertEqual( - req.decode(bytes.fromhex('1234')), { - "inner_param": 0x0230, - "outer_param": 0x1004 + req.decode(bytes.fromhex('abcd')), { + "inner_param": (0xbc << 2), + "outer_param": 0xa00d }) From 6a83b80a3d97ea8cb0ced25293d1b3fc40f752fc Mon Sep 17 00:00:00 2001 From: Andreas Lauser Date: Fri, 26 Apr 2024 09:23:43 +0200 Subject: [PATCH 2/5] somersaultecu: avoid pesky "overlapping data" warnings in request for the tester_present service so far, this fell though the cracks because "status" is a a zero const parameter... Signed-off-by: Andreas Lauser Signed-off-by: Alexander Walz --- examples/somersault.pdx | Bin 25058 -> 25067 bytes examples/somersaultecu.py | 21 +++++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/examples/somersault.pdx b/examples/somersault.pdx index 61e123984f4bd3bfdbab8f8875e703d10bab895a..9badca4424043942cd82f2c122033bfe65e26938 100644 GIT binary patch literal 25067 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

>1dVusJJReUj(5>hK zftXM$&#Gp$5|#8RX@k%H)&n%S9FB~@n zGbXG^noSrZwlt9d4rP1883_5SnZu-PiOdH9oBbax1i$)7i4A^Q^9{Gf)>lGn4dzoD z?NwF?$CuLT-y&-iPA7YM_YdJoIM1?e&@;XZHps=gaN%1z2*_ueXm6nJYBjhESfMZT zS`^3hU5W+AID^EC+>JsW4Y~XI^q6fy%}@VqB5RqyCqNh$ry!*wc`wegPP$yV5mu}T zt-M~7%}banT|oYiQJBxIYgNDuLUREqvhlwd2AqTVfIF2~0y}4s1eE4FCbq5YyHV@e`nck%aRve!M%W#;R3`#gPJAHD1+5NiLycRfefPMXD@;LR4?*W}0 zT?U!jUCsPI5r8rUO5_z9L`KwcRDK^y(kNATDEhtIoQq#m!SG@a*zfN;)u4z zcdn@YyO~cB+!X|6G1AHF1Hk2<*I$K|RT_q79PQ%fxll-(fa@0?^9oE>4*1lL;-nqt z<&ZRXkahDpDjU%lq;o(z)=W0-m(zX9LkymHYc7{2ovAm+U3tZNuEF9eI$_$V*q*@EVPB|0vlvV)86KDj%@oK7_C7Mu=*xFK=Jjzq1b{^AF_)sV4X{ zj=?)E{b8Vg)p4lYy8u)EoI26I>C)AFzM zQU0V^tk}%X2d-%G9ryM%NGf84z{~g5NV6B^<{i51oJXSuzTdiH$Z7!@w%dX_MAuYw z+E*#wBrd^#rP`SP%>QoTySf}Tv6~uR@gFu0&YrRfuJ0o$7~6r^qgv)h(Q3i(K)$iW zn8d9^k@+$o!huUUGOs9y*ISF`tl4D$^%I!RyA(RehY%Nfh&v!^MHG3cu$fQl)1Qot z>Q~+;zYb{oZ{ZHV-V%9^FJQTlTpD%AcHq|ym>GILus7_$dg;1@=vX@Ch^+|3UCKdS zVbOzn+);YoX$rk{1}$N&n-FIxti+${aD!&IrF25k3+&ypKioL<5_I>RZk&_36VwQ% z3&8Wo4)M=BRYF6;3zHV#T22!5jX1!BCas}*F6M~cf4-UoF`T^*I8*<6ZQ{m$bt@wB z7k@Ye3T2{$f!TIqq9n@m5M)xjLGWc?E%J3mcGdC1uTIrf-pa>#7%RrX)9JlSM9O?v zhfl7U`E=-pTFsZGj{;MH!L4&$^OGrqwPql5_(Kq zc3Nc=#K`w0|J<1(M|LXW^B3~(J4gYG37uizD_Y_;Hi3*S?{XL5S+qUNB()$`JfmUI zF$$}XauRPW4OtT;KWC+jy>`(E&fhS?4X%~&!5HyV=Q^}K5SUFW#Wa3ExY|tN{6end zx)dOnikj~+seHnmEj?%utvsbi+abt0OXyiN{S4xLYOL=kO57fKgfe+K2p9(EKXa+o z;G^N8(woS}v7(Z3=@+|9_cv+u?DI&BJbI{!AJ4m{8AxR8&DO}0OlV5HDcX)xaA@xw zy;{deb~Y8vC}FU0$|<(rX-DVb0n%i-e_^?n^z=P~EFiPM0C8GvmW^-24!dquFYmi= z+KvDb%Cv&hP%kIOx~r^O5rjvZI!Z2Wcz+pfRU4;28rxiSF8|>s^J8Xh?(SKBSQS~o zkhR$-A0f(V=SUG_>(LhLgP+!C0g5*iY4me>9>dbVuz5}lt{#Tvb@f*Z;dfco#?OUh zMvam3%6tF@T94&eyTq3wfCWRxb8BgjGBRdU5htUcg?gFkp5YGLhDO6yul$C38Y^ie=)7Ip&z$&Z57CXsGL_DcL-7GyP zqtVg|P$Z;Tg z1yMe613&{x>w=w^3&+!^(#KrBF`&~A)mpwW)xQxsuJR&>@wZVPBgjr}< z)x1OYh@Rj_^NAkIca9k{3zcrz&LNT?cHB%yI>{&~8wlq$!5UVMiSXTc!7O2W* zNlm$eLy;)I^NBFQx1vT&Qb3}nkfI&~OMYHSb&fBlZL`+2s{p}~N$o0EzXx5@az*r6 z{S7oq`U|ZAyC4|fwYu-!(9fCy+v1v1DRXQMJ{pKFg6T!?R%0PsJi24p8rP2|1xPX?9mB&KMA$&We4ory zGsfb9!|kz&hHVbMk5U&iq&W2lm~#&1CIf;Z*FFFprKj#L8H%r6h~Z#YBVod6YqMLP zmDY{O$w+QtN)mJ(Rl{WnI-*3h2EQjtvLY8>HHGGCVr`_JF`%9_hM=E!7VXT*l3b_L zI(r+hUBIaxCWxkA%`^gseE@u*3i|ZP3$S!i$AYAxL|J*V#Q~0+#cXMZN^yXj8M*B5 zh1-d#1uT`=5)M|I?}|(8(c*9E62YLJsn(f$EhwQbDLmHepJVA8-rr>L3z$ zFYkBL>FC9FYb{z2TL$`3ZqaG{*LViv=t6CQL}WX7vB{sfiDEz!ZXZAHE9R3Uyjcob zuVAGxnb$lRC?n@s_&X(E1gtl);-6=C@^OMX2wY-NFO>ILju&I5->E%Dq(P@TLwFIP zKhE~!C%Xs@nEo8cp!?}tLtrORo-4!xINlEyla&f0-=*Z=GIG*~8$~Z7`7oH#h4Kip z*y8|I=OZfxkMa(f(ZzuzJc*Dz3H~J7v6^m@TSv-O4(Ozo#Y!G04-aYXG_})K4qH8` zbWb_El!`c=$UO}j?{(T6uq4teATfb#U3h{6CD0%J46q$u#ok!qJB;;Y!FOb@;@n)f z;)9p^q`p!8o5Z`U&2Z;?4jv@6dmk4gWm2z^T-~A4wEHI@xDBr&hOr5o0&_1!$-%q>;Ez=Ecy%MTi zq<|Z7?o4G~uQbkvJ7>OyU-$gbADaxBhe=7X1k=$(+E_3Xf!YSL7oy>6Tn_zSS*K{( z_Xm!ugjAFD0ori+o@3FEguvFdAgl)-jXZ0OWE!k%g(n5$Mt)#~4#sXxJmhZ6oGFFP zEbPYpxxqyJ_WDGhV7E1dlIPsQ!j%l0X_3FkX5DXzAmSz|e&@g~zt{dZmK4z5nocEguVpJ5F(MwVI2FHrw3wvd~)9Qw{Ryy8eZWNMSRCTj>sl zAzO#djAY_4_ln}6h}@wY=5^s9xYo%caDyZR#Oo%&J18n*kdB@u$tjq(&f_10tcf?_ z{OD@sgL=?0Go@9r?66<$$)RK$Fr7Z1HywN-?#60)H{A;A)YO|2#iqur!yJ0QH5!_%YY^ILd=97 ziIXbtwP<`Z`MFIv(rVmVbS&?7PhKw+S8Y31Pc>6rXqI|>%y#>0%``b)TjlE9r1?lc zRa3%w${`OA)YHK(=8ctb&s?zt*-K3fNB+^~3b(Nx!?$M81LvreNHiAC*(9QAM20(d zRXU5ThIgEn(Tr|$Zz1d{C#sEO-I4LMSafT+ms1zcZ3wkgyTh-9eq}}4f|9#otHRw} zNx|;d=IRk z;O?_Qju0Gn)FUS{zA(EyjVuY!`97tXi!T#Q-%^dwg`MS6NeE-Lcs zLAkgdCKT_RZ_=IQGAC|H{e)y1?ILA13<|?NR4M3*v#zbe=qbwG+Wxr zE%sDhf58M=jl-Q)t5T+)u0TVP%`xqY(%(YkHt8QL4~#EJIX%<`^ga)BREhIq*BV); z9}iZv{VN_4CwF2JcR_l*1@YP;@>Jk)Q|L%G)Vz(EE@CFc6%|bTN}E48OYjSu!oELX zO!^Q3E%umZN204WJm@a>J#u}mS1Nf5tkrh7=*BXm_3E;*AjD1Lb7Pml6_+jxLs(+K zE>b&Mb>JEkqbT(Bn7g%4ug-eVKuSmpv>HTc|yLSA(y;$TpH3#1^$vpB&9%mb? zL-r$%IJu{et3Z3hTC6v9xJ#h9R8GJrV%*l#RaQv*mIUAd1yI>-xfDwwdb%^_OC0Z2 z{J;@s_nL|&rJeR`S)@4Rzmenjl8Wxh*hso!y^;HG6w$MPJ@IIxSMjo8CjIBAbWR}1 znH>CzG22(-@!aTYo|o{Aohe3V9{q|rti~0$YxF)ee4nIa^ezF)@NFE1;rqDyU!7@4 zQD2@efw9Um)`~p+XOcw>%@PSgC{uSQ2jR8-@&Y7CZf+cBM|8_@>`$O4&hwFM@14r(GgeR6B)waU_^6orX;`qeIn9ie zhSt{oMC!|y0K^>*h9~lBze+JA(Q!@8n4MvQhB^E9b65H>5ATTWlb^@^Ymf@o_ck=1 zgsl<$S~DvDzWV$B`Ob65N)2c{006M7|KE2q{a@edddZpk>occR{z#>P3L#YLrJ867 zGz+-`Vjjx$jGqcj)=sbW*ACA%Ta5 zT9f)w+p@VL`tz9|PowW*KJVDq+j;sjF#c4kCnu|`tJ_F7QhmdngW}lnJuw%i*~Q05 z<~iD2f71)tqWx55vi1^jAp6ogu)B8Md#f;bJz({^f{W9i_Ln*lj~{#C5h`LzJ1?c{wYADw ztl7a^-iqDxy)p8kQRjXi68x)1>(Xo&vs!l*q}b%aZ(BdE^3}U=X?$4%ej{4p>r%*F zc48Ux*%og2$Uvh~$*p{qAz7hR*jJ!g@mr(yS-OLvTKaV-ulsFUM-}5(TH;f8IN!F> zv@i4&ql;h+7^BBGR`d7)ahbL`kb{l(_z z!uTeuY~iL~Zc7=<`>aoL4*yUY$fy51$Mm6HYQHdX)`vFRcM7fc548ouztU4J1W}EX z^=;h`D#-(WsBmZ5W)c5lHR(V%=U%K)T8gTrU@_;S9S@U!@tvvfBdPo~NbX00Hv;1{ z2G-|*N60T>LdVpR{@F~=iR0Z2S7@&dckKL#(go6c(>~mYJ8M+xTlgH;=QF_ODQGldYLyxA@aE*CYy(Gai`6gWeDQaEhcq(}P0slXWiRqMg}N zQ|p!ET}dFGk$7^6-4Ri4a-ZsXC#`547Qx%@`HwN%ND+^t^eUqhRp5>LtW6|9j;~2**Atm1Bw`sGxgPV&B z^!+doil{0_kL|7V7U1(qFi~aNAO@yyH+hc_aXilp&XInNWNgVWT*IYeFgFll_1a&uX7SuI=okU zG<-eT#m;Kwm8V&DE0He+ksT5a@G<3H95nZ^^R3K$!Fs$cIbhyQqJ4zVX6eyW5cb-~ z>3V}X(M1?ldr-_Am8cRy*8~=a6sg9E_@4AR{84^FBcK?fHZpiNPN8Fi>PmHNfyT|S zlbnb1bQt1dR62onP4edTMW767`dh19x?|&GWw{m}OC}kyWYasq{=*jgS=(IB%B?xo zIN@?xc&W|Uk;p3ethBIYicM`P>oVS*ILKXoI@jY^1Ah)?-T?=A8qSDcSF|5tLfnsa zya=!r!dxKq1BMQ7&zz@bn{=*JRy)Wy=GkV->6#(gsVx5+>> z5oVR63f7EnOW_LFx_X^@dyT8-POEYs`!t}>#Dnxo?&~r=n8_R_-6$h-GY0`Uq!!rW z$KnG3tLvAzJh;{&#~7KwgZ z*C%q}%@{=gZH#nU@{NR&+cRW9AFPxg4mNP8sp`@KkC3t-!67USn!v&&d^x;)@zQydmh6`svtLiyQqLd5NhSiST`Ayo7YFrCAD z`5ag>tGW)q#U4)PJis%LdapeD;+nmKV{Bvbpw$Z@3u-pssZxDZ)TI)X+B6O4XtlW^SFeuTcfq=}I}2;} z)e#Q%@YX~R`_05B_&z~yXp6q|`dkPaS@#d>i)imq|M;W)P$Y82VW^Vy!*CK4L^bOV#@0I>k>JF{OMFMdmV%JT*z7Q%2z3>BK^%J z>w=}|^Wpylp*PW5zje9ZYWP^c{8uc(J|K5N>s`x~>ykB7S^%wcJ5Ao9(&VO+6%v$S z|1rY_oXF3`$X+%;iv@QYOXcH1YpAMQN!&S|w{&iZUoAEUwfi&k{hIb2uXZVWh2F!+8yen7->V8JXrY)$-*kfMTfzTEO%7r25J{2y8&SK&+p;6-81HhBAJJe2xgSOMJhE z!ilmdlm&xQR&sfLqgRuI6wjN6o?>i{%uJ!ETf>CmgA1ACAg0L)xDqV?S{dM7(m5b`OM$3l zzpp8wW*OJ-miTR!pEiMtHtLn{g6`PD_MK+QHbZVVyM1YNPxPWe&yd&zR0OEb-$0qa zrJ{^EI5vrt!kRjhwZDs0!FIGl7Y^jbVE2CV_h?I4N5@tnx&+9(D?Og|o|Asy5yBTR zk9Hm%H8T*~t-&C)gnY_@d7UVrp@uLKcKBI1U=6VoON+#^T@|6Q(yPP6B|SA`ef`79 zf*?UK`n+0!{D$CRs~JDDfe*qTq>E%gl|9SLBNrB4zbMattdVqa&ghi@9~jOZVuumv z0Lc}lp?4;THe0p;oMgaOqUh>4Vv5$rn5_O3p>SD=0m5%HOiivynqSLEpPkcwvCyv~ ztuOYqyO23(QF+A%{}xQ0n4;cK*&b&YspU|i7Krl6Vbn>Al%}D zM5%Tl6Zp4nC)_bobkHMzN7`hA1KiA&X2S+b_Jd)P^=3FH5RmL`m7~UUipI>1S9owN zmD9^R!V=wxlzyl`ZD1)&Hrl_;Gt$dvjpoUJ>~b3pDI))nt)6*4Xg5Fo zh9}9FQYUEPq%S{5e$@-jIE0KQE4;K_0P_c8!+o7MVbe{s?dPx3k2b)S1`xSSW0WX8 zNv8o+J@3d$;my763W_*^VS<6pix*;2K&5_%&3v7iL6Q1U46dVGI9UJ*rJg4Ws{gLo zvu2~_EH^iJL|n@Isq&eB9Kh-BAOQz!6&#N}o+iB!^fJ$SE^1rl?chPDis)*g@7~EV zs-xd_mmc??-U|BXc2>s*_utP%r7n*)yFoEi^ zDBL}o55X8JE>38OfO>w#MO|V(^VJ1+8|H?iudAx|&>-b*fMuT!bW~!qwZVhnR2yMA zJ7gX0b`-m8XCA=}zIfP`iz zgnEG+O<=JV|FuwEH&D8^f&vvmm$SvVota_ZPgh`*3hnXKG{T`kuTd+Bz`8#^X?0?`I8AI942er6U{FJVvMwj~2d~TJrf4O)4sC&{k>|40DD%y3&+HYto zBq37;*eYg0iB0Y5jIuiXw9N4@U2|+Iyz7usF&>b?t7t!S#{)$U2M2WI4-_OL(i6GI%b62Go8j$%#)VC?xmFbF2uvi##3jgBnFWC$--$*9bom$6}5n9n%>p@&QJHCog3eY0M4`L*3p92`d`Iz0-R9wTlDH-MsQ)xGoHV(4 zLKY^o6lhU#df?;TN1GUM2K8+)2}lj304vaBaQG=)4LywY>Gc3{wP7xSHO!V$HNv}) zp0Ef0j#WVfFT*dabB(a(MD5?Q8AV}M#uLG@=bYX@%VG)9fQ`!MMstx&@vA3Z1{-3* zBN_ICfISMc^G1zi@9(lbryL`PTd(d$n?1-)QBg2<|Ey3xi-mDK%OS%S8}eoX=7aeI z46|;1xAl@=2V;*L2!5HPq+L;$`Tdd^HaDJyYQNIUCURuVIizVH6RPOZx z4(6KnDLvO5iwjYoHyH8ssAS5vOm1eB+N=#ir~dIcADSgD9L!~i82pzaW>#z}VfG80nQ`#2Rr~4`oKB&Epj$3iwZ9RAMU0Y}kjaDREo^qI8!;t*;Nu z?vHG6t)QVa?go7+w}4TfJHHI!U8N!uE~cIwK&R=&X9tGzMs(AvwDv?VO;?&qxLJb# zz*;NCilj9zVPVi_8o+z^hMZEw-XezVQFkH^th+4Iv~V(l03QL9k_zq^AVnTFyd>pm z5amZpHx}DA5x`8MdHAOChPGX%;-lQ9?Gov2eir3yF}Yc)2mSRAal%Rofz_+Pum<*; z*vD2Tyna)zaOgjlwH9&N{eBry&V3ZX;QfpikrC0bJG5iTlSXrxN0N^jGV}u>=XV!U z&dzC(fM(6iu`c}jq8t|jJks{}B)e3(+408oZW-O_c=)W&vY$z4g)&pi0iB>5w;m<1 zC)8zZ75zBj=9WlqB`vnG^XqJ>+d&%svU(eL{cmkZ~5 zkTTO;0O#fbrD#tAfKD~_TLRl`6^E&-gEeI!C-4eQ`kWFzErl2u0;DDk?vJS#FuxF@ z%RYVRet|!;s6;|?e|{R=!wo#}?BB|_$s@zs%-@0mp8FFt`_4h0NPVw=i zgvPLX&~J$mQez=o31z%;+HBHt#<${|C9X1yD_M;g)d*^!jfC~#2ZYHF3TEI<rk~kPzu@OX^RGFUUBUryz~Fcl?1JTCc-At1Hi;<{<6?dS>@wsmyPih7`9>qq zd^uYV(wYcn<-K)2G&7B#HkS&w>QV7D4$T(tfo>$8{pp*94(X^Qb~PTOs~-wjXx{5i4~#_RKJxTcpp&n@@K= zvL|UJq#yWmUKWbcrQmFBxa3;DnPIDX4Bq6Yb4sbXH)9Fs!c6J^8feFsFF*AV$h%p- zB2yn_TKt@xEgsXtuMG_)&PcYY)vj#fN>}{O2Q65*G)Eq?N9(dqD$sYLnKS8cHi6Yw z@Ka_oYgD<-i6EuV3Qw4~0ai^gE{Up1kVK(PVg?XjnqaSoN*#}aJcSKUf-Du9SuB-8 zQd*|rpR)hjqm6^yz6M1uWPm9Zjw*^nu8PtK{T;t`L+G#!@axzbSP#!0A$PL+>xa5N zS6K52gOphC6I~StLBV%TyBl) zkO*xz08h0(ShL4s9+<}s+fPqC6x+uJx!`mkO{Q43jK)m^OG}`7PL+a|KV@eidN8PJ zm!;h(h^%?|$?{~ip+;vMdbsYXQ*vCUAD|$J2I5CKjw>aDKwMk=WVG7|$^elk0f>>L zXKkqKP``#VWO&*g$J+)af99GRROd_)T~}O9 zCjM4kWN$@1;$w*A<`X#|$cM)R|DH!MVuglcts`Ru1CP-E##gR;dBUe?NUrj3re^TFK*AsawE6pfk*m#S7- z6=My^O(^hfO#EIB%%?ws4QvjEk)FgfXOWYIjq#)|cW7Pc&{}vWXKL>zZygNhlTwz) ziei!vT0X&+7b$BI5^?#mfr&)T$poM81;$h8ir$hh>lnw>+Jy0)n-Von`xJgJt=B#? z-@&WWPRJW$|d2?WGC4eFK^Lm_ppt7*#eaH4^Q#2IvM5(Y z(?X!m0bQoL-Nl^$C-&54><~!DA|oMO?KB*h{?e1AIx6h_3S;*-cty7*dU4@1Ewlrg zVsRmf_qEd`TPsZ*$N~n0(&h34cg`{{l_V(DJjBF&>R9lZD%QTGudbZvbn)p;(B?hb+nR3dwB^j+%_^WUg^5;rF z+P+JLw;ZX;D5*Hwo*w5HO~jpsHc%$+*gM+dLs{uAK7mN})`#km zok+|*qHy-OU zm4j%sXRc*B@#;fegMXV{gKrQK#k!`ok%Q9SZesr$X?R&x#Xed+D&y!X>Z+rCWwN5{ zdTFvEZn6?cD>x5Zw;HM%X9~Z*&R18W9gO0^j_5ORwZ5jR>hs&pATi6M_}pRt08=fdOyA`h;G2sk-v{+)A=v4VkVp#B@eN-c zZE=UM55LofJnk#yWo^)$vSoQctAo+hslKJld)6Jz!S@nJ47TK|P=S79JiKx1!BE;$ z0T+F{{isypZUHxf3|gxKv}uI}(#znpL)?g=GlznCN8TfIZhpOEq+xAINO)sMwVRHC zYn?!#q(w3UK7X5;Cv!LXdHhJlE40llO$Ntw(ndxq{g`IOklfPXIn8W^eKct&M$FkG zF*Vm6aCaPTV^V*YQZrSjLD081d3NwLCK0 zbql&=V?mp!t1t*G6Yp3DygiNhYEsajfrD^?$DXQX%=$piPyw*GR z_PlcTOdk3hHhoVBgMsl%t~DioD)y1}Y(#XBBxCw;g|>J*JOYmIY=^eFx^BZEk3rIz`C~g%GmM#m;Sw(EfKccz4raL_%{Yv*0&yDmap?~D@d<^gEr13q1eR&3UUUeg3cA0c!R6wKepNB2&DRU*eB&$W z3vXV#_{V1$-40FN1R4n3`d0ROo?g+m&upH0A*TE5$da2e(~b!8eSRs}bv$B3>TR$n z&Ad~r$?GMs$ggGyyPb)K`o4Hb$w61w$ZgH`%R9ANEWC>jaEdU(7H2tawDF>ivH5jo z?xs?u%1zu0Br10q|5c6L=t;iNI&hAu65RVfDROFtAb6OW$x9VAuAl*dw>#sXNYk^8 zNK#A+|KoxDuC7z>C~C%aT@LY+Clr`&%>ScnAFb zlI^l|k1=ZiwB#Tx5LIECE--5i&GGGLc5v38&s@xt{1*IRs>n_Y;e6^d=?G%xN>MuY zi`XZ`*ZwcGh6r@;BfYCc3pcwByrdo;3uWK!kALWS5CS1_At?_h1HV-5OX zf3Nc^-;(2e>iO|`%83}JwR;iME+ogDu|n7^@5E3oUS&49ng&>-^q58dJ+w~urImhm z{tBv`fyz+NmQt(=4{}(&E5H$-*ClHROx|0c4$FVnv$sA#Agou5QSnoizHf+~T;rVB zv3mV&^R9QW&IcS7ax^<0I_G%V7)chIjNWV4ZrJe=KTYskyLd(OcgA0kcAy{v;lQ2> zn1r$@oWM5DXpgGY@?iCF)0m{bTEGtqb#rEPMd8W8;tSxju_gaCzX5=wE^k(gk3>dN%3WG+IL7 zd?)U@*medKo=gu}#JJYvnMQ4uIERosk&{ zDQ+JCBef_q88XmE zVy}<#mJUL&QUydK&NNImHBdy0ydc9X8|=N@n<3~ITX!;jdCT8FxZoDYS7V+zJ|av} z#hiIOevdWDzgk9B!#kO^ z0||K=5tZFehn){%h9|G)YFQm=O3S8!sB`kVJlP+e(>CoHVpWv~aM7OB;bkOGM58#* zqqJgY&4wfB2f#6QuL@72CLs&VlW zB9BD$hwH>XL$xpl?wmgkpE`JYldXPZe`5%VtR!DhIS_F9 zS|J5$W^+kGf!P~_P%&UL@w0XEPD6{~tA8U#-JZFf`F?fIkWrXJfW5)-@NIAT%A0Zb zk^pytvwwZ~{3Zpom=9<}<#wYj*ik%c`y7{?T-%Zwodrfg_aZI8j26hAM)@r|7^1M( zGB$*)EFm6MTH189@V0Nx{~6fCT#i~$W>zsXl!eVg|Z-G7kY zJnI0q%%Pg|JQweGZiYD+k3t7za9!cKu3CqP4MZXs4pfc!PYy#n?KEYaUtJtXN4yvl zkO$aw3W5jNH!t8n4GGF40+z||pBEkZ{-`~y!=T*f4>J{DCW6mhz>CV0N5g(t;T#7) z;(V5ycue*-a!8KAYP{wLhEQ3KXX_8i>mAG1;P75?U5z_P4UVUqpL$rQO3^>4xHr>N z?XPnzmotf%WbKb!w?0F6$218QGJ}{AL6yqBdXehE-5y5Z7JZ)FVYDBY8(@BX#D29w&15C*bfO>_biQLUIG4|ZbDUa~ zPKp?+;?+fPK;((Z)H~!&9bNZg-TP!3T$$j@8CQ0jZ~vp4u_^@XvWXxa1}>~`XL&SU3mJtRcC=te$z6PlFLeoy0~eQSaXR%gXw1bu|g)Hu*} z{~-VJAn1TDPtlr4Bx@@ze?reir}Juzz*MwpF3nh_FNg>x4Me#Y-OxOoj0GP?5t1z{ za;|%{eD?gQ!?WXOQ2k+i7(X78h4WjyaBRjU0!9oyNY#v|mnEIO1R_7l6vy70?QxoX&VR72C{Z z=^Rb^kx4ABN9`phwD6%>;8(c><{M@k7oyc>52q8(*@$^ah+YYl8X*app5W5mIj={l zsmY^`f&B6LGQR|H>lZM#G~6!m9p#5g^esVM7@TqvSVJI5>^OXE#!UZ2GUQ(%COqt; zUejS==H(C2z>@*xIl{26X+SNO1TB1d9O2aPq--6#!J_=rHLaCA8xpcwj)`62r9$n{EZ^9Xiti` znuHi#7JQFu*-g-Nl~hnCekb(tzp*S3J(h**_{>coV+ImF|79x`U4$Ch_=GA95^N`(Lq z%`xIhPs#DbPd*a)s5NfBe&K*ojaf!?>CO@yIf|AihLB}Q09hvYDFZgkgXZGkRMCic zvi+@AH1flJw9{6NO*?Sv%ooN-o4t|qsG;q8pOzW6jqLo3P^Z6=S932A#3QS>of<$C zDIyi8FYlv^E$QjKE=5YP>hsf)f|)``*`kj$A~`J%B;8VH>hkPKGaFH=!lg8^cb{M~ zZ$hDVG=&+gimEv-BAyMPYlGzYD9t$yCBBsfE)NN@5)m#}(lPyFYHgb{&SnrPp@T0% za60Uj+3K#0i)rL0Adpt!C7_7WFxpO0_!D>Q{RSO^f4YM_*ZNR87Pj32Da3dtAvH%O zFiB~l-oEzGn+~53&126@@`4}ne*Nc~0H_&q9M)iY;GK(6qQ-OwL&!_+O1=SRVp1Ps zfg~1v#7fBohtE}rPDTW9UMwf}BQ)F}?nyK-@oKS|p$t{FW<|nF1rW5wdYi|) z-_4Z)^H_ook}cvD(B+9H59ROn@!r!)LStv7m9ayA$AU}1zlAp4&@|3aoEx^3-Yy8I zXA~#E1A{Zk)#~SPuY-|qMn!|Pz_vYE0&+NY4vaK!09nLd7GT(4brf2LtP4NjA@BO3 z1n6XifsrCqPz#{Rf)TIwLgjk&h|}<6QC5E< z%W4nQ=7Qcb^|2y6HJF-OWSs~u^Cw@geveI9TUD}Bq6KeCOJdBkO$ttHvvLZ}>c6DQ zuI`FIbA}rLZ!usevqRg{K!vWI9InUXkmUl&LGU&v_f3}4O&*ACU=!e=01LHb_k88D zeu|6k-H^@UjLWnZ)3A4g#{TinP^KbiDVu(;10%}gBY7t3kw?%!Xi+d9I9e3p$crr- zCN3V(f|l00)5aEY&k9;O#ShHf;=E{%sek|EZ^}duV$=BCyfmhuL~f5=h7i|^7|F3~ z<*)k8sNB>)J8qp(D-EGLD4Roa`eBZ&eD^QZpE@U1 z1RFB1l^${-{g~3@KlcwRUE_(&Tx9zXD&^}N1lDfj^-cyKgDa5~z3K~o zhoU;$bYldha3!?Y2fpXQCVXgIA%?y*ZmEP^Jm=}r)2{AH3oDa~C=h|gmK3P7-^gJF zr-imPYM+zD;|rdI*b#Nf-CCE-_6Pp+;;d=qX;VbwNJn%E2H} zKm|?$k|<^s?_4U~(iNTYd4k@*cZ{eKxK;gtvJ z!_!Qh!$^bP=4eS_*|zhweKP*;tN5C*zm;Ey5jV1Yj%4t)`NL3dZr^9u&ee@ou4t2^ zvP0P-$E?n>t<6l}j7f-IbylU8wi=G0GVd*%oJOULXvx%cVNRvq@0+qN@l*alhJ2|xhi zdlg&)$DYdc1spy7Fcz#PCL5}Y-AopW@$K%}K#(XNUb65WpEF!;_gVOR6@hyvNzm$b+xhs@Ku(?T^dxVO67hO`^CBC(7xle7_-e9R*6nJ#9++itS zP-Zu{dl+18Hc=l;-6E^m8_EUY6Q=%zy}i`CrQNS;8148(>^-Z5tEBGj$R49FQ^N54 zev~=oRScD8p?Bk!R^-1j3jx7j=B&Cz2JVaJgtZI^ZQs9F1?*a4e9kbKBd0JfKt zDktW62AvV}+LwwxZmdq=_^pn|hnVTNVLeb{2Z?qpy~~JKL!Oi{ij8x|kIrfX`*B)F zt*?V6Fq;;_D3Oz&W1$q+4fVcktc;-J8dv?SYj3x(#nkrEGyW;zJ&Q#m-7SK{@uAVf z_sfy+XALobC0@hGZ+D(a{Va@^qTb6ly^7u$ds}-3VL#f_z|_g&^oEknF}8jlA`{;I zr7s;NjgaGu-)jL{ZHj;x2RgW$+q=^EKLyv}1jLpKe*4CH zJW3BkG2pUnr?COnMB=J$X`7Drv3=vW`-JNUb~kMaHv%Lg_TL1*#PByclSxN4_=Wnl zU|z_^I=W2pub>biI4P0{HMYwP(J-yY8AZOp@n^cOy*+|T{{De$XLBP$riOYFSgC^Y zAhkVST1r^|0G{?W3-Y@fDA9hQF!-0P;maPg^!XuhsBA8W1)cQ2W&kC`dFh#akf z)KHNvPs*(w^2q>GQ7G47e7f2bSu4l=l^6EgrD35;~d!7v<|v{Xk{-k}ehX}Tb%v*ygw$t= z*>N)X(&f|DS9q*ged|q`ck{6`kmF8aVkv)xY9yCNJ0+s|$p9WEj5}^Tl+v$AC`^wP8SrDN)y2lSYAh+j!eZ;ImNKl} zPj|%c=C8_-X^!0`51A(@Cy&}41y(VBBF1j_COaar5)mSIH7c|-9pU#khUpPGS^s#O z&$>{W2UQ)VkzY?!Nia0ml0lMWOu`CZ8FfJj?A9YXc<{S@YiMH}Y(j-3yz2ZnIs;EB zCnJeRGktQDVd1}8*fY$IQ0~)!^==GVf|X%fE?1wgLnJ&c&G+qU^;%Q0*8;lU=Dxf; zR@aTIWjD9r9@FC^R%JuDs_;#JEsF8&uuy8Ec4ySNta^$pvo42t+0Oi$e29Z*rNA6< ztOc(MQO44e*-_u~2XMWOM1SmkD{t{s!7DR3!|%PO<+>eP+22MII=rsPKiK2%rcvbY z{F}XX{;_-Uc*?{#!Fue6NXTZM$rFjJn+ykzuYSC5gDT7>*Akl=r<;f}@Y-M|7d@0& zFsNY58d2)gWlyogc0MEGwMq?rWj|nQAaoOA=$_U3LRYTC+=>+!f>MdqeIE-Imw~~A z>HZUYICrkaJS~ePDjt#V^jtGx;SvX1Z!#f5I1!ncA#3Rjmlv^jJzDMMJ0dF zzb`rNG4fL2evHvHSDt_dd+yRPI1OVf8lVG3!NjmTA}bx^5>%Xd_=j~QRn?LM7@U&< z8kcAFWX3&}=}|&$P0Z0EBmD}$7CE}!Lt^aSm{v=koKO=K5Xd%TAOcN&<7)EekxYQ7 z4rO{-vEg&$6y*DGL+KHKL7KfWhn~IjOudxc7xKLyeZRGzKe|?enQmKnk7t?qc53hW z%kfj~-Ubm=KGS*}MB}MiUztf}iFyb{ngZ>7!!vfholpF8==A-1o; zSpa?lMgAvs>I;-w)WIyN*2ANFh&~EV10%W7p!NX-AQPmyEfV!6`|o)UEQ61T6G%00 z_uiXRMs4HQrE9xbI^&KvMpFRoulgOp1Dp+{0-t(P;Tp?_NW_&YH0u+^M7j}94BxjY z_b#Ibk3@8Z-Gh0f_{RoPe*Gs_`1fOU;!=~5h57@FwcExEcTHDbdO!~F#E)D^e&Xl< z>oI0knD{Btc+t90j|<+*Wx%$Ber=)>4p{suR>7Cl+xP?eKaHkvTn{ykKtVuKAwfX? znva55*czL-Gq~GWA0^4y1qxz>Jbz*)t|5aT6sCZL-u(bVkQ!O38H0&R9ioO|`T9yI z8g-mCySU9{0iJX0oSH1uF8a(|cRf_je#9HY)U*7W@7z4Im(yVjP&yE5khOfAG9BQ@E`b8+YWDpfWN9*2jQ)rxuiEu=2r=D(%(mGng;pw6pj}66xaEbUG zjr$5Maj_mwJ^nU>T;tC|4cc#W|Ar zF_bY8(P8Cy72!qH0R26B0@0n+elwS@884J!hr<_&mA#ZiS&#*e@{+M zjvt&@qjR-MXW;&J2H4A6cwRf|3;8GlHHWs%ka4H+N8l_X7yj^P^S~g`y9%-k4%vg{ zej>$+-jFd#JNl;r|3&1p!iWk=lufaQ_YV;b8RP~>sq8(XXp=Ivc)cPyWxEFR__{DB zOWyl84)e=4CzQJ4Wtu5~?|tt3kSZj<_^w&v7mVli*jB9^pZ08EZ*S@#kfY?px|zh< zPRB!4s@~080lQUoIAbni#>J_YQc?oU9V2Zgnh=H?ocL2~)nK4DUw88JniuScE?vl; z)XB!`l(n?l5({Gg4%|YW=ahMBxw z{@0G)-~*OaBqI@C%1Y%q;8fO(iorvs*3x%69<>kNZe>ZQb={+O(n~*e(xJw>X21=y zZ-eSMe0PG61jE&S==JIy4lXxHJ)qJMY62cNj1qY(i6rQt5O%;5rTPPmjvqis0Vamw zcF1$XM#vY?__Ri#sz>Y_BvFqhj>3$z%nIlhHk=`(K0u8scUW&uSq0*7d7_Rts5UNW zAM_{p#dy=!lb_ojC0OZMz;-!RpmxLT%xDA&!nw(dvMR#qQ&i*?L zNw7!CG>no4M>~@5Wjs}0<@9_9k?bw}roZ3+=MtECx^gfBo){=-ET}u_6klv&Os{Q2 zS?54Qw$xCZ07V?BYaiTwI0KN(zWl&0Qc0XsEvkFd!;?9oqO$X6c^l1FX}l@H8;Jl! z9Gc9rb^#I8-b;J~{_ym^;xsENC@Qo+1DOriRV*vPv=&ONIBt)l6^45Em#P&8y~G@(*36@I-KpOte(~b{a+U}%mq}+?lRYy?k-+qc! z&v=#$Q)?}n;=@M+2`CwMc>bzYp@W-|bnMFpi}4yr$XJaJ^XoO*_whcdv1Q;`rj+?R z%DI)wC=Y;mMYfm~otBu2nCWC399}+}I&b=5{T5~!J0Bi%LubTl23Ia?3z9E~8JC+K zKLm6sM=dwwa8-s==V8*rkt{LO!!+fVzYj{!Xo=9I?8-poU~c*9wN%>|akgB36lniC zM~Z0M0HotY@+mK4RKA*6s|Tl9ZeBE7R4gRc1da@)x*KqF5wuM>a*VlMnoGd zdszBxml~Gl%so?#Hy|3=kEWCwE%c*}yJFdxgn@4Un8||p)HQRUk}80bhvdPqQ;KaL zCi3iF7+5we(udMmL7apR=K)3VTtNW&yFxsOZSY?8UJoM1aeQ)l!yjr@)qxF=Xxy)< z)!Bke=u+77!Jzq(m+#^g8vpVidf@G(Kp)@_R}w_R z?Z5f~gDwO)^rgCpi3mbSMGeB|Eotw0fxPkn8v(?vBM@TIQUoaTUBB;f&9!!MjD#zH zn5s4$BI{i~fG8KWnxq)flql9I$(H^59{$-LnpSE)nvW%^*jwl|qe>WYMETpScsxe% zXjpaM-fYGn=m86&P9b*3juFDO5kfL&kF*cSSsX4e#3G~*f4Mtj)TR&2-G;j|a+2); z--Z)8$^r5msu#elj5I&tJ~a0C#(JJi!4=P5sv9TSq|^QLrl=ij_N`8kN#6^f$5pQZa}&{3wTbf}X!ehCIa+xGfRkXvXWj#=x_v;Y zQM_0|+%ETZn8=byl_wq46(FF4`1#hA{&1O(ByaDKNN(VwM)aa zO}HtEQ668N`-%9iTvbCYsK+8@)^1*|O1MHF&bsYRBfDgz5XxVb-Eu}x?trH1j;me5 zun@@*)Z|3SWPn?eTHV=(xArzq56rMv7eWB1?7GkwK&FHB6>; ztTKyzh&n#HnUUtbv7CC4q&i>rcr?_gN4MfXG|`-w1RFp-n@3Q8hdOVX^`AGe^Bvel ztL^8$*F8Yz19THX2k)c-U_XbDV=w~>-GWkRTQ7OzGRj7$yV0(_tV&JSqQ#5UHi^vA zct`3N1l+FLpI^hHIClb?6%exgs_9h9<#o=B?adiMk9KS2Y7Qx5D+fC!ngarZs6Dot zUegBbm5J$IXdCOfpEuc$@G+VZeRP>lUvA;oc=_!eCO;EeHP+o!V@urS&YMUB#x()o z>cMV4=|*3M)fcVQy9VxJe`xeFS{2AA6u$D9I7-jE9nrx&#)~azphCWHTF^L#1W2q;5q6%N?koD zL|I|d?h>nj#1uYRo3etfDfbX8MMiNVHRb6)Cf>LOrgK;del>wOJZ^KkO$!A|wSKWs zT^W1;72MTEEtAE_6ER`TG~_1QaOYUIF=sZjB#ffVjOxg$f4(?tB!+tY=OHVF{haE) zIyND@)jIBhB}AjqE3dL>zfv61LCQedr4{)&=3xK;Jq`c> z_kVGc^*>HZsQ$m8yl~EvLH)idOIKS`&rvxc21%@?aKePKO%TsV9j$+fgB|=IP{L6w zOnYC!T_)Mh5Zj}os?(o1(@N{kJ-Q>&cnBqZyxbL=o ztJDJ7YcrxCBaJ6K=$USvYWJWJ9>W~xmr8iq<|(wy?2jS}PPJ?MIlYV#!QSVV%0{o) zkDa=@tebQ5^0H>G%#j6}JyVagJ;;{oPCSi(HHyP6Kuz)M6e7!IP)XT|Vd-AnRdqQ6 zxNOBK#e@Zas-GC6*^k*6lglDC@IR!d(LsGtvsw@MK90AN->p>F_tCuG!I(4m1eo8X zp(qMU9{F*@`(pUO;UhXlXEU!3lil6KJPhp+fOLv_W%hfg{+YTlk2$&0W?5rMD|U8|C6u8sK=k6--_ay2O$qMO#UQU-CS0j|GxD*W`!h9MJsXLU z)&QO(?EqDD$~Kbe;`l3)U`fm-z?#d1-@vcdX3QfVG%dEi({+R*Z0-mf8!Zl>D(lY% zwkY>1(oF+?h))}3$LTsOcsLraZey)ik7)E&VQk~9>SO5sBByYX&$*$QPBdfgWtKTV zcoA}-iB;sk`apZM77?E$8)res$~L#k)ssyDg5b^ZbWsq! zi?bw9Rr4^p=pIY(E$DJi<%NyoPg^dN6+f@ zBs9a(Crn!I!x|PoRom2Bs{etqi6)1V;iTiQTh$@B8EMD4Z1MkqQcilj)K?7{%FIJR z=YUB%OB?2YJLz8hA1J}JBA?C3Zc2hh%)HkQ9zQQ#m+gDtbOm!u+zu}(;ngQ(U@MfX z_t2~ZmC>3wxIr6L3*E>-sdPxkE4UW|WijYw@wdd)g6^sv{^plQ-l>b{B=3Q#u|?|_ z39?Uq)OVGzVWTgbd}r`=y}ckAW{R-~?mKhXh+00uid-r5^H5t0@k_+3~joa1rmB0}+C6 zW+>I8;J`t`ejJ_SMDLBR2~RH=vIai5Ht0pb@(85S6o1kj zU3IR7#K(B=yekkf5^cy~@Bzs9tD_j2VI*2F-6w3PAqT@!{|g}N zBH0Dxa$#}j-iqrOGws0ACRS&d&Sv1gDy}d-XKPt3x2KQw6bZ)WEd4SR6$$g*dJ*t5 z>sqweXt!WjqM^MW4!_hv>O;qrSwxwWjOqi)lxoQpt`oAp6~m-DeI?3-{5I}4D8k*(U}H`RTn9Q6xe^f5L@28D>Tf(9Cp!gsLW-^RDK{n{o4I&N$MRgiZo%#YvmfCn2}(M=teVR`d+9s`rowkiA{sT(s1KS+1dBXiar)k|VE+Him@c&AzX^!QBM0!rX}F1;b^FEZ%!og!RGb zph8O$vH{nCd`e zjr99QaPPajFX#9QkWGZv&^%K3wfh(72NG=~xr$(k>6o1f2zgO5ccE^Of173->Y;TY z<87W&HadszSGoI(p~eg_JW8{I@Dlcc)~4N4Mo1k%qtT3XUhHOv8@bqM_*Jz|o8s{9 z*Voi0S~}#r*3^*QYpqVu6cu3--RbM+E8aId(`Pa{x ziSIXh_{x;Ei4*nswvQ)Y=Q|3%VbE54x8od|BWk|-)_{Y1!)fpDckL!9=BuA`UHdDq zcNY%8{$l9{)^(`x_fK@t%X-mv=F^S*n|6X)MF24gnRwAo(>1%Pyz!&WJ;6Jv>sV`r z&oV1YNA-hVysgcZ|WFG!p_GE(OY6iFkI!i#3MbM^ogpSCiuvb9f!WD|_tMP466G zZz58tze1v-6_<4I!;9d>P6euvblP?B57_^fpi-ZedCz}2V{s<{fck$W$j-^ZZou+? zBSpOn_pH%|Z+@@H?e~;wJMwr3r_^fSIw4@A+Xg82*jte}Iew!AC_?^f#>+aBWA3G@ zhf6PXd_28(_E>dzX%$UnPfcY_&+Plkf^)zh>(swl!)`HEjqJrZtS&Wpf;Bd1B@;f? zBpbhC+B=vLt~$)q*&l14JC~JSBi}`ZFM%HWnyH~d7ut>6{|;yKjr@K$JYB2@9aIaj ztJdf-n9+2Fm3IW7RFLSh)t)A|N=Kh{1^kgm@IJKMslC^z^__5}Yi zUwM=-;XF%zrr)sMA4*_iki)F7OA{)zKTJeDST#N)Qhd95ihuIq`*qgPnEWALQNq)( z9%~(}AAo=DSw&=@wGJMa?%sC1w-0Bz;Y=W3Ne@G_a6jxLQXA8&8%2A#dd6An&&`yVIxcT*wdwsJz%~(QK!PL8-&WNbf~`fXpMQnY*aL z`ET5A1#pBwa>cAEx>R`>{6ro_rD98bXh^jCOmadS_RT72w14;U`h-A{Lvx_0m>Ng> zne;tFlfpVIqbRDSn7IFyjIwfq^5%^%hTXh`V;VNHa;gJ^!bNXZ$DUlbkmLIr%Sx5% zLV`MQBo-O6gV)_*UZ^Y0nNC7rj26J-Nm_S^6y<+tGBo0qxgL+9$R^f^ziad^?- zJxf}ceWCssd8*lZ#Vi$f9=my{dSS=?KtQsZ?#8o|)yDKfOE;0-Mkdhjs-s6My;0Eg zi^;BWydWazY{P;^@~YrBUeizhM!wVtCGJU#;AP zaICcb&m!UDT3}$)g8PwAfKG3XQfhPt2S z6Dst&p!;f>eJk!Nd_S)Ctgw3n_8jhhY6-z*HX-M0}2_|^QPdxri;E|}|QAtEw|*tUg2m8#j8L^ugDkxg>e`%P$D`^`(fDN+0AIQ(zWQM+o6G84|P4uLT>1~Am*0tPh2U&CbRL1nK!^_PITQjb1}7vGB@1#jHE5n8jss!>>! z%H9Gb0}g40?w1R?vk<}z-ClNVWa>po5$iSABvJ3m+&^~1QeOK45|r2!teO`Yfkemw zfJ%)Va?{Uk*wz-;-3WOvU&bfX%;wDD78eaMZdE2vXU5ss;CD4JmA zoj;VE3)_ksw#i1UK;xDZ4+x}FkUeq?l$xg)8xjuVKHZ(A{t`ka3$~nBJusb?ucVt3 z3)vmY#M=XvAKfyB2Q{XCGk=q)w%=j@H!yGYCX|e6QaJ5h_e9ujHt0QRpaW7wc)K%l zyK{NgW2@f^0ly((vfNtgf$_?X}ics;JQ=n)j z79?a%2M&6o5-S3W4>ynfB-9J5JKaMp_Oa}z8YO;s+G_~#fpQ0*CTQVCDjw_($}1!EAES%#tPm*fxJ>i-F;QC^jq^(+zMFC=Q7!G@ z9-UI2ygggoV|Hg7#I93FE#A1x6pn?Pr;M}^2$Y0F#$^raDkLOwAb_XVW$NAPO#e;f z{ZLcKsuB#SJOWOiwDvKTqlzEr5F++%>Hw39{_XtRj#hkHt|iXdFFG=;%7!m4Mp)XL zGFb~ZX?aB&c7@=ja0E)I3bMW@R8*SDo{waa93d!wCUY6)Ui~0`2@~!K>rd)Nf`3W- zaePDwPvL={TeJ3Qzo}RDJR4@O{@Oh(&hJl6olL~1yb<)cVa*90S%O+U^q9RxNWv1$ zVieA@``yyCi_@<@cylrUjXL>a1 zpdnxbc%uC!3R4W?V$@SLq=!$|nISSC;G|yeaLjPbeW+cBZPNbCq|VE^7PduOgPS`_ zY6Af@?PHInC&&j+t%?r7;s0#7vt90)CvdX6GPTO?vBy`lmEjs^rE5}{6W4BO0WA^{ z!9~^l@?HKO1Bm`D!^^Y|FzC1hb-$!(Z7N$}fyGPU2SOZCa+Vjx;zA29Ux$ zbt~3in#5Ar(BqAm*o~6n^Fr@ewjX7gs(Dm&4OfXi?Xd5`%zVFm6 z5)**0*&u9V;@LK?8h#kIpQ}!*JC4|6)Q0o9_hKJ40yYo8hxm#(yC|c?^ny-n=Sibt zKv?=5F}CMqc?l=E$C17Jz10p#`QHc|3%g@ZlHs2`gz!w4#zA7G*)yW#^-ga* zURqlkGJBxq3{ZD3Qx@AHez$i0V$q!_ZnDX^JlBTC^clJLaHq^KCCt@MBa)lIDV7%^ z0;T`7Ty#-Hn;4c2T;gjhMlL2uP2n!GQMEe&`?_P;^Mmw)E$lO-F2d8t91DLdhfV&!+b)oQ0;7j;qP zOx)kLIuM-_NfSV-FMH?oIX${XHH3owcL$*684RcnMm&TC{}RV%<*#6(u_K+>W%=d7 zg8Rk^b{8i5#X0I}4;LMhxJFW#Bl1U`5Z$mWv3neIaZ-VfFiMg=lBCQRjJN%DgS&T`2D#_$}nklz@u^4yOmtbZnQ>l1|^%#Gsy4gBHbI&@YH zUxn|(zXx@kv@;5Fg6KR?AVy^L$!suJMd_u5^;HBn<*0v4mj2?pPPv?`V_l~BM3EziylJ)C(>T|JwTPLKC z`r32##B2+D_$Z>1A8okBd{&0o{1)~uhTgk67F-Guki`7%QuJhWxtwxHTvL!1dIr;< zJiGRVyV86q$+Hxgv@Fo%l~Cz&QAQjXpe8NpkCz+8NKLj6Hf>GFaWK#9*igshe^LIrPCp@AcTkO7J{4kfdb(Iq18z7JK<&lw(rvXODEQj7dnFfE0* z;ie{!tmUz$-%rpWyjP>Sx87}Eoq}8UG4rZBl{DyVGo=Dw?rN#MBB6m+7o@6~`RW zeK^46CNu z?WDKzFQ5;;b5bfjH~{t~L*R!Utn9T0POeU@psIUq#QLB+xu`Kx!Utwv8E63O=Dwd$ zylc76bd=4OxFP!`G6iIUnMyYz&3aw3q$kg^u?a=0pt5N7<0@#(xrWK@f6IOCGx8Bb z{T|OjXx_Z~xyE$vdj0?xP8kK}Z>O=+GrXEpuG!XlQ8j0?;V zbA?_y_FI=kTIur_8Q+s0*r6Zv%mV2o6x5>e`Ps1i5FXokr?tf2*BtRp2Falmx&i-}5T&X{pKHhV)6cj7)-b@hee(=sNRDv77i zh7-h`Fz8OSuJBr2f^#Z!EZ~nKZBB;3UDhV-Wa-Y*_uB`p&V{Ub_=7WJ*KoSi>tZ*& ze3A<*PvR~S%PPPstWJcLQP|byi?+>op7Zo#^Z506M)2(nJLO!uVo=|XU!C@A$?Ng2 zkAMsNgrc}D^w-=C;z6|cB&361_l1!ihjbvWSb)v@ST(^)^b9}BkA&z;x}}+bF7!2XkHt_dqgYmORf zH&y=^{Gr#@#Z)wgR7R@t;zMPCP{Wr$2o;uR?cf5=JLi|*S|pdP-|Ee!7kj2BoHYj& z`;Br`(9C%Jhbd7BA1aV|6rx@64+UsmiTn-_e}X=#%qF-nxui$)w1tB;=3l`OgM(kh zj$AQ-b*@j>w6Rgai0Y^=4w{#Bf_`B7=gR|4@G)#t;^ve|@sDP4|H~h2FK4nSrQRiM zSc8q9|F8ER8#X$FAp`&*F8=?tllA{eJNJ1~zkSbX7T;2;5rbtZQdH=qkwD5skU$_s z^^TDyK$6;!R-^5lGhQ;W(y6k#-RezkgXEzDa63L8VOq!?r1PHTb#$fn`EUywYUgTnH{jFGLXF|`#pq5*q8Q`I*~wBbM#RvZ%#NXy<2j_WfGTt z)C)imeW-YR3ER)J7-~fMq{WcQgS$0IoY3zuFHJbK)2u5pv@cK1?R(fQgx zOKV*C^R|l?@mZ#QX1$}GV~_@w;{M^UYm@-@)v|PUoSDLVB~$UosgSSi2>0)CYr640 zkU_0tYbmQ;yF{m;w?Mn(?XSjLsa}SA&C89F{-=4Z&0p)9BH!AB(Thv1R}w!7#tz)a zCxy<=WuDu6$oHrp*eC8GFYP;x(7HJYO-J+4thwIz+UL-Rw@@(hsal{ zEH&iA;0X~v`X3n;Udgmdi_~1pT}|3^6+@%hzUeK$$!orD7YvgXjYGXmL=ApV%=(RC zOuIl}zc18=fPOjwZi+}?!G zD^*>5DWA=V6c|M2-GzT6SE8^UC>5*~Z56Mt4#E6jkOn#vc8rV4h4;vfgs&z))!?~( zVQQDzjpdC)X9tG?-eg>hMHHR3pj4VHISzHkg-#pUHuv*l%|3h!AfJ2Nr)_a3c=ll# z_sE)HQPiMo8z5s*qn6uIUeUe8yvh%&g_Pkn#6?a<%Ga+o5IQe#TR7|ak_q)}j3r!6 zN~JXLF!Xx)PL~5R{GyU8S~`2z*N=p&Vpt02)0vPNb{2$ZLY~>WtK7(=RLdxHdnl67 zbjWx(+2w2X_2g>L$@T>^|AiS(Pf{rkdzuHv9%LH2rHkHwLc=HU;VM$LbM~WyFpM_k z?hbv?{Vh}TJ{)5Sx0tU@2r&7mVGVWOMo%V6!pGECfRh|)Vvd*a3E_BGv)m->0h>ZI z;R;UOyP+2MnrBRqee-&K@<3fsa}GzxHgrlT;O5Y;cy_yajoU*)Vknp<(94#qHC#Jz z1b3P3`6>x&b2zkKmoHg4J?_!K<-e-bq*fZD+G{R6Qg!}&;b*;MU$bS^jHXtjUA0xa z(qxW>cabK$@?_szCi@gMh(DV~IHP(sS60eh{eJBUOf4vw0^ychWkH@vQ-E|#AwvYZ zIDjfMq&tNbykC1aE3DTX@Pbi0oxod|Bx!PFbW8(4;5y>Nc6QfEK^iw%36bpt2|+Hu zi;fa1S*`GDfF!rAR(LNaE~B%P)*Yb^k>@kZo^np`v%dpRL_w8;nt^z`D-w;sIe9$7 zj>jDWms>F%kG~NR82JJpYDJS-;^HQmNNm!g8AbC`@!?g|P=_qz-%~yQfM5OY z{|s5iz?ToHmnmZPJH~?2Nyt1S%e>S%9vBze2mBM2iR_2{sLn?5jvQ zs=6!s!g|AH4gp2S;$BZTrTI&{tI`(wW$N+N>0x|u61S_PL>`P>cGq~cAQS< z3DAmT1kfPX$0G-BCOA(TiZlbop87Jh<+Q*WYD4N!sK-IGJ=DvUkZTa=ZBgv0^s()5 zh{CbGxPN%wGdcl+t&TDjmWWCcR z(@{BneV(cVgzaE0juGW5@*KMZWqm{S`n~3=^C@KmSuiGm4*5bQD|S!bQxa9m7anZg zH6A)SCYOd9l~P+PG(IdJ=BIU5{0icU++pT5I`MEr)}_z2qozfUJr5w258J zot57nu@k_Vt+(`~`=rtQt2S{^_YRwD1v(?m>z}H@zv`-0ZP-T%*CeNN&q-SZAATh+ zW;MwgFuFboD3sMOGE`35!$kCQ*vt8j7BtQ9TPIyyBZeh-DfsaU{%z8ngP2|OW8Dqc zf*mw?{L{>BRDTEMvv3Lrm;to&V(~YUlt+9-$w{TWScF-Ka=DNeCt6QY(yYw~7K^!+7Y=DHNG5+%meD;{BxemSdAez8TmvIt^${(>> z=}u^o-Sj5eM}w8N5Q<)ju@Y-^Rty?F2AwnbME_JW&bv6Lh^b*AW{n6joVh}qch;5E z9u-ZCRTC2)0vBsPhaK@jr_z_<-P$WHvcN)$2@yChP~tK89~<*& zIoDx?Vb)5aL7BR2lJb@LS{earyaGZHs4sif1o2fRiC(4NbZ9E++3nGxe9H6WI$`lA z^Ov7Mj1YhH^~q6O(atun43Mb5bEjX1&@p77I?q{s9#Eouzps=dn$@gpWL8n}L!%h? z##Ij8D2KwI4Dn2k!%|5YM?*r)ptP^)oVhIZpr7)2RxLuQvJV4Lg)yLdnv=^QaiTeC z{xGJ%wWA)S&n#YxTt7z=J;=R`fnIBQax>+j>sOkYC%Olq1L!O{D4IUTWlXrd+V2yG zBSX+OJB^nlHrF8%4B^8VclZpq?Z}_kgj2$K`c67&+F5s4ReWY23k#|#1S|z_LG}yv0iFWA6W6A{9+lvQNw`;NBiJ^ z#Bz1qFI~1qcEe7ioJB%r9%dBLNsTXERh8bpY>b>>m${Pi(XKfQ zH}|(6^D15v*Enim)^kQ$vP6>pL^n+NxgX~X(FQb6R}s3v;S$EpI>HuD_oeUSN@&5z zIdL5`H&ddySmN{;?+~@f`V!waIQ|PJrS>A^K>SoI7y&5I%e(GzT4H^8q~wij6g083 zRahju?SN(M!NF0LT3suJ{)E`^UFHqhbun!E`K$F|_T!}iA{QA<66MC3v_Pw8ojGWH z_%_`kP{y#$(D6VBL(Gh*HHp&ME;BP|THXwU^;8ST3m{-u{Usm_UzHno7HmQt4)^X* zREUCT!IbWW2tJ(^5amBmNH~+3G8&*RggMV7?91MdKGmy9FXv|;5p1G6`?mXZ`<;?>E?CDo_t^gly%Ic2*G z3TF!Dqq~5GGlEg3r2eHfe3l$BJ^*{d#et?4`rI%390hLqiHSzNN2%F_jt7t=g)83{ zXdrp?6hOPV+zt&gKvG(PZYK(XX@?l_hHN)M&)0y| zBX!@y8aj)M|MkC|FCpp8k^OwWft^-pR;^|c4TpMT+`3-i$=T5U7x!$h8B^j2CQ3|W3h{F0T4?vCQy7qwy`BTm>E`qTz#F9%u!LmROQC?40y9}QEbZ$ZO; zGo_Sm>cXMNgtZwcRJv(F3=FJ8A59WZ0$BkueZ|bNdUSy_#wz76wBC--6Qn6^8U3}? z-v^ccuI#zI&GSXjB9NMg{ssW2ZYlu?mp`HolWn5>vN1t%G16Y`qK;|6#Uv8y9vpQf z)HO9@beWgvbf*WaSjCB}hywm>R&;!N2Y_k#ur?mpdbFu|btk%P>8RGDGltpg<{-#S z-y4s$tM!yUkytS}-|z#H^{-QQWUo_Nt->hDU&k*{eU98g^)+Gp&n07FF}c)C4< zTF~Hh4+<@b%-ug2x!r2-nc~CcDi76ny6J*8!sw?AEvAxzm3TnQXi2P}n0( z>*~6Z>vR?0I$t68wJe5_z{rA>v32P7vHO=uyCKxg?x`?xAsYwvBCqF-m`0-}GCrUL z%Sm+%jkVrtE_OvD7wW)q0Au>ew(dEgSP{90qC#Iw7k|O3I~A&NmklkbVE^sHIa2PQ z+W$fD@EX?0|0{yskWZCjBEBj7AHEOe_XoDF$n7Tl z3QGvzmdKG6+J!TBE$@d#2A%Y|dn=?I@C;MtR(D4*tC-)!5$vh}tz$4vu=X*Q3W6YdBb~ zpK0&%4N*+jj(4Ltw}}X7tBLzt@d+j=1!2)VG5s{24gFy&bG3(JPPc(?$QGKBLfJJd z1bD2edMJ<`kq30im+%1x%$;zA5)!o>1_zsxX#VhS8^vJb3H`FmyD z5Ys;^4sfq+ZokvVUH(u9N%r2AM`*$+;MYK^!k8dtl8)DF!q@eIQ!|xEK0IJr^&j6h z$~X1iK(urDcfmN$s1084Xd}h#XE>|~MIhZ6bP=tDg6G;LQzcdGLd{Hl1es9` zkYP0Q4?J2kYz}C#n-is{4_cdyk^%40$~EIkGbyrBj=$>U>;8+NWOc}{8Y27l9+$lm5EQQ(meUq+m-sUpQD<~13>dV1yl*Upp zHRHYDvx;NtqRmRmTb@F&_RC`aCeU{5!LN>WaIU6uOwY3QEq0QPUNpAd8)?yYG37?y zeUxHsgxiQ)3+e7vu1rQ|$mE@=r5W1Km;0nW$V8>xxqFKJ%qo2}-g2_jSujZ_A$`xE z=X_t1DG7gL%`Mmd#R5;=Yy2udm`7H_vkO;@P;SHY+ekOIeDR^1Sjhu-8jSuR)a(c2 zaQ=`HX=Y?F@k+baz>e*au~fD9V(`?pLtEYnf0zmHh#FfDt_g?vN(*dNl^R1nwR)-Z zgcL^JqSCZ!!+*&&!|I@_I8hw#D7p{jsR4mtfy~(?@J$reEYNJBu}NzIJf-=;!v(*W z7tR#a<3o7pbTW|UmkCWVxJ^;2k>kC$esE2uVLnYOGl%iX9gH5H5VI)nyJ`y&Y2Z9P zae`Yzf3U=Uei;mB=Rly`YhOb5No|wsy$zR8)2X5?_)RlgLw#77L0F!jJfs>J+-;0`gvfsrH^Q1BrYVc5lf2^yUi9r7vHWYqr0)`Di$W5%l9w3 z_{(w!H(*EP?)W-u=~M9CCLLj+$wKG>!Q&j0 zM!AVMMm}ck9~P`XD23B*&u?kD5TKVq2OdU{h)4UN*2&h7WUcZ7d!KCWk};4>4;7FVEsRJeFab*-MZz0-~@Mf*Wj)>1b26L0ta_@cY;GWK!UrwTX1&|?ykeVGgJ4y zf97>pb?vU*YgO0YyQ{kU`_}rFrihkKGaaQ?Y95QI;2D|?YitC{>h*f;EVK7r4i)mK z|B~gOum-9DU@`LI8>N6-2{eM7Sp0guKT$vtd^>T`=O@m_;al?+@BU4DDv`i6Cx9F!h~IV^iTcX2gAn-G-O38-sk}>(Kl6*b zUOH^62m_pRii2p=J(+q1C8{G0Rk)A9kxpF{EYXfR$Fq94R(|-3iNDr`u4~+v1E!~m z>c=fMvbn)1CYxTV%IACMg6zkg_yb{O^ji~yrRV|un7Kh>wxKuY*3nJ-zCkTXMXTJo z-oAXivNR7G7x9vjqd{~nO1u3E38tiI&ap{s$OX7?P(H}pU`YR}-@e3@hCCEy;RD<# zbEe(V-Ws?zu$r12Dk48&os^QMcB+fN5QjMx`-|Wf5{?6 zeObYFx-T(GA0tCy8_0g{^QDXVb~F^StQHbkvtAHg{cF$5ZIu0>*s7jOR6_h?Y9ClS zU1@U8*R?g}I20Qg@C&i`w@U`kIupG-R$&0vq!Am(9_XO?O4D^GY<`$iWa{XrZ~j{} z_F4Ukwb5jO0wDlT4L;U>CZf}d`_;ml3wH{-tP(xg9?ak%2wGY^WU#Kjsf7+ucCA?qb z&}>5A0!2KI$^eQUbCgtWkEYd~2^lsrwa#a6y;t`$c?KJjXAiufVu;B5A zv;%BXhXwkvG5Rj~0Si$_EMYO@rkwCF4$UQrE_b8ycYy3fL7+4qZ<4b8M4T};bNlgS zqcMW87)3#r&A>!EA7&3z~<1|r7E(;3+xL}?%%X$kxetuPMrT3kKAEJDQn|WJ8a_Y ze4!uo$gx%je{>xN&X4>fh>}QEY#p{)sDo-UD)`S*OHO*5;caB30;6^*PRYx+2V))L zlhX*iZ&FiL5m!)je3M@6c^ns)a<1f2ZMAYByTmM_1U-ZC`x+aT z*1H;;f*PA{EYhW5O=EEe$y#W;(;Q6|X0h=0jL;4vw^Lh&(hm5RDyi|dX&2rlSVb`= zm8NoKn-nrT++(~c!D4+U#-qSd3w5}XuE?v?@}oX2}jP?ECYTCrNH0M2P!MoPULpn?LQLN zRR)3^jbING0-Ytuhr8#kMV_>a3|Y$j7A(g?7G}WvD|q(o+S*?C?p&>Wl9taaBWeB6 z-!gXwZ?Ne=WU~Yk78q?;>dkG|6Rh{2o#1$@Nf-AFt{EZp=PLRt?CabtJ6eN$lMUlVm!l; zcRo*E!G!Y5w#Y}XA*ODq`>vy{p_^1&9D6ko=kC&9A|z4$u263!GzZPC?d(YMJ05TL z=wy^!!#nqE{@hy_qKZCwN;);&x%!!f#FqdI<1vFs|h)0m$0 zkbEZCxpuu$w=%Fd7sr*h;at<)CcOBPkU6-wCa~b`>Zz@NYw4uu#@R(bcOU)==%e51 zM2t^K!v;f_{7Qh&shK_tym}FLx|g5-91yZ}h+|?TI)|C3smagIr)je!mnr-f&T01s zCyesD;)~xq*{B=+Nez^LpsP~>7cQpN-5Ce%{86y}8|abz*eiI#6G=HZ6TC6GBN)D!J7&$+2YgQ@Pe?{r3w`AQst$kekM1Iso-?zXWdN=amX}0L%mXCU~Th zTU_#R2olpL(ueU};}NrpsJ%SjAoMNo{Llam41r@DL$4T)){MYNJ@0PovENlq;o)Y~ zapPKWCBuc`>Hjs06l-{Q%y=lBiOgb?&uZv#<6SF&*t1nM_~&9KHD<d)( zIN95oIXW4+*gF3^ZftrgxZ=9PlX(7$g)2CAmLoL#$8Jz+G@}r>tRDB(N|HX}9vFsA z7KIU^nv_g*uW`zKlEwsB2cUy#fsSRthgo%+w5TF$ifT<&*Kw7lRPKQl7QAO0pCd4I4S%-~XEHUxuyrCRt@5%p_| z+S`p8DhfH6hbr*x|1dZ<5Uiblg?4_UhTBZCc@(Pa9uOyj{x$2CRS3&`bgoDv`!M@< zTYdZX)8)%Ig;VqAM(QUNILHd-+kPD*()H1jNdmf-MGaG|o5Iozj)OTtix5jA+fhL&#bGybgQ+IAjC2COo^aD5(BfQxt)1y-acoA zY8dW6TjMu#9^uJ0fXM+#?z&C}if;|IQdSP_M^R#>jZ{+8npK>$fU|{@vxNNg>JAfn ztdkQLCL39>kKc9|$#1m8-sgq_x!|$r-hm{D@_@`*-udR)jnb^AObE7Tx7cC~FzPHL z#7R9uMvMmMCt!u?QSFX{1T|^N%Z<@MTj&@)yHfBq=5qNSbam^;<#(`G_Y3=b>U58; zc%b~4GAC(S)*$qu*$aWuRUU#Kp5XVYuUM(Eo$lQL^^qc(oys&01%ost{6p=0=7ovL z;OUaw1(qvK?4$1(pu&*1qd)r}gc?^Yf3W1!wBoUUIL!yMQ*+}A*tDSi7&C}~M$RlS z|D8uF2&z^|+at%^9P2$Ck^iGMWg9|WT8${U;aSI8K%!?+J5WDAna$6<&zzH%C@I={;j|FgkWTCy37D0%3(_19hEYmC$FF5s)X5vg)sFX#2C9*a zM6Tc3+6d7k7yF~-&`On*S?MADCax(Dji6!#0BPT6NZfQzYd6OEkzUwyPM)89G(;tc zk>{0J|W}ed|qHX zDI*_{c9%2ie?vMRDC)Hy@M%t`71TRG`WjPHipk1Z^As~(I*~Ttk{Zs$NvPXX&pwN4 z4=3+>Wu;i2dKwWXMnjqSG9DbV@>Bq{G;8Ve-zFun(36}NIl8Z+L!A@BNop2*#3d&k zvGI2B^q%z1$OyHUl{@$Y_ow(XEuM2IG@~(IC}7DK61@b1QaQ-te(G}!rlg8PUP9Yt ziG=peJgq|oC+&^91Dx1>6L)kfOsKtZJ7SlG00s+te~ZyGqA zB4)tFt2Ses*prjBr{9|{y86Mk>&`r|EYX)c@z-U(gJK8K@B6imU~h6vBIJ0hNWl86~8gQ}n3s1%PICx z7S9}~UJp$Zqp3VXqw#okdM)d`O(~_*zk4fqGrKOC)zjs2-1Ms{`jIhj>3)R}{xvbt z5xQcqh_xe*4H42UPq;s|*VP;XB2*ghlHu|O$xIk8oOZ+{O}Y@d@i~ndOHf;4k;K`N zkPuqH#~5GZZl4T()_W^E-o6#tXQe2K8K5jhAik08g35!;K@}ILOjr^Lr_b9`jlPpo z4-xfSTxgXeOs0wxxEXerEtrY>ke}GvgDq_#4!mbuolGKReB2u})N_Qr(8NVh}aY_FE%`G%aFek6G_pS7$6s>sW3(^7nD z($<-o>u~hqxvr|VeXp71L5&`fCs~o7NQ>RVVGA^DgO*tREV2ET#bZN~fd%p#xgb{& zvkUy;U37v4Eq84b@>V@B^MKVNiv9hxhu4{GeL_9nO}0v!if9m%k7dW*r*c%Rts}6A z16$;FN5$ZL4gNj(cZ5gS6WKGa>{GggEC%<)t0{Z1>{;T%ioEHaSNNlBkaIljUsJc? zv5N;Nb1O#ZgFFj|G_Rn><0Ejff~rRx=%RJCJC076hO;Pnm*=c@D`RH0KPdVH3QJ^M zx`#XN^ZO&!_xk9x#LXGsVXcbHz!=rYKm8n@tmX!;6w@x6JFq{MFJ#T06Sj^ zZE&yx+Mv^BYs5W!AeTrU5m|Z5Ms$3(uIb1CT7gCa@{kZBNnzwU?-kbWmN#v{)gkR1 zpw#?#=|zlbH6{`HJb#Jx?rmwWMfpB+w#{r>f0~7^=8sP{t7eDPO!1v~F=_f_@^7W# zgiX5=cxm!WM?#RVR`gP_0S?Jf1m3<#d84e}q+nh}3a5klA z$Fm#@Ml~C=B?1z*`go*2&ZNO#z7`oVPAIoY5qxWD^^S!O(OCkASOR6Ip8$>D7K6Dt z9_A4owJ2`uFGbo=`2C4&k9b8}(Y&4{vFe17GcFIR%*_I%LZ<%2P#-(vv6*R+n*E@F z)jvWp3$X?N+1k`B+1X;X8iToR@O}!5{yQT~yDI%|6h2TWEGKl+K?LVHB_I_k@n|F@ z)jNG!1S`~n>X`?d#va(2()51c*={0TI_c}CZl6t(*mHv*L7X2{+*e_$lUF9&!1^aY zyxMyoO2P=y!a1L?oNP`fM5#kZ4v0YRK@=^)d!GG;z`y-(mRi|8A|ZptFJ*(1ztDr0Mof0QSpzqlP1 zA|)vef-8h_x%o|5f7DZE8Sz@oV|Z528f|t( zd&-eBLhq+WBThha04~ufVH6KBZuwX zkLa!yS^b9iOk1FqqZ=%a;xqFvH4$ss^lKluuY5kTM_)bi2z&aheohAtfg>CRaAd>O zB?8>ge>810b3|OTLzImQDQB)Vo;1YNJol2-XJQ0#XuU3;n^DuCv?$Dfme7eP&arP2 zDu2x=+tOm?KFTloGAl13Wrrsp5`IWdjCbfW9{o4vc{!H;-Gt*h5%REGc!*`+iFV?D%8Pt2bn$C3*#8w@bxT%;o*{@&zNSWm1Q~ zA?-@*Is+kuAvsyFe^Be3LU8;kIe1V@qsED6=`z6pUa!r4Uo^8?kk2@{ELO>@=I7r~ zG-taG%z#*)gr}Oo=REkt7bW88frq*^wV0FpJOd_%#Z753RSIzhV#v6npPJkkKnAK|0o!HQj(m- z$J2J*b()8-?3U+t7W+RJpp=QXZC=gYghj)pOZZtAnWvl3A=UouESRryiQDHs_XxEMU(Q?Rxew{F++ zjC~H&_4teS=ykJ>ZjSUNsF{;yA*$3Lq*_Qz5iLN0B4niP-)uCY-!~8o(}91HT&E- zKbh~D##N?m&Bg*p41CnuYb#}6s?qR{z>mT7UzC2~&l=({O(|9dzcLoLI(sHalSn+J ztX^UY6jM;&L~Y4zm&44PKYy!Ig%(@de-01g7l$!N$(IK4X=% zJBetJp;{>jd>uM`Rg$e_VxMS9UC$?|L;r5m0=FwUk_F=F!S+xzP7>(9KUqJS)&K1S zr9nA=MdY*VxPxTh+WiqjkGeY?8PZX# z&UGH&-V14VT4;#o9u(H?P3J`U?ySp8-Ph>XUm4ywPJVvp1~`s$G5iCuek%K2G`nCD zgKlG|XV7gyNTljA5AP(Bt3$i=4?@=?;kZ1IS10XUL8FZaQ#NlTqBFsuDgZw?np)J9LuxhpMVzAK#vnJw*rFqxRO0z(!A~6(hI- zE}W0YDo*mOfI}%si8mIW?ZdZ%!UnB_(@AZyQC6#)W{B-?!9BcD;!M|AdGP7A(-YWX zJSIeky7@tWXJ_2;BwZi}nH@I;QUhVt&es6iHvlonsr5-`Sk!w|H-oopBwLoEY({AS zV_mGu-nUHgUV1Ie;a4(IVy4;|H$3wE?;u*pR=w0rguE~EK!|mNH;yZV8&Dzz7;<_; zjyHK6(`R4@4nyt(NEo+pUu9da+m!03eEpI8vi~taJ{)=wVp?3mz$ei!Nk;Sj*Y!H zcW8nS($9t|+RMLyf?H@E>lMNpwygInQ`^fH6S|+UvhvJP{Dq@4G1h1t%L=bTQn}Qv zI3um8{N;BkjC;lpOpTIzb&YTou)XjI@lnt(;E^Sh0;guOv|vg4;ScttpLdZl0FJiV zieR52Drq+5T$$yLWz93N{7A?+#sEwwO)(ufq}4J4FW;VRjZ8%!b(WYSJBt&{$QX(G z9k##*MC$MfS{|>uvvCm*ly%rMytILutmb-G-aLRNQ^I8S8^`hP~_h1Jmx)aIaF%#+G^=1c{YT~om=8bWd*s122?1lEkHS5mz)7Spi zyXWhmJNc*>1QOx+zh&NSq1stS^@?7n*c}oU@Ru!cZ(O0Zmog;-OQK`qrH}2WpU8sc z(c^avHLg)ZBnM2iFhIC`S>4k-E&rVUlzO@kZ6QAvr zq8BcCr&;uM5D;NLJRCW@a|?UvI8Wx;9nlK8f8<%%8m5mn0^z1O*2wh5JmtiqaaeLV z*;DN!(S@7;Q0Cm-PN(}!m379R8Wj^Z?I2}_KzHLrZh^M93x3`8Z&rw6i&gPGkc;Ar zSSxp_Zu08k`vFOsj2uH5CAeBpwavdd2&hy7O7p6_AM$ZqE%tFI5 z*)8AdyZsXHUwjVVkv~t=j>XhozA-o@nPTPn7A&hmSYuZsD3${1|9x<5DEU7v1Vx5S zE$Cl`kg7G2ObniYd}5xc(X30L>S8Eng2+^V2YD$W*c2-}HV%Z7IHzU?M)cW}SAT9{ zgkPF)**8}FZufa-vijngF~;uvZD63$$Ie#gxfyQQ4H5lJ0#cM+ct}IhDva%SIL|E0 z__sbwvm|s-$o5ztk#Wk!Nt3}v_T)xt?gQ19VmG%j$_v_3FzY3(81^Kq;6~MT$1Fjb z{quJuwbx|ftgi{QP4aAHlMup^;`M>{zTp|0-opps8%6ni0r%d$dyYo})lloFr5s*< z$zcD3YRwrM9lBsPZnuG(*&WmHV}ewob&`X} zrp#hT;Z23wZl;#xy@SDw`=*z@Dx7PqWrQ5hDq5OavxhMBwQOW_LfKUIK~`;8p^Qt< zLGx>Bnw+k|!ea7c3mWgP6ASFSQyP&Hvq9^nYHioDRHD*LCa6arO8W9Mp2>Q6M8 z_|>^;wOs}B%?8_3|KI`!;0qoD+*wM+R`btTvIiueR$u-8`|oqHsPFhD5CA|LGyw2n zI|{J+ZffSv>~3efmn`EH$ch9COsQy1A)!r{M9stLNAWGnaiipldP_ z+tEVc8(rELz6cS=jZ{~vh9Oaxlx(?d4jyu`;L$hz!c%a9qaG?`_PkBK6hM%}ZMOM} zIenIxq41BDsq|})!o@JVZ zSqyfa*4UtXWAt|s?QMKJli7uDCpp;K>18Fmhgx~xsJar0C`q z{<)03+|C<8<@P|>p zKmEu4!=v?a%m3#2Z*FA&#Q#&#_aFQ>y8nv*zxuxaivRxwcK(C+|MvfedH#w1=eqt6 cx>NtZta3#eXqbO>!F=pNAHytV_^-471w#4bhX4Qo diff --git a/examples/somersaultecu.py b/examples/somersaultecu.py index 2440f0ae..d80020cb 100755 --- a/examples/somersaultecu.py +++ b/examples/somersaultecu.py @@ -32,6 +32,7 @@ from odxtools.docrevision import DocRevision from odxtools.environmentdata import EnvironmentData from odxtools.environmentdatadescription import EnvironmentDataDescription +from odxtools.exceptions import odxrequire from odxtools.functionalclass import FunctionalClass from odxtools.modification import Modification from odxtools.multiplexer import Multiplexer @@ -669,21 +670,22 @@ class SomersaultSID(IntEnum): long_name=None, semantic=None, description=None, - diag_coded_type=somersault_diagcodedtypes["uint16"], + diag_coded_type=somersault_diagcodedtypes["uint8"], byte_position=0, coded_value=uds.positive_response_id( SID.TesterPresent.value), # type: ignore[attr-defined] bit_position=None, sdgs=[], ), - CodedConstParameter( + ValueParameter( short_name="status", long_name=None, semantic=None, description=None, - diag_coded_type=somersault_diagcodedtypes["uint8"], + dop_ref=OdxLinkRef("somersault.DOP.uint8", doc_frags), + dop_snref=None, + physical_default_value_raw="0", byte_position=1, - coded_value=0x00, bit_position=None, sdgs=[], ), @@ -941,6 +943,17 @@ class SomersaultSID(IntEnum): ), } +# this is a hack to get around a catch-22: we need to specify the +# value of a positive response to the tester present parameter to +# specify ISO_15765_3.CP_TesterPresentMessage communication parameter, +# but we need the comparam for the raw diaglayer which we need for +# retrieving the DOP of the "status" parameter in order to convert the +# raw physical default value. +param = somersault_positive_responses["tester_ok"].parameters.status +assert isinstance(param, ValueParameter) +param._dop = somersault_dops["uint8"] +param._physical_default_value = int(odxrequire(param.physical_default_value_raw)) + # negative responses somersault_negative_responses = { "general": From 4b3e514be541654306c84b3316d247c8c6e57ccc Mon Sep 17 00:00:00 2001 From: Andreas Lauser Date: Fri, 26 Apr 2024 10:23:30 +0200 Subject: [PATCH 3/5] EncodeState: provide a default value for `coded_message` this makes instantiation of that class more convenient and in 90% of all cases, this is supposed to be the empty anyway... Signed-off-by: Andreas Lauser Signed-off-by: Alexander Walz --- odxtools/encodestate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odxtools/encodestate.py b/odxtools/encodestate.py index 0eb8dffe..7cb0208e 100644 --- a/odxtools/encodestate.py +++ b/odxtools/encodestate.py @@ -18,7 +18,7 @@ class EncodeState: """ #: payload that has been constructed so far - coded_message: bytearray + coded_message: bytearray = field(default_factory=bytearray) #: the bits of the payload that are used used_mask: bytearray = field(default_factory=bytearray) From f325e8f252f9941fb6e463395fdffa7ac61f2e5a Mon Sep 17 00:00:00 2001 From: Andreas Lauser Date: Fri, 26 Apr 2024 10:25:14 +0200 Subject: [PATCH 4/5] fix the new false positive overlapping parameter warnings in the tests for length- and table keys, this required to modify the `encode_placeholder_into_pdu()` functions because the bits occupied by placeholders do not count as used. Signed-off-by: Andreas Lauser Signed-off-by: Alexander Walz --- odxtools/leadinglengthinfotype.py | 6 ++++++ odxtools/parameters/lengthkeyparameter.py | 5 ++++- odxtools/parameters/tablekeyparameter.py | 10 ++++++---- odxtools/request.py | 3 +-- odxtools/response.py | 3 +-- tests/test_diag_coded_types.py | 13 ++++++++----- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/odxtools/leadinglengthinfotype.py b/odxtools/leadinglengthinfotype.py index d702539f..a1e7ca01 100644 --- a/odxtools/leadinglengthinfotype.py +++ b/odxtools/leadinglengthinfotype.py @@ -55,6 +55,12 @@ def encode_into_pdu(self, internal_value: AtomicOdxType, encode_state: EncodeSta byte_length = self._minimal_byte_length_of(internal_value) + used_mask = None + bit_pos = encode_state.cursor_bit_position + if encode_state.cursor_bit_position != 0 or (bit_pos + self.bit_length) % 8 != 0: + used_mask = (1 << self.bit_length) - 1 + used_mask <<= bit_pos + encode_state.emplace_atomic_value( internal_value=byte_length, used_mask=None, diff --git a/odxtools/parameters/lengthkeyparameter.py b/odxtools/parameters/lengthkeyparameter.py index 7a3127b3..9410f6a9 100644 --- a/odxtools/parameters/lengthkeyparameter.py +++ b/odxtools/parameters/lengthkeyparameter.py @@ -114,7 +114,10 @@ def encode_placeholder_into_pdu(self, physical_value: Optional[ParameterValue], encode_state.cursor_byte_position = pos encode_state.cursor_bit_position = self.bit_position or 0 - self.dop.encode_into_pdu(encode_state=encode_state, physical_value=0) + # emplace a value of zero into the encode state, but pretend the bits not to be used + n = odxrequire(self.dop.get_static_bit_length()) + encode_state.cursor_bit_position + tmp_val = b'\x00' * ((n + 7) // 8) + encode_state.emplace_bytes(tmp_val, obj_used_mask=tmp_val) encode_state.cursor_byte_position = max(encode_state.cursor_byte_position, orig_cursor) encode_state.cursor_bit_position = 0 diff --git a/odxtools/parameters/tablekeyparameter.py b/odxtools/parameters/tablekeyparameter.py index 253dd2d4..564bdc33 100644 --- a/odxtools/parameters/tablekeyparameter.py +++ b/odxtools/parameters/tablekeyparameter.py @@ -178,14 +178,16 @@ def encode_placeholder_into_pdu(self, physical_value: Optional[ParameterValue], odxraise(f"No KEY-DOP specified for table {self.table.short_name}") return - size = key_dop.get_static_bit_length() - - if size is None: + sz = key_dop.get_static_bit_length() + if sz is None: odxraise("The DOP of table key {self.short_name} must exhibit a fixed size.", EncodeError) return - encode_state.emplace_bytes(bytes([0] * (size // 8)), self.short_name) + # emplace a value of zero into the encode state, but pretend the bits not to be used + n = sz + encode_state.cursor_bit_position + tmp_val = b'\x00' * ((n + 7) // 8) + encode_state.emplace_bytes(tmp_val, obj_used_mask=tmp_val) encode_state.cursor_byte_position = max(orig_pos, encode_state.cursor_byte_position) encode_state.cursor_bit_position = 0 diff --git a/odxtools/request.py b/odxtools/request.py index 4fba2fe7..d7f6a924 100644 --- a/odxtools/request.py +++ b/odxtools/request.py @@ -24,8 +24,7 @@ def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> return Request(**kwargs) def encode(self, **kwargs: ParameterValue) -> bytes: - encode_state = EncodeState( - coded_message=bytearray(), triggering_request=None, is_end_of_pdu=True) + encode_state = EncodeState(is_end_of_pdu=True) self.encode_into_pdu(physical_value=kwargs, encode_state=encode_state) diff --git a/odxtools/response.py b/odxtools/response.py index 3876d377..a80e5519 100644 --- a/odxtools/response.py +++ b/odxtools/response.py @@ -39,8 +39,7 @@ def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> return Response(response_type=response_type, **kwargs) def encode(self, coded_request: Optional[bytes] = None, **kwargs: ParameterValue) -> bytes: - encode_state = EncodeState( - coded_message=bytearray(), triggering_request=coded_request, is_end_of_pdu=True) + encode_state = EncodeState(triggering_request=coded_request, is_end_of_pdu=True) self.encode_into_pdu(physical_value=kwargs, encode_state=encode_state) diff --git a/tests/test_diag_coded_types.py b/tests/test_diag_coded_types.py index ed5123cb..edd9cb80 100644 --- a/tests/test_diag_coded_types.py +++ b/tests/test_diag_coded_types.py @@ -88,7 +88,10 @@ def test_decode_leading_length_info_type_bytefield2(self) -> None: is_highlow_byte_order_raw=None, ) - state = EncodeState(bytearray.fromhex("0000ff00"), cursor_bit_position=3) + state = EncodeState( + coded_message=bytearray.fromhex("0000ff00"), + used_mask=bytearray.fromhex("0700ffff"), + cursor_bit_position=3) dct.encode_into_pdu(bytes([0xcc]), state) self.assertEqual(state.coded_message.hex(), "08ccff00") self.assertEqual(state.cursor_byte_position, 2) @@ -687,7 +690,7 @@ def test_encode_min_max_length_type_hex_ff(self) -> None: termination="HEX-FF", is_highlow_byte_order_raw=None, ) - state = EncodeState(coded_message=bytearray([0x00]), is_end_of_pdu=False) + state = EncodeState(is_end_of_pdu=False) dct.encode_into_pdu(bytes([0x34, 0x56]), state) self.assertEqual(state.coded_message, bytes([0x34, 0x56, 0xFF])) @@ -700,7 +703,7 @@ def test_encode_min_max_length_type_zero(self) -> None: termination="ZERO", is_highlow_byte_order_raw=None, ) - state = EncodeState(coded_message=bytearray([0x00]), is_end_of_pdu=False) + state = EncodeState(is_end_of_pdu=False) dct.encode_into_pdu("Hi", state) self.assertEqual(state.coded_message, bytes([0x48, 0x69, 0x0])) @@ -739,7 +742,7 @@ def test_encode_min_max_length_type_min_length(self) -> None: termination=termination, is_highlow_byte_order_raw=None, ) - state = EncodeState(coded_message=bytearray([0x00]), is_end_of_pdu=True) + state = EncodeState(is_end_of_pdu=True) dct.encode_into_pdu(bytes([0x34, 0x56]), state) self.assertTrue(state.coded_message.hex().startswith("3456")) self.assertRaises( @@ -760,7 +763,7 @@ def test_encode_min_max_length_type_max_length(self) -> None: termination=termination, is_highlow_byte_order_raw=None, ) - state = EncodeState(coded_message=bytearray([0x00]), is_end_of_pdu=True) + state = EncodeState(is_end_of_pdu=True) dct.encode_into_pdu(bytes([0x34, 0x56, 0x78]), state) self.assertEqual(state.coded_message, bytes([0x34, 0x56, 0x78])) self.assertRaises( From 3f5012cdc002e752e97924be61b04e408c343713 Mon Sep 17 00:00:00 2001 From: Andreas Lauser Date: Fri, 26 Apr 2024 10:30:15 +0200 Subject: [PATCH 5/5] wipe the used bits of an object before the actual data is placed this allows to ignore garbage data which might be located at an object's position. Additionally, only set the bits which are actually used by the object to be placed. Signed-off-by: Andreas Lauser Signed-off-by: Alexander Walz --- odxtools/encodestate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/odxtools/encodestate.py b/odxtools/encodestate.py index 7cb0208e..dba83d54 100644 --- a/odxtools/encodestate.py +++ b/odxtools/encodestate.py @@ -212,11 +212,12 @@ def emplace_bytes(self, for i in range(len(new_data)): if self.used_mask[pos + i] & obj_used_mask[i] != 0: warnings.warn( - "Overlapping objects detected", + f"Overlapping objects detected at position {pos + i}", OdxWarning, stacklevel=1, ) - self.coded_message[pos + i] |= new_data[i] + self.coded_message[pos + i] &= ~obj_used_mask[i] + self.coded_message[pos + i] |= new_data[i] & obj_used_mask[i] self.used_mask[pos + i] |= obj_used_mask[i] self.cursor_byte_position += len(new_data)