Skip to content

Start passing destination address to callbacks on message received #61

Description

@khauersp

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:

  1. The current callback already has 5 positional arguments, which is at the edge of usability
  2. J1939-22 FD support may require additional fields in the future (e.g., frame_format)
  3. A dataclass provides better developer experience (types, IDE support, self-documenting)
  4. Since this is a breaking change anyway, we should choose the most future-proof design

Questions for Discussion

  1. What should the class be named? J1939Message, ReceivedMessage, PDU?
  2. Should we include a raw_can_id field for debugging purposes?
  3. Is there a preferred alternate solution?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions