Skip to content

Multi-radio systems (Radio Extension)

This guide shows how to use the Radio extension in prototype-mode to override communication medium settings (range, delay, failure rate) on a per-message basis. It complements the standard CommunicationHandler by letting a protocol use multiple radios with distinct characteristics.

The guide will follow an example protocol that uses the radio extension to simulate a node with two radios: a short-range radio and a long-range radio.

What the Radio does

  • Creates one or more logical radios inside a protocol
  • Each radio has its own CommunicationMedium
  • transmission_range (m)
  • delay (s)
  • failure_rate (0..1)
  • Messages sent through a radio use that radio’s configuration, other messages are unaffected
  • Only available in the Python simulator; requires a CommunicationHandler

API reference

Instantiation

Create Radio objects in initialize() or later. The initialize method marks the simulation's initialization. Radios instantiated before the simulation starts raise an error.

Protocol.initialize: create radios
    def initialize(self) -> None:
        # Two radios with distinct characteristics
        self.short_radio = Radio(self)
        self.long_radio = Radio(self)

Warning

Extensions only work in the Python simulator. Instantiating a Radio in other environments raises an error.

Configuring

Each Radio's configuration starts with the handler’s default medium (same range, delay and failure as the global CommunicationHandler). You can then override only the fields you need by calling .set_configuration. The parameters that set_configuration accepts are the same as those of the CommunicationMedium. All parameters are optional, with unspecified ones retaining their previous value.

In this example we're configuring two radios with different ranges, one with a short range (10 m) and one with a long range (100 m).

Per-radio range overrides
        self.short_radio.set_configuration(transmission_range=10)
        self.long_radio.set_configuration(transmission_range=100)

Sending messages

Messages sent through a Radio use that radio’s medium:

Broadcast via short-range radio
            # 1) Short-range broadcast: only nearby nodes (<=10 m) receive it
            self.short_radio.send_communication_command(
                CommunicationCommand(
                    command_type=CommunicationCommandType.BROADCAST,
                    message="hello_short",
                )
            )
Unicast via long-range radio
            # 2) Long-range unicast to node 2 (50 m away)
            self.long_radio.send_communication_command(
                CommunicationCommand(
                    command_type=CommunicationCommandType.SEND,
                    message="ping_long",
                    destination=2,
                )
            )

Messages sent through the provider are unaffected by radios and use the handler’s default medium:

Provider send (unaffected by radios)
            # 3) Direct provider send uses the handler's default medium (range=60 m)
            self.provider.send_communication_command(
                CommunicationCommand(
                    command_type=CommunicationCommandType.SEND,
                    message="via_provider",
                    destination=2,
                )
            )

In this example we send three messages: - A broadcast via the short-range radio (10 m) - A unicast via the long-range radio (100 m) - A unicast via the provider (default range 60 m)

Three nodes are placed such that: - Node 1 is within 10 m of Node 0 - Node 2 is within 100 m of Node 0, but outside 10 m

The expected result in this scenario is that Node 1 receives all three messages, while Node 2 only receives the long-range and provider messages. This example illustrates how protocols can use multiple radios with different configurations to simulate multi-radio systems with different capabilities.

We chose to only override the transmission range in this example, but you can also override delay and failure rate per-radio if needed, or any other field of the CommunicationMedium.

As expected, running the example yields the following output:

INFO:root:[--------- Simulation started ---------]
INFO     [--------- Simulation started ---------]
INFO:root:Node 0 received: []
INFO     [it=3 time=0:00:00 | RadioProtocol 0 Finalization] Node 0 received: []
INFO:root:Node 1 received: ['hello_short']
INFO     [it=3 time=0:00:00 | RadioProtocol 1 Finalization] Node 1 received: ['hello_short']
INFO:root:Node 2 received: ['ping_long', 'via_provider']
INFO     [it=3 time=0:00:00 | RadioProtocol 2 Finalization] Node 2 received: ['ping_long', 'via_provider']
INFO:root:[--------- Simulation finished ---------]
INFO     [--------- Simulation finished ---------]
INFO:root:Real time elapsed: 0:00:00    Total iterations: 3 Simulation time: 0:00:00
INFO     Real time elapsed: 0:00:00 Total iterations: 3 Simulation time: 0:00:00

Full example

Full protocol code
from __future__ import annotations

import logging
from typing import List

from gradysim.protocol.interface import IProtocol
from gradysim.protocol.messages.telemetry import Telemetry
from gradysim.protocol.messages.communication import (
    CommunicationCommand,
    CommunicationCommandType,
)
from gradysim.simulator.extension.radio import Radio


class RadioProtocol(IProtocol):
    """
    Demo protocol that instantiates two radios with different ranges and sends a few messages
    during initialization. It also collects any received messages for display on finish().
    """

    def __init__(self) -> None:
        self.received: List[str] = []
        self.short_radio: Radio | None = None
        self.long_radio: Radio | None = None
        self._logger = logging.getLogger()

    def initialize(self) -> None:
        # Two radios with distinct characteristics
        self.short_radio = Radio(self)
        self.long_radio = Radio(self)

        self.short_radio.set_configuration(transmission_range=10)
        self.long_radio.set_configuration(transmission_range=100)

        # Only node 0 performs the demo transmissions
        if self.provider.get_id() == 0:
            # 1) Short-range broadcast: only nearby nodes (<=10 m) receive it
            self.short_radio.send_communication_command(
                CommunicationCommand(
                    command_type=CommunicationCommandType.BROADCAST,
                    message="hello_short",
                )
            )

            # 2) Long-range unicast to node 2 (50 m away)
            self.long_radio.send_communication_command(
                CommunicationCommand(
                    command_type=CommunicationCommandType.SEND,
                    message="ping_long",
                    destination=2,
                )
            )

            # 3) Direct provider send uses the handler's default medium (range=60 m)
            self.provider.send_communication_command(
                CommunicationCommand(
                    command_type=CommunicationCommandType.SEND,
                    message="via_provider",
                    destination=2,
                )
            )

    def handle_timer(self, timer: str) -> None:
        pass

    def handle_packet(self, message: str) -> None:
        self.received.append(message)

    def handle_telemetry(self, telemetry: Telemetry) -> None:
        pass

    def finish(self) -> None:
        node_id = self.provider.get_id()
        ordered = sorted(self.received)
        self._logger.info(f"Node {node_id} received: {ordered}")
Full execution code
from __future__ import annotations

import logging

from gradysim.simulator.simulation import SimulationBuilder
from gradysim.simulator.handler.communication import CommunicationHandler
from gradysim.protocol.position import Position

from radio_protocol import RadioProtocol


def main() -> None:
    logging.basicConfig(level=logging.INFO)

    builder = SimulationBuilder()

    # Three nodes in a line: 0 at origin, 1 at 5m (in short range), 2 at 50m (out of short range)
    positions: list[Position] = [
        (0.0, 0.0, 0.0),  # Node 0
        (5.0, 0.0, 0.0),  # Node 1 – should get short broadcast
        (50.0, 0.0, 0.0),  # Node 2 – outside short, inside long and provider default
    ]

    for pos in positions:
        builder.add_node(RadioProtocol, pos)

    builder.add_handler(CommunicationHandler())

    sim = builder.build()
    sim.start_simulation()


if __name__ == "__main__":
    main()