Adding support for a new testbed

To add support for a new testbed, the following tasks need to be done:

Add testbed specific code blocks

Add a module with the name like spectrumwars.testbed.xxx. This module should define two classes:

  • Testbed, a subclass of TestbedBase, and
  • Radio, a subclass of RadioBase.
Add testbed specific unit tests
Add tests to a Python file with the name like tests/test_xxx.py. Please make the tests automatically skip themselves if the testbed-specific hardware is not connected (e.g. raise the unittest.SkipTest exception)
Add testbed documentation
Add testbed documentation for players to docs/reference.rst. Add any testbed-specific installation instructions to docs/installtestbed.rst.

Class reference

class Testbed

Testbed objects represent a physical testbed that is used to run a game. Unless stated otherwise, subclasses should override all of the methods and attributes described below.

The class constructor can take optional string-typed keyword arguments. These can be specified in spectrumwars_runner using the -O arguments.

RADIO_CLASS

Should be set to the subclass of the RadioBase class that is used by the testbed (i.e. Radio in the testbed’s module)

get_radio_pair()

Returns a rxradio, txradio tuple. rxradio and txradio should be instances of RADIO_CLASS.

This method is called multiple times by the game controller, once for each player to obtain the interfaces to player’s radios. It is called before the game starts, and before the call to start().

start()

Called once, immediately before the start of the game.

stop()

Called after the game concluded. This method should perform any clean-up tasks required by the testbed (e.g. stopping any threads started by start().

get_frequency_range()

Returns the number of frequency channels available to player’s code. The value returned should not change during the lifetime of the object.

Corresponds to Transceiver.get_frequency_range().

get_bandwidth_range()

Returns the number of bandwidth settings available to player’s code. The value returned should not change during the lifetime of the object.

Corresponds to Transceiver.get_bandwidth_range().

get_power_range()

Returns the number of transmission power settings available to player’s code. The value returned should not change during the lifetime of the object.

Corresponds to Transceiver.get_power_range().

get_spectrum()

Returns the current state of the radio spectrum as a list of floating point values.

The value returned by this method gets assigned to GameStatus.spectrum.

See also usrp_sensing.SpectrumSensor.

time()

Returns the current testbed time in seconds since epoch as a floating point number. Selection of an epoch does not matter. Game controller requires only that time increases monotonically.

By default it returns time.time(), which should be sufficient for most testbeds.

class Radio

Radio objects represent a player’s interface to a single transceiver. Unless stated otherwise, subclasses should override all of the methods described below.

PACKET_SIZE

Set to the maximum length of a string that can be passed to the send() method.

Approximately corresponds to Transceiver.get_packet_size(). Game controller adds a header to separate control data from payload which adds an overhead of a few bytes. Because of this, the player visible maximum packet size will be lower.

set_configuration(frequency, bandwidth, power)

Set up the transceiver for transmission or reception of packets on the specified central frequency, power and bandwidth.

frequency is specified as channel number from 0 to N-1, where N is the value returned by the Testbed.get_frequency_range() method.

bandwidth is specified as an integer specifying the radio bitrate and channel bandwidth in the interval from 0 to N-1, where N is the value returned by the Testbed.get_bandwidth_range() method. Higher values mean higher bitrates and wider channel bandwidths.

power is specified as an integer specifying the transmission power in the interval from 0 to N-1, where N is the value returned by the Testbed.get_power_range() method. Higher values mean lower power.

Corresponds to Transceiver.set_configuration().

binsend(bindata)

Send a data packet over the air.

bindata is a binary string with the data to be included into the packet. Length of bindata can be up to PACKET_SIZE.

Corresponds to Transceiver.send(). Note that the game controller packs the packet with payload, so bindata will not be identical to the data string passed to Transceiver.send().

binrecv(timeout=None)

Return a packet from the receive queue.

timeout specifies the receive timeout in seconds. If no packet is received within the timeout interval, the method raises RadioTimeout exception.

Upon successfull reception, the method should return a binary string. The returned string should be equal to the bindata parameter that was passed to the corresponding send() call.

Note

There is no way for the Radio class to push packets towards the game controller. Instead, the game controller polls the radio for received packets by calling recv() method, as instructed by player’s code. Hence it is in most cases necessary that the actual packet reception happens in another thread (started typically from Testbed.start()) and that the received packets are held in a queue until the next recv() call.

Corresponds to Transceiver.recv(). Note that the game controller unpacks the payload from the packet before passing it to Transceiver.recv().

class usrp_sensing.SpectrumSensor(base_hz, step_hz, nchannels, time_window=200e-3, gain=10)

usrp_sensing.SpectrumSensor is a simple, reusable spectrum sensor implementation using a USRP device.

The sensing algorithm is inspired by a real-time signal analyzer. The recorded samples are converted into power spectral density using continuous end-to-end FFTs with no blind time (and no overlap of the FFT windows). The spectral power density is then averaged over a time window.

The algorithm is very CPU intensive. Using a 2.7 GHz CPU, it will be able to sense at most 64 channels (even if USRP frontend bandwidth would allow for more).

Sensing in this way is necessary because the radios usually have a very low duty cycle (e.g. a “while True: send()” has only around 10% duty cycle on the VESNA testbed). If we would only take one sample the spectrum when players request it, it would mostly appear empty. Hence the need to take a moving average if sensing is to be useful for detecting player transmissions.

base_hz is the lower bound of the frequency band used in the game in hertz. step_hz is the width of each channel. nchannels is the number of channels used in the game. The values for these parameters should be chosen so that the channel frequencies correspond to the channels used by the testbed’s Radio class:

-------------------------------> frequency (Hz)

+---+---+     +---+
| 0 | 1 | ... | n | (channels used in the game)
+---+---+     +---+

|---| <- step_hz

|-----------------| <- step_hz * nchannels

^
|

base_hz

time_window defines the length of the moving average filter in seconds. The value depends on how often players can look up the current state of the spectrum. In most cases it should be longer than the period of Transceiver.status_update() events in the event-based model.

start()

Start the worker thread. Should be called before first call to get_spectrum()

stop()

Stop the worker thread.

get_spectrum()

Returns the current state of the radio spectrum as a list of floating point values. Length of the list is equal to nchannels.

The value returned by this method can be directly used as the return value of Testbed.get_spectrum().