Thursday, November 17, 2016

A Cordial Chat Between Digital Friends

I realized that many of my projects would need to be wireless and that I wanted a consistent and flexible way to communicate between microcontrollers.  So I developed the Node Network Object which can be included in any Arduino sketch.  Since I first developed it, I've created a few different flavors and I have updated it as the hardware changed.  But, a number of things have remained consistent.

1)  A readable text-based format for sending and receiving messages.  Let's say I wanted to send a message between two microcontrollers about the environment.  I'll title the message "environment" and perhaps I'll send 3 numbers indicating temperature, humidity and light level.  With the text based protocol the message would look something like this:

     environment|temperature:45;humidity:60;light:1013

That's a lot of text to send for 3 numbers!  So let's abbreviate to keep things small (and therefore quick).

     env|T:45;H:60;L:1013

And, since the labels for each number are optional (they're nice when auto-connecting to a host computer but that's another post). the entire message could be VERY short:

     e|45;60;1013

In the future I want to add the ability to send data objects.  The new message might look like this:

     e|o:042D3C03F5

It looks longer, but it's actually a few bytes smaller than the previous message since each byte is represented by two characters when I write it out.  Here, "o" would be a reserved label indicating an object.  The next byte (0x04 in this case) would indicate the object size in bytes (a maximum of 255 bytes) and the following bytes would be object data.

2)  Easy setup.  When the node library is included, only a few lines of code are needed to get everything working.
a)  Call nodeInit with the encryption key, the network ID, the node ID, and the transceiver type:

 node.nodeInit("MYENCRYPTIONPASS", 10, 2, RF69_915MHZ);

b)  Register command handlers.  These provide places for different commands to be processed and they look like this:

 node.handler.registerCommandHandler("env", environmentUpdate, "T,H,L");

In this case, when the controller receives an "env" message, it will call the method "environmentUpdate" and expect data fields T, H, and L.

3)  Easy handling.  The command handler methods take the following format:

int environmentUpdate(char* message, int length){
  node.handler.setMessage(message, length);  //do this if you want to use values in the message
  char* value = node.handler.getNextMessageField(); //do this for every value and label
  int num = atoi(value);  //convert the values you want to use
  return 1;
}

When a registered command is received, it's associated command handler gets called so the message can be processed.

4)  Easy scheduling.  Since the radio is a shared resource, it needs to be scheduled if you want to send out a message.  Whenever part of your program needs to use it, just call the addQueueEvent method on the node:
node.queue.addQueueEvent(sendEnvironment, 0, 3, 45, 60, 1013);
The first parameter is the method to call, followed by the number of milliseconds to delay before calling it (0ms in this case but it can be up to 60 seconds).  The next parameter is the number of optional values to pass to the sendEnvironment function.  The maximum number of these parameters is configurable and I recommend keeping it small.

5)  Simple Sending.  Event methods look like this:
void sendEnvironment(unsigned int values[])
Values passed in will be in the values array and MUST be positive integers.  The job of the send method is to create a string as seen in #1 above and place it into the node's radio buffer and remember the message size in bytes (use sprintf for this).  Then call sendToNode.  Your message code might look like this if you wanted to send the message to node 1:

void sendEnvironment(unsigned int values[]){
  int msgSize = sprintf(node.getBuffer(),"env|T:%i;H:%i;L:%i",values[0],values[1],values[2]);
  node.sendToNode(msgSize,1);
}

6)  Loop maintenance.  Once all the above is set up on both the sending and receiving end, each microcontroller must make continuous calls to the "nodeLoop" function.  This works much like the standard arduino delay function and takes an argument for the number of milliseconds to wait.  I do not recommend a value under 300 milliseconds.  During this time, messages will be received and processed, queued events will be triggered and called, and messages will be sent.  An example Arduino loop method might look like this:

void loop(){
   //read the temperature
   //read the humidity
   //read the light
   //put the send environment event in the queue

   //wait one minute
   node.nodeLoop(60000);
   //even though we wait a minute, a lot can happen if messages are received.  Especially
   //messages that require a response.
}

This microcontroller would send its environment data every minute (see #4 and #5 above) and the other would have a method to handle that data (see #3 above).  More complex interactions can be achieved if one microcontroller periodically requests the data and the other only sends it upon request.

Ok, that is a LOT.  You should be able to find a short video showing how quickly this sort of interaction can be written from scratch here.  The code repositories are located below:

Node Code
Command Handler Code
Event Queue Code
RFM69 Library (from lowpowerlabs.com)

Also, documentation can be found here.






       


No comments:

Post a Comment