Close

Implementing bi-directional comms part 2 - coding

A project log for Tangible programming

An Arduino-based tangible programming effort

amosAmos 01/14/2019 at 06:280 Comments

[My attention was briefly diverted by the arrival of some PCBs for this project, but I'll try to get the documentation back on track...]

In a previous log I detailed how the bi-directional communications will work: Blocks will listen for incoming data on both the hardware Serial port and a software (AltSoftSerial) port. 7-byte data packets have been defined, which should hold enough information for now, although the packet size may need to be increased, or ideally made variable length, in the future. When a packet is received, the destination is checked - if it is 0 (addressed to the current block) or 255 (broadcast) the packet is acted on and if not, the destination is decremented, the source is incremented (unless it is from the Master) and the message is sent on via the opposite port that it arrived on. (i.e. If the packet was received via the Serial port, it will be forwarded to the next node on the AltSoftSerial port.)

One minor issue with the protocol as defined: It uses a "BEGIN_TRANS" value, but that value could very well be a legitimate data value. I really need to come up with a better "start message" indicator, but I will leave that problem for another time... Suggestions for how to appropriately handle this are more than welcome!

So let's look at some code: (NB: I am not going to show all the code here, only parts that are different from the precious sample, or particularly interesting snippets.)

First of all, let's define some constants

// Some message constants
#define LEDS_OFF      0
#define RED_ON        1
#define GREEN_ON      2
#define WHO_IS_THERE  3
#define MODULE_ID     4

#define START_MSG     '#'

// Some address constants
#define MASTER_ID     254
#define BROADCAST_ID  255

// Set the number of bytes we need to read per message
#define MESSAGE_LENGTH  7

And we'll use a STRUCT for the message:

// A struct for the messages
struct Message {
  byte destination;
  byte source;
  byte messageType;
  byte data1 = 0;
  byte data2 = 0;
  byte data3 = 0;
  byte data4 = 0;
  bool recvInProgress = false;
  byte receivedBytes = 0;
};

The recvInProgress and receivedBytes fields are used when receiving a message via a serial port - it is not guaranteed that the entire message will be read at once, so some state needs to be kept to allow the messages to be received in several chunks.

Finally, declare a couple of variables to hold messages coming in on each port:

// Variables to hold the up and downstream messages...
Message upstreamMessage = Message();
Message downstreamMessage = Message();

For this sample, I am using a simple struct, but later on I will turn Message into a full class, but for now I'm keeping things simple-ish.

Now I should warn you, I tend to use pointers quite a bit in my code...

Sending a message is as simple as sending the START_MSG value, then each byte of the message struct:

void sendMessageOnPort(Stream* port, Message msg)
{
  port->write(START_MSG);

  port->write(msg.destination);
  port->write(msg.source);
  port->write(msg.messageType);
  port->write(msg.data1);
  port->write(msg.data2);
  port->write(msg.data3);
  port->write(msg.data4);
}

Because we are listening on two different ports, but the process is identical for each port, I created a function to listen on an arbitrary port:

bool checkForData(Stream* port, Message msg) {
  byte incomingByte;

  // If there is data waiting on the port, process it...
  while (port->available() > 0) {
    incomingByte = port->read();
    if (incomingByte == START_MSG) {
      // Initialise the message and start the receipt...
      // Note: if we are already receiving a message, but haven't received the full 7 bytes, we discard it.
      clearMessage(msg);
      msg.recvInProgress = true;
    } else if (msg.recvInProgress) {
      // We're receiving a message, stick this byte where it belongs...
      // incremented the bytes received
      msg.receivedBytes++;
      switch (msg.receivedBytes) {
        case 1:
          msg.destination = incomingByte;
          break;
        case 2:
          msg.source = incomingByte;
          break;
        case 3:
          msg.messageType = incomingByte;
          break;
        case 4:
          msg.data1 = incomingByte;
          break;
        case 5:
          msg.data2 = incomingByte;
          break;
        case 6:
          msg.data3 = incomingByte;
          break;
        case 7:
          msg.data4 = incomingByte;
          return true;
          break;
      }
    }
  }

  // If we get here, we haven't received a full message...
  return false;
}

The while loop here checks to see if there is any data on the nominated port, if there is, a single byte is read. If a message receive is not already in progress, the msg passed in is cleared with a helper function and the recvInProgress field is set to true. If a receive is in progress, the number of bytes received is incremented and the value read is placed in the appropriate field of the msg. If the byte received is the seventh byte of the message, the function returns true, otherwise the while loop checks again for any waiting data and does its thing all over if so...

Sometimes several bytes of data will be received into the Arduino's buffer while the code is doing something else, so several bytes may be read at a time. This function allows us to process the data as it comes in, and lets us read a few bytes go off and do something else while more data comes in, then read the next few bytes.

The main loop for the blocks is now:

void loop() {
  // Check for something coming from upstream (the direction of the MC)
  if (checkForData(&Serial, upstreamMessage)) {
    // we got a complete message from upstream!
    handleMessage(upstreamMessage, &Serial, &altSerial);
  }
  
  // Check for something coming from downstream (the direction away from the MC)
  if (checkForData(&altSerial, downstreamMessage)) {
    // we got a complete message from upstream!
    handleMessage(downstreamMessage, &altSerial, &Serial);
  } 
}

We keep looping around, checking for data on the Serial port, then checking for data on the AltSoftSerial port. If a full message has been received on either port, the message is handled by a helper function. This function gets given the message, and it is told which port the message came in on (the first port parameter) and which port the message should be forwarded on if it is not for the current block.

A simple implementation of handleMessage might be: 

void handleMessage(Message msg, Stream* inPort, Stream* outPort) {
  switch (msg.destination) {
    case BROADCAST_ID:
      // If the source is NOT the MC, increment the source for the next hop...
      if (msg.source != MASTER_ID) msg.source++;
      sendMessageOnPort(outPort, msg);
    case 0:
      // let's process the message...
      switch (msg.messageType) {
        case RED_ON:
        case GREEN_ON:
        case LEDS_OFF:
          showLED(msg.messageType);
          break;
        case WHO_IS_THERE:
          // Send my module id...
          Message reply = Message();
          reply.destination = msg.source;
          reply.source = 0;
          reply.messageType = MODULE_ID;
          reply.data1 = moduleID;
          sendMessageOnPort(inPort, reply);
          break;
      }
      break;
    default:
      // Increment the source and decrement the destination
      if (msg.source != MASTER_ID) msg.source++;
      msg.destination--;
      sendMessageOnPort(outPort, msg);
      break;
  }
}

Here, if the message is a broadcast message, it is first forwarded on to the next  block and the execution of the switch is allowed to fall through to the next case, which is if the message is addressed to the current block. If the message is for the current block, the messageType field is checked and handled appropriately. In this case, the LEDs are turned on or off, or the block replies to the sender with its moduleID. (The module id of my blocks can be hardcoded, or set via jumpers, but that is a tale for another day...)

Woah! That is more code than I expected to include here - sorry about that. This code works fine, but it does have a couple of issues. There is no error checking or correction for starters. Some kind of CRC value may need to be included in the packet for this.

Secondly, the START_MSG sentinel value may appear as a legitimate data value in a message, so a better way of flagging the start of a message is needed. Why do we need a START_MSG indicator? Quite simply, not all the blocks in a chain will necessarily wake up at the same time. It is possible that a block may be slower to start up than the Master unit and it may miss the first byte(s) of a message. We need some reliable way of detecting when a message begins so that partial messages can be discarded. Maybe a string of bytes would be better instead of a single byte? I'm not sure how to approach this just yet...

Another issue is that not all message packets will need the full four data bytes, and some may even need to send more than four bytes of data. Variable length messages would be needed to solve this. A possible format would be destination, source, length, messageType, [data bytes]. I am not yet at the stage where I need more than four data bytes in a message, but I can see that day approaching, so I will definitely need to address the possibility of variable length messages sooner rather than later.

I'd be glad to hear other people's thoughts on these issues, or any other comments you may have on this project in general.

Next up I will start to describe the actual programming language and show how I have implemented it so far.

Discussions