Safely Sending Signals (Part 2)

So if you’re following along, in the last tutorial [LINK] we created a simple secure communication method. It allowed us to send a “GO” signal across an entire field of Blinks, but that’s it. It didn’t actually communicate the type of “GO” it wanted, though. In this tutorial, we’re going to learn a few new concepts that will allow us to send and receive more complex signals.

The first thing we need to do is create a few more states so we have more interesting information to send. We’ll replace our boolean from last time with another enum, this time with a few state names. And just like the signal states, we’ll need a byte to track out “current” state.

enum gameModes {MODE1, MODE2, MODE3, MODE4};//these modes will simply be different colors
byte gameMode = MODE1;//the default mode when the game begins

The first step in communicating more complex information is sending it. To do that, we’ll need to learn a little bit about the technical process of sending information on Blinks.

Blinks can communicate 6 “bits” at a time. A bit is the smallest amount of information that can be stored electronically - it can only be a 0 or a 1 - and 6 bits in a row give us 64 discrete values that we can communicate. The simplest way to use this information would be to just take every message you ever want to send and assign it a value from 0 - 63. But that’s going to be annoying, and frequently our messages share bits of information. In this case, we might want to say “GO” in each of the 4 modes, not to mention INERT or RESOLVE in every mode. Instead of creating an enum with 12 values (INERT_MODE1, GO_MODE1, etc.) we can stack these messages up using bitwise operators.

Let’s label each of the bits we can use, like this:

[A][B][C][D][E][F]

We have 4 modes, which can be stored in 2 bits. Why 2 bits? Well, if a single bit can store give us 2 different values (0 or 1), another bit can give us 4 (00, 01, 10, 11). In fact, each new bit doubles the amount of values. So let’s assign the mode to bits E and F. We also have 3 signal states. Unfortunately, we’ll also need 2 bits for this, which is technically a waste, but not a big deal in this particular case. We’ll assign this to C and D.

To actually accomplish this stacking, we’ll need to use bitwise operators. This is a much larger topic than I’ll be covering here, but I’ve added a link at the bottom to a more comprehensive lesson at the bottom of the post if you’re interest. For our purposes we’re just going to be using three bitwise operators: shift left (<<), shift right (>>), and the “and” function (&).

We’ll start by using the shift operator to do the stacking of signals we want to send. Like the name suggests, bitwise shift moves bits the the left, placing their 0 or 1 values further along in the overall data. We are going to replace our current “setValueSentOnAllFaces” call with the following bit of code:

  byte sendData = (signalState << 2) + (gameMode);
  setValueSentOnAllFaces(sendData);

By “shifting” the signal state over by 2, we’ve taken it’s bit value (00, 01, or 10 in this case) and shifted if over two places bitwise, placing it in the C and D positions of our overall data. We then simply added the mode data, which doesn’t need to be shifted, to the end.

Now that we’re sending this stacked data, we’ll need a way to parse it on the other end. To do this we’ll create some little convenience functions at the bottom of the code. We’ll start with one that parses the mode data from the overall data. It’ll look like this:

byte getGameMode(byte data) {
  return (data & 3);//returns bits E and F
}

There a few things going on here. First, and I realize this may be a bit remedial for many of you, it’s worth pointing out that this function is not declared as a “void” but instead as a “byte” type function. This means that when it’s called, it returns a value, specifically a byte. The rest of the functions we’ve written have been “void” functions which mean they do not return anything when called. Also, this function requires a byte to be sent when it is called, which will be named “data” within the function itself. You’ll see both of these come into play when we call the function.

More importantly, in the “return” call, we’re returning data that we’ve operated on with the bitwise “and” function. This function is slightly tricky to explain, so I definitely recommend following the link at the bottom of the tutorial for a better overview. For us, using “& 3” lets us isolate just the last two bits in a larger piece of data. Again, this isn’t a real explanation, but I’m going to just put a little table below that shows how using specific values with the & function isolates different different sets of bits.

data = [A][B][C][D][E][F]

data & 1 = [F]
data & 3 = [E][F]
data & 7 = [D][E]F]
data & 15 = [C][D][E][F]
data & 31 = [B][C][D][E][F]
data & 63 = [A][B][C][D][E][F]

By returning just those two values, we are able to receive only the information we need from the signal.

The next function we write will need to give us the signal state, which is currently stored in C and D. That function will look like this:

byte getSignalState(byte data) {
  return ((data >> 2) & 3);//returns bits C and D
}

Looks pretty similar, but with a key difference. Before we use the & operator on the data, we’ve bitwise shift it to the right by 2. This takes all of the data and moves it to the right 2 places, effectively deleting the values that were in E and F, and leaving in their place the values from C and D. We can no use the exact same & logic as in the previous function to isolate the last two bits in our newly shifted data.

So now let’s call these functions! We’ll actually start with the signal state one, since we’ve got a place for it. You’ll notice that we actually query the signal state of our neighbor a few times throughout the code, so we’ll need to adjust in a few places. Specifically, we’ll need to replace this call:

getLastValueReceivedOnFace(f)

…with this call:

getSignalState(getLastValueReceivedOnFace(f))

By doing this, we send our new function the full 6 bits of data, and it then returns to us just the two we need. This is a good place to stop and test it out, though we’ll need to change one last thing to get it to run. We need to remove the “changeGameState” call and function, and adjust the display code so it doesn’t require the deleted boolean.

void displaySignalState() {
  switch (signalState) {
    case INERT:
      setColor(CYAN);
      break;
    case GO:
      setColor(RED);
      break;
    case RESOLVE:
      setColor(YELLOW);
      break;
  }
}

