diff --git a/example_apps/__init__.py b/example_apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example_apps/send_events.py b/example_apps/send_events.py new file mode 100644 index 0000000..c1e9c03 --- /dev/null +++ b/example_apps/send_events.py @@ -0,0 +1,112 @@ +import sys +sys.path.append("..") + +import asyncio +import ipaddress +import logging + +import src.service_discovery +import src._internal.someip_header +import src._internal.someip_sd_header +import src.server_service_instance +from src.logging import set_someipy_log_level +from src.serialization import Uint8, Uint64, Float32 +from temperature_msg import TemparatureMsg + +SD_MULTICAST_GROUP = "224.224.224.245" +SD_PORT = 30490 +INTERFACE_IP = "127.0.0.1" + +SAMPLE_EVENTGROUP_ID = 20 +SAMPLE_EVENT_ID = 32796 + +async def main(): + + # It's possible to configure the logging level of the someipy library, e.g. logging.INFO, logging.DEBUG, logging.WARN, .. + set_someipy_log_level(logging.DEBUG) + + # Since the construction of the class ServiceDiscoveryProtocol is not trivial and would require an async __init__ function + # use the construct_service_discovery function + # The local interface IP address needs to be passed so that the src-address of all SD UDP packets is correctly set + service_discovery = await src.service_discovery.construct_service_discovery(SD_MULTICAST_GROUP, SD_PORT, INTERFACE_IP) + + # 1. For sending events use a ServerServiceInstance + # 2. Pass the service and instance ID, version and endpoint and TTL. The endpoint is needed again as the src-address + # and port of all sent events + # 3. The ServiceDiscoveryProtocol object has to be passed as well, so the ServerServiceInstance can offer his service to + # other ECUs + # 4. cyclic_offer_delay_ms is the period of sending cyclic SD Offer service entries + service_instance_temperature = src.server_service_instance.ServerServiceInstance( + service_id=1, + instance_id=1000, + major_version=1, + minor_version=0, + endpoint=(ipaddress.IPv4Address(INTERFACE_IP), 3000), # src IP and port of the service + ttl=5, + sd_sender=service_discovery, + cyclic_offer_delay_ms=2000 + ) + + # The service instance has to be attached always to the ServiceDiscoveryProtocol object, so that the service instance + # is notified by the ServiceDiscoveryProtocol about e.g. subscriptions from other ECUs + service_discovery.attach(service_instance_temperature) + + # For demonstration purposes we will construct a second ServerServiceInstance + service_instance_2 = src.server_service_instance.ServerServiceInstance( + service_id=2, + instance_id=2000, + major_version=1, + minor_version=0, + endpoint=(ipaddress.IPv4Address(INTERFACE_IP), 3001), # src IP and port of the service + ttl=5, + sd_sender=service_discovery, + cyclic_offer_delay_ms=2000 + ) + service_discovery.attach(service_instance_2) + + # After constructing and attaching ServerServiceInstances to the ServiceDiscoveryProtocol objects the + # start_offer method has to be called. This will start an internal timer, which will periodically send + # Offer service entries with a period of "cyclic_offer_delay_ms" which has been passed above + print("Start offering services..") + service_instance_temperature.start_offer() + service_instance_2.start_offer() + + tmp_msg = TemparatureMsg() + + # Reminder: Do NOT write "tmp_msg.version.major = 1". Always use the provided classes in someipy like Uint8, + # so that the data can be propery serialized. Python literals won't be serialized properly + tmp_msg.version.major = Uint8(1) + tmp_msg.version.minor = Uint8(0) + + tmp_msg.measurements.data[0] = Float32(20.0) + tmp_msg.measurements.data[1] = Float32(21.0) + tmp_msg.measurements.data[2] = Float32(22.0) + tmp_msg.measurements.data[3] = Float32(23.0) + + try: + # Either cyclically send events in an endless loop.. + while True: + await asyncio.sleep(5) + tmp_msg.timestamp = Uint64(tmp_msg.timestamp.value + 1) + payload = tmp_msg.serialize() + service_instance_temperature.send_event(SAMPLE_EVENTGROUP_ID, SAMPLE_EVENT_ID, payload) + + # .. or in case your app is waiting for external events, using await asyncio.Future() to + # keep the task alive + # await asyncio.Future() + except asyncio.CancelledError as e: + print("Stop offering services..") + await service_instance_temperature.stop_offer() + await service_instance_2.stop_offer() + finally: + print("Service Discovery close..") + service_discovery.close() + + print("End main task..") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt as e: + pass diff --git a/example_apps/temperature_msg.py b/example_apps/temperature_msg.py new file mode 100644 index 0000000..27e4427 --- /dev/null +++ b/example_apps/temperature_msg.py @@ -0,0 +1,79 @@ +import sys +sys.path.append("..") + +from src.serialization import * + +# With someipy it's possible to either send and receive payloads unserialized simply as bytes-objects +# You can also define the payloads structure directly as Python classes and serialize your Python object +# to a bytes-payload which can be sent. You can also deserialize a received bytes-object into your defined +# Python object structure. + +# In this example we'll define a "temperature message" that consists of another SOME/IP struct and of a fixed size +# SOME/IP array + +@dataclass +class Version(SomeIpPayload): + major: Uint8 + minor: Uint8 + + def __init__(self): + # Ensure that you always write an __init__ function for each struct so that the variables + # are instance variables owned by each object + self.major = Uint8() + self.minor = Uint8() + # Reminder: Do NOT write "self.major = 1". Always use the provided classes in someipy like Uint8, + # so that the data can be propery serialized. Python literals won't be serialized properly + # Instead use self.major = Uint8(1) + + +@dataclass +class TemparatureMsg(SomeIpPayload): + # Always define payloads with the @dataclass decorator. This leads to the __eq__ being + # generated which makes it easy to compare the content of two messages + # For defining a payload struct just derive from the SomeIpPayload class. This will ensure + # the Python object can be serialized and deserialized and supports e.g. len() calls which + # will return the length of the payload in bytes + + version: Version + timestamp: Uint64 + measurements: SomeIpFixedSizeArray + + def __init__(self): + self.version = Version() + self.timestamp = Uint64() + self.measurements = SomeIpFixedSizeArray(Float32, 4) + # Arrays can be modelled using the SomeIpFixedSizeArray class which gets the type that + # the array shall hold (e.g. Float32) and the number of elements + # The len(self.measurements) call will return the number of bytes (4*len(Float32)). + # If you need to now the number of elements use len(self.measurements.data). + + +# Simple example how to instantiate a payload, change values, serialize and deserialize +if __name__ == "__main__": + + tmp_msg = TemparatureMsg() + + tmp_msg.version.major = Uint8(2) + tmp_msg.version.minor = Uint8(0) + # Reminder: Do NOT use "tmp_msg.version.major = 2". Always use the provided classes by someipy like Uint8, + # so that the data can be propery serialized. Python literals won't be serialized into SOME/IP payload. + + tmp_msg.timestamp = Uint64(100) + + tmp_msg.measurements.data[0] = Float32(20.0) + tmp_msg.measurements.data[1] = Float32(20.1) + tmp_msg.measurements.data[2] = Float32(20.2) + tmp_msg.measurements.data[3] = Float32(20.3) + + # The @dataclass decorator will also generate a __repr__ function + print(tmp_msg) + + # serialize() will return a bytes object + output = tmp_msg.serialize() + print(output.hex()) + + # Create a new TemperatureMsg from the serialized bytes + tmp_msg_again = TemparatureMsg().deserialize(output) + print(tmp_msg_again) + + assert(tmp_msg_again == tmp_msg) diff --git a/src/_internal/someip_header.py b/src/_internal/someip_header.py index 0b38005..fa5f5a9 100644 --- a/src/_internal/someip_header.py +++ b/src/_internal/someip_header.py @@ -71,4 +71,9 @@ def from_buffer(cls, buf: bytes): ) def to_buffer(self) -> bytes: - return struct.pack(">HHIHHBBBB", self.service_id, self.method_id, self.length, self.client_id, self.session_id, self.protocol_version, self.interface_version, self.message_type, self.return_code) \ No newline at end of file + return struct.pack(">HHIHHBBBB", self.service_id, self.method_id, self.length, self.client_id, self.session_id, self.protocol_version, self.interface_version, self.message_type, self.return_code) + +def get_payload_from_someip_message(someip_header: SomeIpHeader, payload: bytes) -> bytes: + length = someip_header.length + payload_length = length - 8 # 8 bytes for request ID, protocol version, etc. + return payload[16:(16+payload_length)] diff --git a/src/logging.py b/src/logging.py index 91e89ce..a4657c6 100644 --- a/src/logging.py +++ b/src/logging.py @@ -1,6 +1,6 @@ import logging -_log_level = logging.DEBUG +_log_level = logging.INFO def set_someipy_log_level(log_level: int): global _log_level diff --git a/src/serialization.py b/src/serialization.py index 404fee2..a896bdd 100644 --- a/src/serialization.py +++ b/src/serialization.py @@ -179,6 +179,8 @@ def deserialize(self, payload): (self.value,) = struct.unpack(">f", payload) return self + def __eq__(self, other) -> Bool: + return self.serialize() == other.serialize() @dataclass class Float64: @@ -193,6 +195,9 @@ def serialize(self) -> bytes: def deserialize(self, payload): (self.value,) = struct.unpack(">d", payload) return self + + def __eq__(self, other) -> Bool: + return self.serialize() == other.serialize() def serialize(obj) -> bytes: diff --git a/src/service_discovery.py b/src/service_discovery.py index d55a60c..6ce64a6 100644 --- a/src/service_discovery.py +++ b/src/service_discovery.py @@ -85,12 +85,12 @@ def datagram_received(self, data: bytes, addr: Tuple[Union[str, Any], int]) -> N for o in self.attached_observers: o.subscribe_eventgroup_update(event_group_entry, ipv4_endpoint_option) - for event_group_entry in extract_subscribe_ack_eventgroup_entries(someip_header): + for event_group_entry in extract_subscribe_ack_eventgroup_entries(someip_sd_header): _logger.debug( f"Received subscribe ACK for instance 0x{event_group_entry.sd_entry.instance_id:04X}, service 0x{event_group_entry.sd_entry.service_id:04X}, eventgroup 0x{event_group_entry.eventgroup_id:04X}" ) - for o in self.attached_observers: - o.sub + #for o in self.attached_observers: + # o.sub def connection_lost(self, exc: Exception) -> None: pass diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 78383fd..e9063e7 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -36,6 +36,7 @@ def __init__(self): self.z = Float64(0.0) +@dataclass class MsgWithOneStruct(SomeIpPayload): # Expected 1 + 13 + 4 bytes in total a: Uint8