This post presents the final design of the Cosa Virtual Wire Interface. It now includes many of the features discussed in the previous posts. The main enhancement is
reliable wireless communication on cheap RF433 transmitter and receiver modules with node addressing, message filtering, auto-retransmission and support for message dispatch. As before this Cosa sub-system may be used with ATtinyX5 to create ultra tiny and low power sensor devices.
The first section of the post describes the modifications to the Cosa VWI Receiver and Transmitter classes, adding addressing and message tags. The second section describes the
new VWI::Transceiver class which provides message acknowledgement with auto-retransmission and reduces much of the complexity of designing and implementing wireless message protocols.
To achieve this the original Virtual Wire Interface (VWI) has been extended. A new overloaded
send() method is introduced to allow gathering of multiple buffers (
iovec_t). This makes implementation of communication protocols much easier and reduces message memory buffer copying.
To improve performance of retransmission a new
resend() method is also introduced. This method will simple retransmit the message already encoded in the transmission buffer. There are also access methods for the next message sequence counter as this will be needed later for retransmission. Below is the updated
VWI::Transmitter API.
Fig.1: Enhanced VWI Transmitter Member Functions
The enhanced mode is enabled by providing
a 32-bit node address when calling
VWI::begin(). Below is the
setup() from the example sketch
CosaVWItempsensor.ino to give an idea how this works.
// Connect RF433 transmitter to Arduino/D9
VirtualWireCodec codec;
VWI::Transmitter tx(Board::D9, &codec);
const uint16_t SPEED = 4000;
const uint32_t ADDR = 0xC05a0002;
...
void setup()
{
...
// Start the Virtual Wire Interface/Transmitter
VWI::begin(ADDR, SPEED);
tx.begin();
...
}
The
blue section is the only change needed to enable addressing from the sensor node, i.e. the new address parameter. The
loop() of the temperature sensor will read two 1-Wire Thermometers and send the values together with a reading of the power supply voltage. The message is defined as a struct (
sample_t) and given a message type tag,
SAMPLE_CMD. This is used as an extra parameter to the
send() method and provides the receiver with information about what type of message the payload contains. See the
blue sections below.
// Message from the device; temperature and voltage reading
const uint8_t SAMPLE_CMD = 42;
struct sample_t {
int16_t temperature[2];
uint16_t voltage;
};
...
void loop()
{
... // Initiate the message with measurements
sample_t msg;
msg.temperature[0] = indoors.get_temperature();
msg.temperature[1] = outdoors.get_temperature();
msg.voltage = AnalogPin::bandgap(1100);
// Enable wireless transmitter and send. Wait completion and disable
VWI::enable();
tx.send(&msg, sizeof(msg), SAMPLE_CMD);
tx.await();
VWI::disable();
...
}
The
VWI::Receiver class is also enhanced with filtering of incoming messages. The API is not changed, i.e. the
recv() method has the same prototype. Incoming messages are matched against the receiver node address using a simple sub-net mask. The mask is provided as an extra parameter to the
begin() method for the receiver. The logic of the address filtering is:
(incoming-message-address & receive-node-sub-net-mask) == receive-node-address
This implies that receiving nodes only listens for messages from transmitters with the same sub-net address as the receiver. Below is the
setup() snippet from the
CosaVWItempmonitor.ino example sketch:
// Virtual Wire Interface Receiver connected to pin D8
VirtualWireCodec codec;
VWI::Receiver rx(Board::D8, &codec);
const uint16_t SPEED = 4000;
const uint32_t ADDR = 0xc05a0000UL;
const uint32_t MASK = 0xffffff00UL;
...
void setup()
{
...
// Start virtual wire interface and receiver. Use eight bit sub-net mask
// Transmitters must have the same 24 MSB address bits as the receiver.
VWI::begin(ADDR, SPEED);
rx.begin(MASK);
}
In the above snippet the sub-net
MASK will allow for 255 transmitting sensor reporting to the monitoring node. The first sub-net address (0) is the receiver itself. With the <24:8> bit addressing scheme in the example sketch there could be
2**24 monitor nodes if in a flat structure. Additional sub-nets are
possible, if for instance <16:8:8>, the monitoring node would also report to a sub-net with yet another layer of nodes. And parts of the address could be used to sensor type tagging.
The
VWI::Receiver::recv() method will return the full message with the enhanced mode header and the application payload to the caller. Further filtering and dispatch of message will require access to the enhanced VWI header. Defining a struct (
msg_t) with the header and a union of the application message data types, as in the snippet below, is a convenient way to describe the incoming messages. Also it is practical to collect all messages in a separate header file to keep the code base consistent. Below is the
loop() of the example monitor with an example of message filtering based on the message type (command/cmd).
// Message received from VWI in extended mode
struct msg_t {
VWI::header_t header;
union {
sample_t sample;
// Add additional message types here
};
};
...
void loop()
{
// Receive a message. Sanity check the message size
msg_t msg;
int8_t len = rx.recv(&msg, sizeof(msg));
if (len <= 0) return;
// Print message header; transmitter address and sequence number
trace << hex << msg.header.addr << ':' << msg.header.nr << ':';
// Check message type and print contents
if (msg.header.cmd == SAMPLE_CMD) {
...
}
else {
trace << msg.header.cmd << PSTR(":unknown message type") << endl;
}
}
VWI::header_t contains the transmitter node address, message type tag and sequence number. The node address is provided as the parameter to
VWI::begin(), the message type tag (
cmd) as the additional parameter to
VWI::Transmitter::send() and the sequence number is maintained internally to the Transmitter. Below is the implementation:
class VWI {
public:
...
/** Message header for enhanced Virtual Wire Interface mode */
struct header_t {
uint32_t addr; /**< Transmitter node address */
uint8_t cmd; /**< Command or message type */
uint8_t nr; /**< Message sequence number */
};
...
};
So far the modifications are to the original Virtual Wire Interface and have provided addressing and message types. In the following section the new
VWI::Transceiver class is presented. This class adds reliable communication with message acknowledgement and auto-retransmission.
Fig.2: VWI::Transceiver Public Member Functions and Attributes
The VWI::Transceiver interface is now reduced to basically
send() and
recv(). The Transceiver contains both a Transmitter and Receiver instance (rx and tx attributes above). The
send() method will transmit an enhanced mode message and
wait for an acknowledgement. The acknowledgement is transmitted by the receiving node. If the message or acknowledgement is lost an automatic retransmission will occur. The sender node will wait a
TIMEOUT period before retransmission and perform
RETRANS_MAX retransmissions. The
send() method will return the number of transmissions or a negative error code(-1) if the maximum number of retransmissions was exceeded.
Note that the
VWI::Transmitter::send() method will return directly after placing the message into the transmission buffer. This is not the case with
VWI::Transceiver::send(). This method will wait until it receives an acknowledgement with possible retransmission.
Below is a snippet of the
CosaVWIclient.ino example sketch. This sketch demonstrates how to use the
VWI::Transceiver and multiple message types. The first message type (
sample_t/SAMPLE_CMD) contains two analog readings and is sent every second from the wireless sensor device. The second message type (
stat_t/STAT_CMD) contains power supply reading and transceiver statistics. This message is sent every 15th second.
// Network configuration
const uint32_t ADDR = 0xc05a0001UL;
const uint16_t SPEED = 4000;
...
// Virtual Wire Interface Transceiver
VWI::Transceiver trx(Board::D8, Board::D9, &codec);
...
// Analog pins to sample for values to send
AnalogPin luminance(Board::A2);
AnalogPin temperature(Board::A3);
...
// Message type to send (should be in an include file for client and server)
const uint8_t SAMPLE_CMD = 1;
struct sample_t {
uint16_t luminance;
uint16_t temperature;
sample_t(uint16_t lum, uint16_t temp)
{
luminance = lum;
temperature = temp;
}
};
...
const uint8_t STAT_CMD = 2;
struct stat_t {
uint16_t voltage;
uint16_t sent;
uint16_t resent;
uint16_t received;
uint16_t failed;
void update(int8_t nr)
{
sent += 1;
if (nr <= 0)
failed += 1;
else if (nr > 1)
resent += (nr - 1);
}
};
...
// Statistics
stat_t stat;
...
void setup()
{
// Start watchdog for delay
Watchdog::begin();
RTC::begin();
...
// Start virtual wire interface in extended mode; transceiver
VWI::begin(ADDR, SPEED);
trx.begin();
}
...
void loop()
{
// Send message with luminance and temperature
sample_t sample(luminance.sample(), temperature.sample());
int8_t nr = trx.send(&sample, sizeof(sample), SAMPLE_CMD);
stat.update(nr);
// Send message with battery voltage and statistics every 15 messages
if (stat.sent % 15 == 0) {
stat.voltage = AnalogPin::bandgap(1100);
nr = trx.send(&stat, sizeof(stat), STAT_CMD);
stat.update(nr);
}
// Take a nap
SLEEP(1);
}
A server will receive the messages from the client (above), filter, acknowledge and print the messages. Note that the receiving node must filter received messages so that multiple messages with the same sequence number are only handled once (if necessary). Below is a snippet from the
CosaVWIserver.ino example sketch:
// Virtual Wire Interface Transceiver
VWI::Transceiver trx(Board::D8, Board::D9, &codec);
...
// Network configuration
const uint32_t ADDR = 0xc05a0000UL;
const uint32_t MASK = 0xffffff00UL;
const uint16_t SPEED = 4000;
..
void setup()
{
...
// Start virtual wire interface transceiver
VWI::begin(ADDR, SPEED);
trx.begin(MASK);
}
...
// Message types
const uint8_t SAMPLE_CMD = 1;
struct sample_t {
uint16_t luminance;
uint16_t temperature;
};
const uint8_t STAT_CMD = 2;
struct stat_t {
uint16_t voltage;
uint16_t sent;
uint16_t resent;
uint16_t received;
uint16_t failed;
};
...
// Extended mode message with header
struct msg_t {
VWI::header_t header;
union {
sample_t sample;
stat_t stat;
};
};
...
void loop()
{
// Processed sequence number (should be one per client)
static uint8_t nr = 0xff;
// Wait for a message. Sanity check the length
msg_t msg;
int8_t len = trx.recv(&msg, sizeof(msg));
if (len <= 0) return;
// Check that this is not a retransmission
// Should be one counter (nr) per connection
if (nr == msg.header.nr) return;
nr = msg.header.nr;
// Print header, type message type and print contents
trace << hex << msg.header.addr << ':' << msg.header.nr << ':';
switch (msg.header.cmd) {
case SAMPLE_CMD:
...
break;
case STAT_CMD:
...
break;
}
}
The next development of Cosa VWI is to increase performance and adapt to the event driven state-machine framework. One goal is to abstracting the interface toward wireless connection over Ethernet, WiFi, RF433, NFR24L01P, etc, so that applications are written without concern to the wireless device. RF433 is an interesting challenge as the protocol stack must be built from the ground up.
[Update 2013-11-17]
This extension of the Virtual Wire interface in Cosa has been deprecated and replaced by an abstract Wireless interface. This interface is implemented by RF433 modules (VWI), CC1101 and NRF24L01P. The above extended VWI interface is still possible to implement on top of the new Wireless interface. This will be added in later releases of Cosa. Please see the Cosa documentation for further details.