1%
1 words - 1 min read.

Team Skynet's Decoder Device In early 2017 I was on the planning committee for the PotashCorp IT departments' yearly team building event. The event was held at the University of Saskatchewan, and included speakers, tours, and other activities focused on innovation. My contribution to the event was a scavenger hunt, but since this was for IT, I wanted to make it technically interesting/challenging. Participants were given a series of clues and riddles, and the answers to these questions were things or places that they needed to take a picture of in return for points. The twist was that essential parts of the clues were encoded, and participants needed to gather parts to build a device which would decode the essential portions of the clues!

The backstory I came up with was that each participant has received an email from the head of our IT department, directing them to work on a skunk-works project to build an image recognition system. To train the image recognition system, participants were split into teams to take pictures of items and locations from a list. Unfortunately, the email was processed by our cloud Exchange server, and the NSA's snooping technology had garbled pieces of the list of places and items to take a picture of. To decode the garbled bits, each team needed to build a device to decode the messages in order to discover all the items or locations.

An example of the coded clues:

clues

To start, each team was given a set of instructions and a list of all the clues. The first task was to take a series of photos of the team doing crazy things, and in return they could redeem these photos for electronic parts that were required to build their decoder.

Starting tasks:

starting_clues

Parts List:

  • 1 x TIMESQUARE PCB - half thickness black PCB
  • 1 x 10K resistor - brown black orange gold
  • 1 x 0.1uF ceramic capacitor - yellow blobby
  • 8 x 47 ohm resistor - yellow violet black gold
  • 1 x DS1337 - 8 pin real time clock chip
  • 1 x ATMEGA328P - preprogrammed microcontroller
  • 1 x 32.768KHz Crystal - thin silver cylinder
  • 1 x 20mm coin battery holder
  • 2 x Right angle buttons
  • 1 x 1.5" 8x8 matrix
  • 1 x Clear acrylic cutout
  • 1 x CR2032 Coin cell battery
  • 1 x Silicone rubber watch band
  • 1 x 28-pin 0.3" IC Socket

We set up an electronic parts "store" called Crazy Troy's Electronics where each team would show their photos in return for parts. Each team was given a table with a soldering iron and all tools necessary to assemble and solder the device together by following the instructions contained in a document I created for each team.

The device they were required to build was an 8x8 LED wrist watch kit from Adafruit, which uses an Arduino compatible ATmega328P. The watch normally scrolls the time and date across the screen, but with a little work (okay a lot), I reprogrammed it to display clues for the scavenger hunt.

Adafruit Times Square Watch Kit

I ordered a single watch kit from Adafruit to begin prototyping. One of the first challenges was to figure out how participants would interact with the watch. Once they solve a clue and go take a picture of that location or object, they need to let the watch know they need the next clue. Unfortunately the watch has limited input options (two buttons, one on each side of the watch face), and very limited memory, so my first idea was to swap the Atmega328P each time a team solved a clue. The new IC would contain the answer for the next clue, allowing the team to progress. This required a physical modification to the design, as the original Adafruit IC is soldered directly to the PCB. To facilitate hot swapping the IC, I added a 28-pin 0.3" IC socket. This would allow teams to easily pop out the existing micro-controller and put in the new one, which would scroll the new clue across the screen of the watch.

Adafruit Times Square Watch Kit

Adafruit Times Square Watch Kit

During testing I quickly realized that this was a terrible idea. More often than not I ended up bending the pins on the ATmega chip and had to spend time trying to bend them back into place. I can only imagine how many teams could have become stuck with a micro-controller with bent pins out in the field, unable to continue if we had went with this idea. The other major reason I didn't go down this route was cost. The cheapest 328P's I could find were at least $1.50; with 12 teams and over a dozen clues, it would require around $300 of 328P chips AND I would have to program them all individually.

Instead of swapping IC's I decided to leverage the buttons on either side of the watch face. Each time a team started on a new clue, the team would need to enter a new code so that the IC would know to scroll a new answer across the screen. The original number of clues was around 20, so the code needed to be at least 5 commands long (5 bits = 32 combinations).

I had never done any Arduino programming before and I'm not really a read the manual type of guy so it was a bit painful at first (the code you read below is pretty bad...this was a slapped together rush job on my part). I had played a fair bit with Raspberry PI, but never Arduino. Figuring out the Arduino IDE board and programmer settings for this device was a bit complicated, and I bricked a few ATmega's before I got it right. The whole idea of bootloaders and flashing my code via FTDI was a completely new experience. Once I had it down though it was extremely satisfying.

dog

