Close

State machine hell.

A project log for The ides of DEFCON: An Unofficial Electronic Badge

A wearable hardware badge, featuring blinky lights, sound, a sub-1Ghz radio, games, and more. Based on the Freescale KW01.

john-adamsJohn Adams 01/05/2017 at 06:550 Comments

Our boards are still being manufactured at Macrofab so the last week or so has been an intense pile of work to get the game itself up and running. Our combat system is pretty nice (on paper) and the boards are mostly cooperating.

Unfortunately, synchronizing two devices on a hostile RF network like the one we will encounter at DEFCON isn't that easy.

At first our game was working but we'd see the occasional signal loss event, where we would drop a critical message and then suddenly lose track of the entire game. It turns out that the radios, even while sitting next to each other are not 100% reliable. After a ton of back and forth in the code I realized that I was going to have to construct a highly reliably radio protocol in very little memory space.

This code takes concepts from TCP/IP and works well for automatic retransmission of packets.

While we don't have the memory to implement something like TCP/IP or lwIP, we do have a limited amount of space to roll our own networking code. The code, on the sending side implements a primitive version of ARQ (the same thing TCP/IP does).

In a sentence, ARQ is "Keep retransmitting the data until the TTL expires on the packet, or, you get an ACK, or, you get a RST."

My version is a bit ugly, but it looks something like...

sendPacket(...);

current_fight_state = WAITACK

// loop... 
while (1) { ... 
if (current_fight_state == WAITACK) {
      // transmit/retry logic                                                                                                                                                           
      if ( (chVTGetSystemTime() - last_tick_time) > MAX_ACKWAIT ) {
        if (packet.ttl > 0)  {
          resendPacket();
        } else {
          orchardAppTimer(context, 0, false); // shut down the timer                                                                                                                    
          screen_alert_draw("OTHER PLAYER WENT AWAY");
          end_fight();
          playHardFail();
          chThdSleepMilliseconds(ALERT_DELAY);
          orchardAppRun(orchardAppByName("Badge"));
          return;
        }
      }
}
}
/* in the radio loop, if we get an ACK, we advance state */
case WAITACK:
  if (u->opcode == OP_ACK) {
     // if we are waiting for an ack, advance our state.                                      // however, if we have no next-state to go to, then we will timeout                                                                                                             
     if (next_fight_state != NONE) {
        chprintf(stream, "\r\n%08x --> moving to state %d from state %d due to ACK\n",                 
u->netid, next_fight_state, current_fight_state, next_fight_state);        
        current_fight_state = next_fight_state;     
     }    
  }
break;

On the recipient's end, we ACK inbound requests and advance our state machine, if we have something to do.

Needless to say, this sort of thing is ridiculously complex and filled with contention, deadlocks, and all of the things you'd expect in a massively concurrent system. It is severely headache inducing and makes me want to break things.

I hope to have these issues fixed this week.

Discussions