Skip to content

Commit

Permalink
Add example app for sending events
Browse files Browse the repository at this point in the history
  • Loading branch information
chrizog committed Feb 4, 2024
1 parent 854c07a commit df350a9
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 5 deletions.
Empty file added example_apps/__init__.py
Empty file.
112 changes: 112 additions & 0 deletions example_apps/send_events.py
Original file line number Diff line number Diff line change
@@ -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
79 changes: 79 additions & 0 deletions example_apps/temperature_msg.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 6 additions & 1 deletion src/_internal/someip_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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)]
2 changes: 1 addition & 1 deletion src/logging.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions src/service_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit df350a9

Please sign in to comment.