I scrapped most of the Adafruit code except for the button logic. Each time the IC's background loop runs it checks to see if any buttons are being pressed or not. If a button was being pressed, a counter would be incremented and an array would be populated with an "L" for the left button, or an "R" for the right button. I used a character array as this was easier for me to remember than a binary representation of the codes. At the end of the loop it would check to see if 5 buttons presses had occurred, and if they had, it would check to see if the entered code matched one of the codes related to a clue. If the code matched, a variable would be set indicating which message should be displayed across the screen. If both buttons were pressed at the same time, it would clear the array and the counter, acting as a cancel button to re-enter a code if an error was made while trying to enter the code. Here is the background loop:

void loop() {
  uint8_t a = watch.action();
  if(a == ACTION_HOLD_BOTH) {
    if(mode == MODE_SCAVENGER) {
      // Return to last used display mode
      mode = mode_last;
    } else {
      // Save current display mode, switch to scavenger setting
      mode_last = mode;
      mode      = MODE_SCAVENGER;
    }
    numButtonPresses = 0;
    memset(&buttonHistory[0], 0, sizeof(buttonHistory));
  } else if (a == ACTION_HOLD_RIGHT && numButtonPresses < 5) {
    buttonHistory[numButtonPresses] = 'r';
    numButtonPresses++;
  } else if (a == ACTION_HOLD_LEFT && numButtonPresses < 5) {
    buttonHistory[numButtonPresses] = 'l';
    numButtonPresses++;
  }
  uint8_t oldMessageIndex = messageIndex;
  if (strcmp(buttonHistory, "llll") == 0) {
    messageIndex = 1;
  } else if (strcmp(buttonHistory, "rrrr") == 0) {
    messageIndex = 2;
  } else if (strcmp(buttonHistory, "rrrl") == 0) {
    messageIndex = 3;
  .......snip
  }
  if (oldMessageIndex != messageIndex) {
    numButtonPresses = 0;
    memset(&buttonHistory[0], 0, sizeof(buttonHistory));
  }
  (*modeFunc[mode])(a); // Action is passed to clock-drawing function
  watch.swapBuffers();
}

