Skip to content
Open
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
38 changes: 38 additions & 0 deletions python/packages/jumpstarter-driver-someip/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,44 @@ with env() as client:
someip.close_connection()
```

### Server / Provider (act as an ECU)

The same driver can also *provide* SOME/IP services — offer them via Service
Discovery, answer RPC requests with canned responses, and publish events. This
turns a Jumpstarter exporter into a simulated ECU, useful for exercising a
device-under-test that is a SOME/IP *client* of another ECU.

RPC handlers run inside the exporter process, so responses are configured
declaratively rather than via a per-request callback: set a canned response for
a `(service_id, method_id)` and the server serves it. This maps naturally onto
getter-style SOME/IP methods; update the response to change what a client reads.

```python
from jumpstarter.common.utils import env

with env() as client:
someip = client.someip

# Offer a service instance (starts the server on first use)
someip.offer_service(0x1801, instance_id=0x0001, major_version=1)

# Answer an RPC method with a fixed payload (E_OK by default)
someip.set_method_response(0x1801, 0x0005, b"\x01\x02\x03\x04")
# ...or return an error return code
someip.set_method_response(0x1801, 0x0006, b"", return_code=0x01)

# Publish events to subscribers of an event group
someip.register_event(0x1801, 0x8001, eventgroup_id=1)
someip.publish_event(0x1801, 0x8001, b"\x2d\x00")
# Field events are cached and served to new subscribers
someip.set_field(0x1801, 0x8002, b"\x01")

# Introspect / tear down
print(someip.list_offered_services())
someip.stop_offer_service(0x1801, 0x0001)
someip.stop_server()
```

## API Reference

```{eval-rst}
Expand Down
21 changes: 21 additions & 0 deletions python/packages/jumpstarter-driver-someip/examples/exporter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,24 @@ export:
transport_mode: UDP
remote_host: "192.168.100.10"
remote_port: 30490
---
# Provider / server mode - offer services, answer RPC, and publish events
# (act as a simulated ECU that a device-under-test's SOME/IP client talks to).
# Bind to the interface the DUT shares; services are offered at runtime via the
# offer_service / set_method_response / publish_event client verbs.
apiVersion: jumpstarter.dev/v1alpha1
kind: ExporterConfig
metadata:
namespace: default
name: someip-server-exporter
endpoint: ""
token: ""
export:
someip:
type: jumpstarter_driver_someip.driver.SomeIp
config:
host: "192.168.100.1"
port: 30490
transport_mode: UDP
multicast_group: "239.127.0.1"
multicast_port: 30490
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .common import (
SomeIpEventNotification,
SomeIpMessageResponse,
SomeIpOfferedService,
SomeIpPayload,
SomeIpServiceEntry,
)
Expand Down Expand Up @@ -34,9 +35,7 @@ def rpc_call(
) -> SomeIpMessageResponse:
"""Make a SOME/IP RPC call and return the response."""
msg = SomeIpPayload(data=payload.hex())
return SomeIpMessageResponse.model_validate(
self.call("rpc_call", service_id, method_id, msg, timeout)
)
return SomeIpMessageResponse.model_validate(self.call("rpc_call", service_id, method_id, msg, timeout))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a formatting change, do we want to change it?


# --- Raw Messaging ---

Expand All @@ -52,9 +51,7 @@ def send_message(

def receive_message(self, timeout: float = 2.0) -> SomeIpMessageResponse:
"""Receive a raw SOME/IP message."""
return SomeIpMessageResponse.model_validate(
self.call("receive_message", timeout)
)
return SomeIpMessageResponse.model_validate(self.call("receive_message", timeout))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here


# --- Service Discovery ---

Expand All @@ -80,9 +77,7 @@ def unsubscribe_eventgroup(self, eventgroup_id: int) -> None:

def receive_event(self, timeout: float = 5.0) -> SomeIpEventNotification:
"""Receive the next event notification."""
return SomeIpEventNotification.model_validate(
self.call("receive_event", timeout)
)
return SomeIpEventNotification.model_validate(self.call("receive_event", timeout))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here


# --- Connection Management ---

Expand All @@ -93,3 +88,67 @@ def close_connection(self) -> None:
def reconnect(self) -> None:
"""Reconnect to the SOME/IP endpoint."""
self.call("reconnect")

# --- Server / provider side ---

def start_server(self) -> None:
"""Force-start the SOME/IP server (otherwise started on first offer)."""
self.call("start_server")

def offer_service(
self,
service_id: int,
instance_id: int = 0x0001,
major_version: int = 1,
minor_version: int = 0,
) -> None:
"""Offer a service instance for discovery (act as the providing ECU)."""
self.call("offer_service", service_id, instance_id, major_version, minor_version)

def stop_offer_service(
self,
service_id: int,
instance_id: int = 0x0001,
major_version: int = 1,
minor_version: int = 0,
) -> None:
"""Withdraw a previously offered service instance."""
self.call("stop_offer_service", service_id, instance_id, major_version, minor_version)

def list_offered_services(self) -> list[SomeIpOfferedService]:
"""Return the set of services this server currently offers."""
result = self.call("list_offered_services")
return [SomeIpOfferedService.model_validate(v) for v in result]

def set_method_response(
self,
service_id: int,
method_id: int,
payload: bytes,
return_code: int = 0,
) -> None:
"""Configure the canned response the server returns for an RPC method."""
msg = SomeIpPayload(data=payload.hex())
self.call("set_method_response", service_id, method_id, msg, return_code)

def clear_method_response(self, service_id: int, method_id: int) -> None:
"""Remove a configured RPC response."""
self.call("clear_method_response", service_id, method_id)

def register_event(self, service_id: int, event_id: int, eventgroup_id: int) -> None:
"""Register an event for publishing under an event group."""
self.call("register_event", service_id, event_id, eventgroup_id)

def publish_event(self, service_id: int, event_id: int, payload: bytes) -> None:
"""Publish an event notification to subscribers of its event group."""
msg = SomeIpPayload(data=payload.hex())
self.call("publish_event", service_id, event_id, msg)

def set_field(self, service_id: int, event_id: int, payload: bytes) -> None:
"""Set a field event value (served to new subscribers and notified)."""
msg = SomeIpPayload(data=payload.hex())
self.call("set_field", service_id, event_id, msg)

def stop_server(self) -> None:
"""Stop the SOME/IP server, withdrawing all offers."""
self.call("stop_server")
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,12 @@ class SomeIpEventNotification(BaseModel):
@classmethod
def _validate_hex(cls, v: str) -> str:
return _validate_hex_string(v)


class SomeIpOfferedService(BaseModel):
"""A service instance the server is currently offering (server-side introspection)."""

service_id: int = Field(ge=0, le=0xFFFF)
instance_id: int = Field(ge=0, le=0xFFFF)
major_version: int = Field(default=1, ge=0, le=0xFF)
minor_version: int = Field(default=0, ge=0, le=0xFFFFFFFF)
Loading
Loading