If we install this on some Blinks, you’ll see that everything communicates exactly as it did before, even though we’ve now stacked a new piece of information (gameMode) into the communication. Speaking of which, let’s actually use that!

We’ll start by adding a quick update to our display code that just chooses a color for each mode. And while we’re at it, we’re going to make GO and RESOLVE identical, because it’s not super important that we see both. Here’s what our new display code should look like:

void displaySignalState() {
  switch (signalState) {
    case INERT:
      switch (gameMode) {
        case MODE1:
          setColor(RED);
          break;
        case MODE2:
          setColor(YELLOW);
          break;
        case MODE3:
          setColor(GREEN);
          break;
        case MODE4:
          setColor(CYAN);
          break;
      }
      break;
    case GO:
    case RESOLVE:
      setColor(WHITE);
      break;
  }
}

Obviously we won’t be using most of this, since we have no way of changing mode. So that’s what we’ll do next. First, we want to update inertLoop by adding a bit of code to change the mode on click. Then, to accompany that, we need to add a listener for the new mode inside the existing listener. The new inertLoop will look like this:

void inertLoop() {
  //set myself to GO
  if (buttonSingleClicked()) {
    signalState = GO;

    //change game mode
    gameMode = (gameMode + 1) % 4;//adds one to game mode, but 3+1 becomes 0
  }

  //listen for neighbors in GO
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//a neighbor saying GO!
        signalState = GO;
        gameMode = getGameMode(getLastValueReceivedOnFace(f));
      }
    }
  }
}

Install this on a few Blinks, and suddenly you have the ability to consistently change the mode across the entire field. If you click a red Blink, the entire field turns yellow. Clicking a yellow Blink turns the whole field green, and so on. Here’s the entire code in case you missed something:

enum signalStates {INERT, GO, RESOLVE};
byte signalState = INERT;

enum gameModes {MODE1, MODE2, MODE3, MODE4};//these modes will simply be different colors
byte gameMode = MODE1;//the default mode when the game begins

void setup() {

}

void loop() {
  switch (signalState) {
    case INERT:
      inertLoop();
      break;
    case GO:
      goLoop();
      break;
    case RESOLVE:
      resolveLoop();
      break;
  }

  displaySignalState();

  byte sendData = (signalState << 2) + (gameMode);
  setValueSentOnAllFaces(sendData);
}

void inertLoop() {
  //set myself to GO
  if (buttonSingleClicked()) {
    signalState = GO;

    //change game mode
    gameMode = (gameMode + 1) % 4;//adds one to game mode, but 3+1 becomes 0
  }

  //listen for neighbors in GO
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//a neighbor saying GO!
        signalState = GO;
        gameMode = getGameMode(getLastValueReceivedOnFace(f));
      }
    }
  }
}

void goLoop() {
  signalState = RESOLVE;//I default to this at the start of the loop. Only if I see a problem does this not happen

  //look for neighbors who have not heard the GO news
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == INERT) {//This neighbor doesn't know it's GO time. Stay in GO
        signalState = GO;
      }
    }
  }
}

void resolveLoop() {
  signalState = INERT;//I default to this at the start of the loop. Only if I see a problem does this not happen

  //look for neighbors who have not moved to RESOLVE
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
        signalState = RESOLVE;
      }
    }
  }
}

void displaySignalState() {
  switch (signalState) {
    case INERT:
      switch (gameMode) {
        case MODE1:
          setColor(RED);
          break;
        case MODE2:
          setColor(YELLOW);
          break;
        case MODE3:
          setColor(GREEN);
          break;
        case MODE4:
          setColor(CYAN);
          break;
      }
      break;
    case GO:
    case RESOLVE:
      setColor(WHITE);
      break;
  }
}

byte getGameMode(byte data) {
  return (data & 3);//returns bits E and F
}

byte getSignalState(byte data) {
  return ((data >> 2) & 3);//returns bits C and D
}

Before I sign off on this, though, I do want to point out a problem you might encounter when playing with this demo. If you take a field of Blinks and click two Blinks at the same time, you’ll see the two colors meet in the middle and just stop spreading. No color has precedence over another, and since the logic that decides if the signalState can be resolved doesn’t actually care about gameMode, it’s totally happy to resolve into that state. In a full game, this may be problematic, and you’d need to work out a way to resolve this conflict. But that’s a story for another time.

So we’ve accomplished it! Our state change is now consistent across the entire field. You’ll see this logic, almost exactly, in Zen Flow, so check that out for a bigger, more expansive use of this technique.

————

Bitwise Operator Info

2 Likes

This one was a bit trickier to wrap my head around it, but I think I understand it’s usefulness. It definitely helps keep code more organized to use this method of tile-to-tile communication, rather than what I was doing for QuickMatch with having one central tile to act as a “referee”. I wonder how this could be used for controlled randomization of tiles, or if this could be used to help keep track of tile states across a board?

Yeah, the layering of data is trickier, but it’s so so important if you want to use this methodology. Once we figured it out, it became our gold standard method and appears in more than half of the launch titles.

We do use this to create somewhat controlled randomization in WHAM, though we incorporate a layer of Game-of-Life as well, so that gets tricky. It controls the randomization in that it sends a “pop up now” to the whole field, and only some of the field actually decides to pop up.

Keeping track of multiple tile states is trickier, since it requires each Blink to understand the shape and size of the world, plus have the available memory to hold those states. In general, it’s best to find ways to not require that kind of tracking, and see if you can achieve your desired effect in a different way. For instance, to reference WHAM again, we set a timer in each Blink that tracks how much time has passed since the round began. When that timer ends, and no “death” signals have been received, they all understand that the round was a success. It’s all independent calculations, but it is informed by the state of the world.

1 Like