This part was simple and didn't cause me any grief. The part that I wasted an inordinate amount of time on was displaying my clues across the screen. It turned out that the Adafruit code uses a large hex map to store the graphical representations of each character, and bit blit's this map to produce the scrolling marquee text. A portion of the map:

 static const uint8_t PROGMEM
 marqueeDigits[] = {
  0x00,0x00,0x2C,0xDA,0xEB,0x16,0x00,0x0B,0xFF,0x00,0x2C,0xDA,0xD7,0x16,0x00,
  0x2C,0xD1,0xDF,0x16,0x00,0x00,0x00,0x14,0xDC,0x00,0xFF,0xFF,0xFF,0xDF,0x00,
  0x2C,0xDA,0xEB,0x44,0x00,0xFF,0xFF,0xFF,0xDF,0x00,0x2C,0xDA,0xD7,0x16,0x00,
  0x2C,0xDA,0xD7,0x16,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x03,0xB2,0x00,0x01,0x8F,0x04,0xCE,0xFF,0x03,0xB0,0x00,
  0x0D,0x8D,0x03,0xB2,0x01,0x07,0x8F,0x00,0x00,0x02,0xE1,0x98,0x04,0x9C,0x00,
  0x00,0x00,0x03,0xB2,0x00,0x01,0xF5,0x00,0x00,0x00,0x16,0x60,0x03,0xB2,0x00,
  0x0D,0x8F,0x03,0xB2,0x00,0x0D,0x8F,0x00,0x00,0x1A,0x58,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0D,0x64,0x00,0x01,0x64,0x00,0x0D,0xD3,
  0x00,0x00,0x00,0x31,0x46,0x00,0x00,0x00,0x11,0x5C,0x00,0x00,0x91,0x61,0x64,
  ........
  100 more lines

They used an array indexing scheme to index into the correct portion of the map:

  watch.fillScreen(0);

  digitX[]     = { 1, 6, 9, 14, 19, 24, 29, 34, 39, 44, 49, 53, 55, 59 },
  digitWidth[] = { 5, 3, 5,  5,  5,  5,  5,  5,  5,  5,  4,  2,  4,  4 };
  x = curX;
  h = 8 - (f * 2); // Height of upper section of character to blit (2-8)
  y = f * 8;       // Vertical position of character in marqueeDigits[].
  // There are actually 4 bitmaps of each digit, each offset by 1/4 pixel
  // to provide subpixel antialiasing when scrolling -- extra smoothness!
  for(i=0; i < len; i++) {
    dx = pgm_read_byte(&digitX[str[i]]);
    dw = pgm_read_byte(&digitWidth[str[i]]);
    // The blit() function doesn't do real compositing -- it overwrites.
    // In order to pack the italicized digits closer together, they're
    // blitted as an upper and lower half (offset by 1 pixel) that don't
    // overlap the prior character.
    blit(          marqueeDigits, 63, 32, dx  , y  , x  , 0, dw, h  , 255 );
    if(h < 8) blit(marqueeDigits, 63, 32, dx-1, y+h, x-1, h, dw, 8-h, 255 );
    x += pgm_read_byte(&digitWidth[str[i]]);
  }

After a few false starts, I soon realized I needed to spend more time learning Arduino basics, or start from scratch. I had received the watch approximately 3 weeks prior to the event and needed a working prototype in time to order the remaining watches and program them as well, in addition to prepping all the other material for the event. At the time my wife was 8 months pregnant and our due date was about 5 days after the event was to be held, so I needed to make this work, fast.

My solution was relatively simple: a two dimensional array of hexidecimal characters where each row of 8 hex values represented the entire 8x8 LED matrix for a single character:

const PROGMEM unsigned char letters[45][8] = {
  {0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C}, //0
  {0x8, 0x18, 0x28, 0x8, 0x8, 0x8, 0x8, 0x8}, //1
  {0x7E, 0x2, 0x2, 0x7E, 0x40, 0x40, 0x40, 0x7E}, //2
  {0x3E, 0x2, 0x2, 0x3E, 0x2, 0x2, 0x3E, 0x0}, //3
  {0x8, 0x18, 0x28, 0x48, 0xFE, 0x8, 0x8, 0x8}, //4
  {0x3C, 0x20, 0x20, 0x3C, 0x4, 0x4, 0x3C, 0x0}, //5
  {0x3C, 0x20, 0x20, 0x3C, 0x24, 0x24, 0x3C, 0x0}, //6
  {0x3E, 0x22, 0x4, 0x8, 0x8, 0x8, 0x8, 0x8}, //7
  {0x0, 0x3E, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x3E}, //8
  {0x3E, 0x22, 0x22, 0x3E, 0x2, 0x2, 0x2, 0x3E}, //9
  {},
  {},
  {},
  {},
  {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, //SPACE
  {},
  {},
  {0x8, 0x14, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x22}, //A
  {0x7c, 0x42, 0x42, 0x7e, 0x42, 0x42, 0x42, 0x7c}, //B
  {0x3e, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x3e}, //C
  {0x7c, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7c}, //D
  {0x7C, 0x40, 0x40, 0x7C, 0x40, 0x40, 0x40, 0x7C}, //E
  {0x7C, 0x40, 0x40, 0x7C, 0x40, 0x40, 0x40, 0x40}, //F
  {0x3C, 0x40, 0x40, 0x40, 0x40, 0x44, 0x44, 0x3C}, //G
  {0x44, 0x44, 0x44, 0x7C, 0x44, 0x44, 0x44, 0x44}, //H
  {0x7C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x7C}, //I
  {0x3C, 0x8, 0x8, 0x8, 0x8, 0x8, 0x48, 0x30}, //J
  {0x0, 0x24, 0x28, 0x30, 0x20, 0x30, 0x28, 0x24}, //K
  {0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7C}, //L
  {0x81, 0xC3, 0xA5, 0x99, 0x81, 0x81, 0x81, 0x81}, //M
  {0x42, 0x62, 0x52, 0x52, 0x4a, 0x4a, 0x46, 0x42}, //N
  {0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C}, //O
  {0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20}, //P
  {0x1C, 0x22, 0x22, 0x22, 0x22, 0x26, 0x22, 0x1D}, //Q
  {0x3C, 0x22, 0x22, 0x22, 0x3C, 0x24, 0x22, 0x21}, //R
  {0x3e, 0x40, 0x40, 0x3c, 0x02, 0x02, 0x02, 0x7c}, //S
  {0xfe, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}, //T
  {0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x22, 0x1C}, //U
  {0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x24, 0x18}, //V
  {0x41, 0x41, 0x41, 0x49, 0x49, 0x49, 0x5d, 0x63}, //W
  {0x0, 0x41, 0x22, 0x14, 0x8, 0x14, 0x22, 0x41}, //X
  {0x41, 0x22, 0x14, 0x8, 0x8, 0x8, 0x8, 0x8}, //Y
  {0x7e, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x7e}, //Z
};

The position of each character in the array (as seen in the comment on each line) is aligned with that characters ASCII value. To find a character in this array, just cast the character to an int and - 48. For example, letters[((int)"a")-48] would give you the row for the letter "a". If you take each of the eight hex values from any given row, you'll fin d that when they are converted to binary, they represent a binary map of the character. For example, to represent 0, the hex values in the array are 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C. When aligned vertically and translated to binary, you get:

00111100 - 0x3C
01000010 - 0x42
01000010 - 0x42
01000010 - 0x42
01000010 - 0x42
01000010 - 0x42
01000010 - 0x42
00111100 - 0x3C

I started with a kind of binary screen buffer array that would represent the 8x8 LED matrix but it's length would correspond to the maximum length of a message. For example if the largest message to be displayed was 16 characters long, the array would need to be 8x128 in size. The IC's main loop would read an 8x8 section of this array and turn on LED's in positions where a 1 rather than a 0 exists within the 8x8 section. After the loop runs, a counter would be incremented to move the 8x8 section over by one pixel to make the text scroll across the screen (not every loop, as this loop runs many times per second, but you get the idea).

Anyone who has worked with microcontrollers knows how bad of an idea this was. The ATmega328P only has 2kB of SRAM memory, meaning that you can only store 2048 bytes of data inside standard variables. A 8x128 array would require 1024 bytes, which is half of our total available SRAM! Add in boilerplate Ardiuno code, other variables and code, and you quickly run out of memory. The reason this error was so devastating for me was due to the fact that I wasn't running this with a debugger or any output. The out of memory error was a silent failure and manifested itself by simply not displaying a message, or stopping partway through scrolling a message across the screen. I don't recall what woke me up to the SRAM limitation, but man was I glad to find out why I couldn't make this work.

This realization forced me to begin storing many variables within flash memory, of which the 328P has 32kB via PROGMEM. Consider that each message string requires one byte per character, and you realize that even storing the messages requires nearly half a kilobyte. Once I started using PROGMEM for most large variables, things started moving smoothly.

const PROGMEM char string_0[] = " BRETTS DEVICE";
const PROGMEM char string_1[] = " LOUIS RIEL";
const PROGMEM char string_2[] = " TRANSPORTATION";
const PROGMEM char string_3[] = " COPERNICUS";
const PROGMEM char string_4[] = " ALLEN AND WRIGHTS";
const PROGMEM char string_5[] = " VAFGEHZRAG";
const PROGMEM char string_6[] = " ZREPHEL";
const PROGMEM char string_7[] = " 271";
const PROGMEM char string_8[] = " SVFU";
const PROGMEM char string_9[] = " ABORY";
const PROGMEM char string_10[] = " NTEVPHYGHER";
const PROGMEM char string_11[] = " FCVAXF";
const PROGMEM char string_12[] = " PELVAT JBYS";

I refactored my code to use a 8x16 screen buffer rather than an 8x128 one. Binary representation of text would be loaded in one column at a time as the text scrolled:

  bool marquee[16][8]; //screen buffer array
  bool letter[8]; //single character to be loaded here
  int counter = 0;
  int shifterMax = ((int)(x / 8));
  if (shifterMax < messageLen) {
    shifterMax++;
  }
  for (int i = ((int)(x / 8)); i <= shifterMax; i++) {
    for (int j = 0; j < 8; j++) {
      int index = ((int)message[i]) - 48;
      if (index < 0) {
        index = 16;
      }
      convertCharToBool(pgm_read_word(&letters[index][j]), letter);
      for (int k = 0; k < 8; k++) {
        marquee[counter + k][j] = letter[7 - k]; //load character into screen buffer
      }
    }
    counter = counter + 8;
  }
  for (int i = 0; i < 8; i++) {
    for (int j = 0; j < 8; j++) {
      if (marquee[i+(x%8)][j] && i+x < messageBits) {
        watch.drawPixel(i, j, 200); //turn on/off LED's based on binary representation
      }
    }
  }

convertCharToBool() reads a single character from a flash stored message/clue so that it can be displayed. It does this by loading it into the letter variable, which is then written into the screen buffer (only partially as the text scrolls by).

Once I got the prototype working, I ordered another dozen watches and programmed their IC's ahead of the event. The only other change to my original plan was that halfway through the clues no longer came out as plain text but instead used a Caesar Cipher/ROT13 cipher to obfuscate the answers.

Results

Overall the event was a huge success. When we created the teams we made sure that at least one team member was reasonably proficient with a soldering iron, although for many it had been a decade or longer since they last picked one up. Of the 12 teams, two teams devices didn't work when they were finished soldering and assembling them. We deducted points but gave them the answer sheet as a replacement for the device.

Soldering Decoder Devices

Every participant at the event received their own Geekcreit® UNO R3 Basic Starter Learning Kit For Arduino.

Geekcreit UNO Basic Starter Learning Kit for Arduino

Geekcreit UNO Basic Starter Learning Kit for Arduino

A week after the event I held a lunch and learn lab where everyone brought their kits and we built a temperature sensor that displayed live data on an LCD screen using the parts found in the kit.

Lunch and learn Arduino lab

Lunch and learn Arduino lab

Lunch and learn Arduino lab

In our follow up after the event the feedback we received was overwhelmingly positive. I was very pleased to have an opportunity to expose like minded people to Arduino and get them excited about technology and hardware.

Want to try something similar?

Build the watch, install the software, connect the FTDI programmer, then compile and upload your modified code. Make sure to set the Arduino IDE "Board" to Uno.

© 2018. All Rights Reserved.

Proudly published with Ghost