Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[run]
omit =
tests/*
26 changes: 25 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ jobs:
pip install -e .
pip install pytest
pip install pytest-asyncio
pip install coverage

- name: Run pytest
run: pytest tests
run: coverage run -m pytest tests/

- name: Generate coverage HTML report
run: coverage html

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: './htmlcov'

- name: Generate coverage json
run: coverage json -o coverage.json

- name: Upload coverage json to gist
env:
GIST_ID: ${{ secrets.GIST_ID }}
GIST_TOKEN: ${{ secrets.GIST_TOKEN }}
run: |
# Create a GitHub CLI authentication token
echo "$GIST_TOKEN" | gh auth login --with-token

# Update existing gist
gh gist edit $GIST_ID coverage.json
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ venv/
integration_tests/build
integration_tests/install

build/
build/

.coverage
htmlcov/
*.pdf
*.bash
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# someipy - A Python Library implementing the SOME/IP Protocol

![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgist.githubusercontent.com%2Fchrizog%2F6a2e6f355eedf38ae3af74dcdb7b30a1%2Fraw%2F61630b4d2cc9c04aae78b3cca0cbc9c56e76f0e5%2Fcoverage_someipy_v1.json&query=totals.percent_covered_display&suffix=%20%25&label=Line%20%Coverage)


## Get in Contact :postbox:

If you want to connect, have a feature request, bug report or need support, send me an email or connect on LinkedIn:
Expand All @@ -23,7 +26,7 @@ someipy is based on the specification version of R22-11:
- [SOME/IP Protocol Specification](https://www.autosar.org/fileadmin/standards/R22-11/FO/AUTOSAR_PRS_SOMEIPProtocol.pdf)
- [SOME/IP Service Discovery Protocol Specification](https://www.autosar.org/fileadmin/standards/R22-11/FO/AUTOSAR_PRS_SOMEIPServiceDiscoveryProtocol.pdf)

The library is currently developed and tested under Ubuntu 22.04 and Python 3.8.
The library is currently developed and tested under Ubuntu 22.04 and Python 3.8. Windows is supported as well.

## Typical Use Cases

Expand Down
6 changes: 6 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ HEAD

Code changes to ``master`` that are *not* in the latest release:

Release v2.1.0
-----------------
- Support Windows: Use tcp socket for daemon communication on Windows systems.
- Add support for broadcasting SOME/IP SD messages over UDP.
- Implement stop subscribe functionality in the `someipyd` daemon.

Release v2.0.0
-----------------
- Introduced a new architecture with a `someipyd` daemon for centralized network handling.
Expand Down
8 changes: 8 additions & 0 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ The next step is to connect to the someipy daemon. The daemon is a separate proc

In case, a non-default Unix Domain Socket path is used, a config dictionary can be passed to the *connect_to_someipy_daemon* function.

If Windows is used, TCP sockets have to be used for communication between the application and the someipy daemon. In this case, the *connect_to_someipy_daemon* function has to be called with a config dictionary containing the keys *use_tcp*, *tcp_host* and optionally *tcp_port*.

.. code-block:: python

someipy_daemon = await connect_to_someipy_daemon(
{"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
)


Defining the SOME/IP Service
----------------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ someipy is based on the specification version of R22-11:
- `SOME/IP Protocol Specification <https://www.autosar.org/fileadmin/standards/R22-11/FO/AUTOSAR_PRS_SOMEIPProtocol.pdf>`_
- `SOME/IP Service Discovery Protocol Specification <https://www.autosar.org/fileadmin/standards/R22-11/FO/AUTOSAR_PRS_SOMEIPServiceDiscoveryProtocol.pdf>`_

The library is developed and tested on Ubuntu 22.04 and using Python 3.8.
The library is developed and tested on Ubuntu 22.04 and using Python 3.8. Windows is supported as well.

For Inquiries
-------------
Expand Down
9 changes: 7 additions & 2 deletions docs/someipy_daemon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ The configuration file is a JSON file that allows you to customize the daemon's
"sd_port": 30490,
"log_level": "DEBUG",
"interface": "127.0.0.2",
"log_path": "/var/log/someipy.log"
}
"log_path": "/var/log/someipy.log",
"use_tcp": false,
"tcp_host": "127.0.0.1",
"tcp_port": 30500

- ``sd_address``: The multicast address for Service Discovery.
- ``sd_port``: The port for Service Discovery.
- ``log_level``: The logging level (e.g., DEBUG, INFO, WARNING, ERROR).
- ``interface``: The network interface to bind to.
- ``log_path``: The path to the log file.
- ``use_tcp``: Whether to use TCP sockets instead of UDS sockets for communication between the daemon and clients. If Windows is used `use_tcp` must be set to true.
- ``tcp_host``: The host address for TCP communication (only relevant if `use_tcp` is true).
- ``tcp_port``: The port for TCP communication (only relevant if `use_tcp` is true).
5 changes: 5 additions & 0 deletions example_apps/call_method_tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

addition_method = Method(
id=SAMPLE_METHOD_ID,
protocol=TransportLayerProtocol.TCP,
Expand Down
5 changes: 5 additions & 0 deletions example_apps/call_method_udp.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

addition_method = Method(
id=SAMPLE_METHOD_ID,
protocol=TransportLayerProtocol.UDP,
Expand Down
5 changes: 5 additions & 0 deletions example_apps/offer_method_tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

addition_method = Method(
id=SAMPLE_METHOD_ID,
protocol=TransportLayerProtocol.TCP,
Expand Down
5 changes: 5 additions & 0 deletions example_apps/offer_method_udp.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

addition_method = Method(
id=SAMPLE_METHOD_ID,
protocol=TransportLayerProtocol.UDP,
Expand Down
5 changes: 5 additions & 0 deletions example_apps/offer_multiple_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

temperature_event = Event(id=SAMPLE_EVENT_ID, protocol=TransportLayerProtocol.UDP)

temperature_eventgroup = EventGroup(
Expand Down
10 changes: 8 additions & 2 deletions example_apps/receive_events_tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

temperature_event = Event(id=SAMPLE_EVENT_ID, protocol=TransportLayerProtocol.TCP)
temperature_eventgroup = EventGroup(
id=SAMPLE_EVENTGROUP_ID, events=[temperature_event]
Expand Down Expand Up @@ -87,17 +92,18 @@ async def main():
service_instance_temperature.register_callback(temperature_callback)

# The second argument is the time to live (TTL) of the subscription in seconds
service_instance_temperature.subscribe_eventgroup(temperature_eventgroup, 5.0)
service_instance_temperature.subscribe_eventgroup(temperature_eventgroup, 5)

try:
# Keep the task alive
await asyncio.Future()
except asyncio.CancelledError as e:
print("Shutdown..")
finally:
print("Unsubscribe eventgroup..")
service_instance_temperature.unsubscribe_eventgroup(temperature_eventgroup)

print("Shutdown service instance..")

await someipy_daemon.disconnect_from_daemon()

print("End main task..")
Expand Down
10 changes: 8 additions & 2 deletions example_apps/receive_events_udp.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

temperature_event = Event(id=SAMPLE_EVENT_ID, protocol=TransportLayerProtocol.UDP)
temperature_eventgroup = EventGroup(
id=SAMPLE_EVENTGROUP_ID, events=[temperature_event]
Expand Down Expand Up @@ -86,17 +91,18 @@ async def main():
service_instance_temperature.register_callback(temperature_callback)

# The second argument is the time to live (TTL) of the subscription in seconds
service_instance_temperature.subscribe_eventgroup(temperature_eventgroup, 5.0)
service_instance_temperature.subscribe_eventgroup(temperature_eventgroup, 5)

try:
# Keep the task alive
await asyncio.Future()
except asyncio.CancelledError as e:
print("Shutdown..")
finally:
print("Unsubscribe eventgroup..")
service_instance_temperature.unsubscribe_eventgroup(temperature_eventgroup)

print("Shutdown service instance..")

await someipy_daemon.disconnect_from_daemon()

print("End main task..")
Expand Down
5 changes: 5 additions & 0 deletions example_apps/send_events_tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

temperature_event = Event(id=SAMPLE_EVENT_ID, protocol=TransportLayerProtocol.TCP)

temperature_eventgroup = EventGroup(
Expand Down
5 changes: 5 additions & 0 deletions example_apps/send_events_udp.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ async def main():

someipy_daemon = await connect_to_someipy_daemon()

# For Windows use:
# someipy_daemon = await connect_to_someipy_daemon(
# {"use_tcp": True, "tcp_host": interface_ip, "tcp_port": 30500}
# )

temperature_event = Event(id=SAMPLE_EVENT_ID, protocol=TransportLayerProtocol.UDP)

temperature_eventgroup = EventGroup(
Expand Down
1 change: 0 additions & 1 deletion integration_tests/automated_tests/run_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

vsomeip_library_path = "/home/christian/projects/someip/vsomeip_install/lib/"
current_file_path = os.path.abspath(__file__)
repository = os.path.dirname(os.path.dirname(current_file_path))
repository = os.path.dirname(os.path.dirname(os.path.dirname(current_file_path)))
test_durations = 60 # duration of each test in seconds

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = someipy
version = 1.0.0
version = 2.1.0
author = Christian H.
author_email = someipy.package@gmail.com
description = A Python package implementing the SOME/IP protocol
Expand Down
4 changes: 3 additions & 1 deletion src/someipy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@
from ._internal.method_result import MethodResult # noqa: F401
from ._internal.return_codes import ReturnCode # noqa: F401
from ._internal.message_types import MessageType # noqa: F401
from ._internal.someipy_daemon_client import connect_to_someipy_daemon # noqa: F401
from ._internal._daemon.someipy_daemon_client import (
connect_to_someipy_daemon,
) # noqa: F401
33 changes: 33 additions & 0 deletions src/someipy/_internal/_common/endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright (C) 2025 Christian H.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import ipaddress
from typing import NamedTuple, Union


class Endpoint(NamedTuple):
"""Represents a network endpoint with IP address and port."""

ip: Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
port: int

@property
def is_ipv4(self):
return isinstance(self.ip, ipaddress.IPv4Address)

def __str__(self):
if isinstance(self.ip, ipaddress.IPv6Address):
return f"[{self.ip}]:{self.port}"
return f"{self.ip}:{self.port}"
48 changes: 48 additions & 0 deletions src/someipy/_internal/_common/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (C) 2025 Christian H.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import asyncio
from typing import TypeVar, Generic, Optional, Callable, Any


T = TypeVar("T")


class Event(Generic[T]):
def __init__(self):
self._handlers: list[Callable[[object, Optional[T]], Any]] = []

def add_handler(self, handler: Callable[[object, Optional[T]], Any]) -> None:
if handler not in self._handlers:
self._handlers.append(handler)

def remove_handler(self, handler: Callable[[object, Optional[T]], Any]) -> None:
if handler in self._handlers:
self._handlers.remove(handler)

async def invoke(self, sender: object, e: Optional[T] = None) -> None:
"""Invoke all handlers, handling both sync and async automatically"""
for handler in self._handlers[:]:
result = handler(sender, e)
if asyncio.iscoroutine(result):
await result

def __iadd__(self, handler: Callable[[object, Optional[T]], Any]) -> "Event[T]":
self.add_handler(handler)
return self

def __isub__(self, handler: Callable[[object, Optional[T]], Any]) -> "Event[T]":
self.remove_handler(handler)
return self
Empty file.
Loading