Skip to content

Frame Router

Note

The Frame Router is the most prototypical part of Pax25 at the time of writing. It is highly subject to change. This is because there is not yet a networking layer, and so the current implementation exists mostly as a test harness for the applications API.

The FrameRouter object processes all AX.25 frames that are sent or received by the station. It routes frames from the interfaces from which they are received to the interfaces where they can be sent.

The FrameRouter object is instantiated by the Station object, and many applications will not use it directly. However, the fact the applications can examine the state of the FrameRouter, or even create their own frames for processing, is a key feature of pax25.

Special attributes

The FrameRouter object contains two special dictionaries which are useful for inspection, worthy of note:

FrameRouter.internal_addresses

internal_addresses: dict[Address, set[Interface[Any]]]

The internal_addresses dictionary contains a mapping of all SSIDs that are reserved within the station. That includes SSIDs that have been claimed by specific applications.

pax25.frame_router.FrameRouter

The Frame queue is a sort of router. It takes in frames from the varying interfaces and makes sure they go to their proper destination, whether that be an existing connection object or re-transcribed and sent out to another interface.

Source code in pax25/frame_router.py
class FrameRouter:
    """
    The Frame queue is a sort of router. It takes in frames from the varying interfaces
    and makes sure they go to their proper destination, whether that be an existing
    connection object or re-transcribed and sent out to another interface.
    """

    def __init__(self, *, station: "Station"):
        self.station = station
        self.matchers: dict[str, MatchCall] = {}
        self.last_transmission: None | datetime = None

    def process_frame(self, interface: "Interface", frame: Frame) -> None:
        """
        Interfaces call this function to put a new frame in the queue, to be interpreted
        as a received frame.

        This function must be resilient to avoid bringing down the station from one bug.
        """
        # Could change while iterating, since we might do something like register a
        # connection.
        if interface.type != "File":
            logger.debug(
                "Received from %s: %s",
                interface.name,
                LazyRepr(frame),
            )
        matchers = list(self.matchers.items())
        for key, match_call in matchers:
            try:
                if not match_call.matcher(frame, interface):
                    continue
            except Exception:
                logger.exception(
                    "Got error for matcher with key %s:",
                    repr(key),
                )
                continue
            try:
                match_call.notify(frame, interface)
            except Exception:
                logger.exception(
                    "Got error for notifier with key %s:",
                    repr(key),
                )
                continue

    def send_frame(
        self,
        interface: "Interface",
        frame: Frame,
        update_timestamp: bool = True,
    ) -> None:
        """
        Send a frame out on a specific interface. Also does some bookkeeping in the
        process, like checking if we're sending over a gateway and updating our
        .last_transmission stamp if so.

        In the future, it may be possible to filter outgoing packets, or otherwise
        listen for them.
        """
        if interface.name not in self.station.interfaces:
            # Interface was removed, and thus inert.
            logger.debug(
                "Dropping packet for %s (not in interface table): %s",
                (
                    interface.name,
                    LazyRepr(frame),
                ),
            )
            return
        if interface.gateway and update_timestamp:
            self.last_transmission = datetime.now(UTC)
        if interface.type != "File":
            logger.debug("Sending on %s: %s", interface.name, LazyRepr(frame))
        interface.send_frame(frame)

    def register_matcher(self, key: str, match_call: MatchCall) -> None:
        """
        Registers a matcher.
        """
        if key in self.matchers:
            logger.warning(
                "Existing matcher for key %s, %s, was replaced with new matcher %s. "
                "This may be a collision or a sign of a cleanup issue.",
                repr(key),
                self.matchers[key],
                match_call,
            )
        self.matchers[key] = match_call

    def remove_matcher(self, key: str) -> None:
        """
        Removes a matcher based on its key.
        """
        if key not in self.matchers:
            logger.warning(
                "Non-existent key removed, %s. This may indicate that the key was "
                "never registered, or cleanup has been called multiple times "
                "unnecessarily.",
                repr(key),
            )
            return
        del self.matchers[key]

process_frame(interface: Interface, frame: Frame) -> None

Interfaces call this function to put a new frame in the queue, to be interpreted as a received frame.

This function must be resilient to avoid bringing down the station from one bug.

Source code in pax25/frame_router.py
def process_frame(self, interface: "Interface", frame: Frame) -> None:
    """
    Interfaces call this function to put a new frame in the queue, to be interpreted
    as a received frame.

    This function must be resilient to avoid bringing down the station from one bug.
    """
    # Could change while iterating, since we might do something like register a
    # connection.
    if interface.type != "File":
        logger.debug(
            "Received from %s: %s",
            interface.name,
            LazyRepr(frame),
        )
    matchers = list(self.matchers.items())
    for key, match_call in matchers:
        try:
            if not match_call.matcher(frame, interface):
                continue
        except Exception:
            logger.exception(
                "Got error for matcher with key %s:",
                repr(key),
            )
            continue
        try:
            match_call.notify(frame, interface)
        except Exception:
            logger.exception(
                "Got error for notifier with key %s:",
                repr(key),
            )
            continue

send_frame(interface: Interface, frame: Frame, update_timestamp: bool = True) -> None

Send a frame out on a specific interface. Also does some bookkeeping in the process, like checking if we're sending over a gateway and updating our .last_transmission stamp if so.

In the future, it may be possible to filter outgoing packets, or otherwise listen for them.

Source code in pax25/frame_router.py
def send_frame(
    self,
    interface: "Interface",
    frame: Frame,
    update_timestamp: bool = True,
) -> None:
    """
    Send a frame out on a specific interface. Also does some bookkeeping in the
    process, like checking if we're sending over a gateway and updating our
    .last_transmission stamp if so.

    In the future, it may be possible to filter outgoing packets, or otherwise
    listen for them.
    """
    if interface.name not in self.station.interfaces:
        # Interface was removed, and thus inert.
        logger.debug(
            "Dropping packet for %s (not in interface table): %s",
            (
                interface.name,
                LazyRepr(frame),
            ),
        )
        return
    if interface.gateway and update_timestamp:
        self.last_transmission = datetime.now(UTC)
    if interface.type != "File":
        logger.debug("Sending on %s: %s", interface.name, LazyRepr(frame))
    interface.send_frame(frame)