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
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.
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).
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:
# 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,
)
)
Messages sent through the provider are unaffected by radios and use the handler’s default medium:
# 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()