Breaking Change: This proposal introduces a breaking change to the subscriber callback API. All existing callbacks will need to be updated.
Summary
The current subscriber callback interface does not include the destination address, even though the ECU internally receives this information from the data link layer. This RFC proposes adding destination address to subscriber callbacks and evaluates three implementation approaches.
Background
Current Behavior
The data link layers (J1939-21 and J1939-22) call _notify_subscribers with 6 parameters including the destination address:
# j1939_21.py:362, j1939_22.py:600, etc.
self.__notify_subscribers(mid.priority, pgn, mid.source_address, dest_address, timestamp, data)
However, ElectronicControlUnit._notify_subscribers() uses dest only for filtering and does not pass it to the callback:
# electronic_control_unit.py:475-477
for dic in snapshot:
if (dic['dev_adr'] is None) or (dest == ParameterGroupNumber.Address.GLOBAL) or ...:
dic['cb'](priority, pgn, sa, timestamp, data) # dest NOT passed!
Current Callback Signature
def on_message(priority, pgn, sa, timestamp, data):
...
Problem
When a Controller Application can receive messages from multiple addresses (e.g., listening on multiple device addresses or using a callable filter), the subscriber callback has no way to know which destination address the message was sent to. This limits use cases where the destination address is semantically important.
Affected Files
| File |
Usage |
j1939/electronic_control_unit.py:477 |
Invokes callbacks |
j1939/memory_access.py:155 |
_listen_for_dm14() subscriber |
j1939/diagnostic_messages.py:263 |
Dm1._receive() subscriber |
test/helpers/feeder.py:97 |
Feeder._on_message() |
test/test_ca.py:302 |
Test callback |
test/test_threading.py:116,222 |
Test callbacks |
test/test_ecu.py:209 |
Test callback |
examples/*.py |
All example scripts |
README.md:109 |
Documentation example |
Proposed Options
Option A: Add Positional Argument
Add dest as the 4th positional argument:
def on_message(priority, pgn, sa, dest, timestamp, data):
...
Pros:
- Minimal change to existing pattern
- Simple implementation
Cons:
- 6 positional arguments is unwieldy and error-prone
- No forward compatibility for future additions
Option B: Use Keyword Arguments
Pass all parameters as keyword arguments:
dic['cb'](priority=priority, pgn=pgn, sa=sa, dest=dest, timestamp=timestamp, data=data)
Pros:
- Forward compatible (new args don't break callbacks using
**kwargs)
- Flexible for users who only need some parameters
Cons:
- Less explicit than a dataclass
- IDE support varies
Option C: Message Dataclass (Recommended)
Create a J1939Message dataclass containing all message fields:
from dataclasses import dataclass
@dataclass
class J1939Message:
"""Represents a received J1939 message."""
priority: int
pgn: int
source_address: int
dest_address: int
timestamp: float
data: bytearray
Usage:
def on_message(msg: j1939.J1939Message):
print(f"Received PGN {msg.pgn:#06x} from {msg.source_address:#04x} to {msg.dest_address:#04x}")
Pros:
- Most future-proof: new fields (e.g.,
frame_format for FD) won't break callbacks
- Cleaner API with IDE autocompletion and type hints
- Self-documenting: field names are always visible
- Easier to pass around, log, serialize
- Single argument is easier to extend than 6+ positional args
Cons:
- Minor object allocation overhead (negligible for most use cases)
Recommendation
Option C (Message Dataclass) is recommended because:
- The current callback already has 5 positional arguments, which is at the edge of usability
- J1939-22 FD support may require additional fields in the future (e.g.,
frame_format)
- A dataclass provides better developer experience (types, IDE support, self-documenting)
- Since this is a breaking change anyway, we should choose the most future-proof design
Questions for Discussion
- What should the class be named?
J1939Message, ReceivedMessage, PDU?
- Should we include a
raw_can_id field for debugging purposes?
- Is there a preferred alternate solution?
Summary
The current subscriber callback interface does not include the destination address, even though the ECU internally receives this information from the data link layer. This RFC proposes adding destination address to subscriber callbacks and evaluates three implementation approaches.
Background
Current Behavior
The data link layers (J1939-21 and J1939-22) call
_notify_subscriberswith 6 parameters including the destination address:However,
ElectronicControlUnit._notify_subscribers()usesdestonly for filtering and does not pass it to the callback:Current Callback Signature
Problem
When a Controller Application can receive messages from multiple addresses (e.g., listening on multiple device addresses or using a callable filter), the subscriber callback has no way to know which destination address the message was sent to. This limits use cases where the destination address is semantically important.
Affected Files
j1939/electronic_control_unit.py:477j1939/memory_access.py:155_listen_for_dm14()subscriberj1939/diagnostic_messages.py:263Dm1._receive()subscribertest/helpers/feeder.py:97Feeder._on_message()test/test_ca.py:302test/test_threading.py:116,222test/test_ecu.py:209examples/*.pyREADME.md:109Proposed Options
Option A: Add Positional Argument
Add
destas the 4th positional argument:Pros:
Cons:
Option B: Use Keyword Arguments
Pass all parameters as keyword arguments:
Pros:
**kwargs)Cons:
Option C: Message Dataclass (Recommended)
Create a
J1939Messagedataclass containing all message fields:Usage:
Pros:
frame_formatfor FD) won't break callbacksCons:
Recommendation
Option C (Message Dataclass) is recommended because:
frame_format)Questions for Discussion
J1939Message,ReceivedMessage,PDU?raw_can_idfield for debugging purposes?