Jens Elstner
Published

Bluetooth Voting Box

Get a room full of people to vote on a quiz wirelessly!

IntermediateFull instructions provided1,584
Bluetooth Voting Box

Things used in this project

Hardware components

LightBlue Bean
Punch Through LightBlue Bean
×1
TP4056 Mini USB 5V Micro USB1A Lithium Battery Charging Board (25 x 19 x 10mm)
×1
Momentary SPST NO Red Round Cap Push Button (25 x 19mm)
×4
Cascadable Arduino PIC 8x8 LED 5.5V matrix MAX7219 (5 x 3.2 x 1.5 cm, L x W x H)
×1
500mA DC-DC 1V - 5V To 5V Converter Step Up Module (26 x 18mm)
×1
3 Pin 2 Position 1P2T SPDT ON-OFF Miniature (19 x 5 x 5mm)
×1

Software apps and online services

Visual Studio 2015
Microsoft Visual Studio 2015
Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Box

The box with the space for display, buttons and circuit boards

Lid

The lid to close the box and with extra space for the charging circuit board

Schematics

Schematic

Schematic

Code

Arduino (Bean) code

C/C++
This is the source code running on the Bean
#include <avr/wdt.h>
#include "LedControl.h"
#include "TimerOne.h"


#define BUTTON_A 2
#define BUTTON_B 3
#define BUTTON_C 4
#define BUTTON_D 5
#define LED_DI A0
#define LED_LOAD A1
#define LED_CLK 0

#define MAX_ANIMS 8

// Notify the client over serial when a digital pin state changes
uint8_t pinMap[] = {BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D};
uint8_t pinValues[] = {0, 0, 0, 0};
uint8_t buttons[] = {0, 0, 0, 0};

/*
  Communication protocol:
  type (1 byte), data
  0x01: button press
    1 byte: each bit = 1 button (0 = A, ...)
  0x02: enable / disable voting
    1 byte: 0 = disable, 1 = enable
*/
uint8_t msg_status = 0x01;
uint8_t msg_button = 0x02;
uint8_t msg_voting = 0x03;


/*
  pin 3 is connected to the DataIn 
  pin 4 is connected to the CLK 
  pin 5 is connected to LOAD 
  We have only a single MAX72XX.
*/
LedControl lc = LedControl(LED_DI, LED_CLK, LED_LOAD, 1);

typedef struct KFRAME
{
  float time;
  int8_t x, y;
  bool visible;
  float (*easing)(float);
  uint8_t *img;
};

typedef struct ANIMATION
{
  float time;
  float x, y;
  bool visible;
  struct KFRAME *cur_frame, *next_frame;
  uint8_t *img;
  struct KFRAME *frames;
};

uint8_t buffer[8];
uint8_t img_A[]={5, B01111110, B00010001, B00010001, B00010001, B01111110};
uint8_t img_smiley[]={8, B00100001, B01000000, B10000000, B10000000, B10000000, B10000000, B01000000, B00100001};
uint8_t img_smiley_wink[]={8, B00100000, B01000000, B10000000, B10000000, B10000000, B10000000, B01000000, B00100000};
uint8_t img_smiley_small[]={8, B00000000, B01000010, B10000000, B10000000, B10000000, B10000000, B01000010, B00000000};
uint8_t img_cross[]={8, B10000001, B01000010, B00100100, B00011000, B00011000, B00100100, B01000010, B10000001};

uint8_t img_num[10][4]={
  {3, B00011111, B00010001, B00011111},
  {3, B00000000, B00000000, B00011111},
  {3, B00011101, B00010101, B00010111},
  {3, B00010101, B00010101, B00011111},
  {3, B00000111, B00000100, B00011111},
  {3, B00010111, B00010101, B00011101},
  {3, B00011111, B00010101, B00011101},
  {3, B00000001, B00000001, B00011111},
  {3, B00011111, B00010101, B00011111},
  {3, B00010111, B00010101, B00011111}
};
uint8_t img_letter[4][9]={
  {8, B11000000, B00110000, B00011100, B00010011, B00010011, B00011100, B00110000, B11000000},
  {8, B11111111, B10001001, B10001001, B10001001, B10001001, B10001001, B10001001, B01110110},
  {8, B00111100, B01000010, B10000001, B10000001, B10000001, B10000001, B10000001, B01000010},
  {8, B11111111, B10000001, B10000001, B10000001, B10000001, B10000001, B01000010, B00111100}
};
uint8_t img_start[15][9]={
  {8, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00},
  {8, 0x00, 0x00, 0x00, 0x10, 0x20, 0x00, 0x00, 0x00},
  {8, 0x00, 0x00, 0x00, 0x10, 0x30, 0x18, 0x00, 0x00},
  {8, 0x00, 0x00, 0x04, 0x14, 0x3C, 0x18, 0x00, 0x00},
  {8, 0x00, 0x38, 0x04, 0x1C, 0x3C, 0x18, 0x00, 0x00},
  {8, 0x00, 0x38, 0x7C, 0xBC, 0xBC, 0x98, 0x00, 0x00},
  {8, 0x00, 0x38, 0x7C, 0xFC, 0xFC, 0xF8, 0x40, 0x3C},
  {8, 0x02, 0x39, 0x7D, 0xFD, 0xFD, 0xFF, 0x7E, 0x3C},
  {8, 0x0E, 0x3F, 0x7F, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C},
  {8, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C},
  {8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
  {8, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF},
  {8, 0xFF, 0xFF, 0xFF, 0xE7, 0xE7, 0xFF, 0xFF, 0xFF},
  {8, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF},
  {8, 0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xFF}
};


struct KFRAME ani_RtoL[] = { {0.0, 8, 0, true, &easeInLinear}, {2.0, -8, 0, true}, {-1} };
struct KFRAME ani_Bounce[] = { {0.0, 0, -12, true, &easeOutBounce}, {2.0, 0, 0, true}, {-1} };
struct KFRAME ani_Blink[] = { {0.0, 0, 0, true}, {0.25, 0, 0, false}, {0.5, 0, 0, true}, {0.75, 0, 0, false}, {1.0, 0, 0, false}, {-1} };
struct KFRAME ani_Move[] = { {0.0, 0, 0, true, &easeInLinear}, {0.2, 0, 0, true}, {0.3, -1, 0, true}, {0.6, 8, 0, true}, {-1} };
struct KFRAME ani_start[] = { {0.0, 0, 0, true, 0, (uint8_t*) &img_start[0]}, {0.1, 0, 0, true, 0, (uint8_t*) &img_start[1]}, 
                              {0.2, 0, 0, true, 0, (uint8_t*) &img_start[2]}, {0.3, 0, 0, true, 0, (uint8_t*) &img_start[3]}, 
                              {0.4, 0, 0, true, 0, (uint8_t*) &img_start[4]}, {0.5, 0, 0, true, 0, (uint8_t*) &img_start[5]}, 
                              {0.6, 0, 0, true, 0, (uint8_t*) &img_start[6]}, {0.7, 0, 0, true, 0, (uint8_t*) &img_start[7]}, 
                              {0.8, 0, 0, true, 0, (uint8_t*) &img_start[8]}, {0.9, 0, 0, true, 0, (uint8_t*) &img_start[9]}, 
                              {1.0, 0, 0, true, 0, (uint8_t*) &img_start[10]}, {1.1, 0, 0, true, 0, (uint8_t*) &img_start[11]}, 
                              {1.2, 0, 0, true, 0, (uint8_t*) &img_start[12]}, {1.3, 0, 0, true, 0, (uint8_t*) &img_start[13]}, 
                              {1.4, 0, 0, true, 0, (uint8_t*) &img_start[14]}, {1.5, 0, 0, false, 0, (uint8_t*) &img_start[14]}, {-1} };
struct KFRAME ani_smile[] = { {0.0, 0, 0, false}, {1.0, 0, 0, true, 0, img_smiley}, {1.6, 0, 0, true, 0, img_smiley}, {-1} };
struct KFRAME ani_wink[] = { {0.0, 0, 0, true, 0, (uint8_t*) &img_smiley}, {0.1, 0, 0, true, 0, (uint8_t*) &img_smiley_wink}, {0.2, 0, 0, true, 0, (uint8_t*) &img_smiley}, {-1} };

struct ANIMATION anims[MAX_ANIMS];
bool has_anims = false, timer_running = false;


void render(int8_t x, uint8_t* data);
void render_buffer(int8_t x, uint8_t y, uint8_t* data);

// 0: start, 1: after button
uint8_t state = 0;
uint16_t stateTicks = 0;
void setState(uint8_t s) {
  state = s;
  stateTicks = 0;
}

/*
  Animations
*/
void anim_init() {
  has_anims = false;
  for (uint8_t i = 0; i < MAX_ANIMS; i++)
    anims[i].time = -1;
}
float easeInLinear(float time) {
  return time;
}
float easeOutBounce(float time) {
  if(time < (1/2.75))
    return (7.5625 * time * time);
  if(time < (2/2.75))
  {
    time -= 1.5/2.75;
    return (7.5625 * time * time + 0.75);
  }
  if(time < (2.5/2.75))
  {
    time -= 2.25/2.75;
    return (7.5625 * time * time + 0.9375);
  }
  time -= 2.625/2.75;
  return (7.5625 * time * time + 0.984375);
}

void anim_tick(float duration) {
   // little speed-up for when there are no animations
  if (!has_anims || duration < 0)
    return;
  clear_buffer();

  Bean.setLed(random(256),random(256),random(256));
  
  has_anims = false;
  for (uint8_t i = 0; i < MAX_ANIMS; i++) {
    struct ANIMATION* anim = &anims[i];
    if (anim->time < 0)
      continue;

    //render(1, 0, img_num[(uint8_t) (anim->time/10)]);
    //render(5, 0, img_num[((uint8_t) anim->time)%10]);

    // render to buffer with current settings
    if (anim->visible)
      render(round(anim->x), round(anim->y), anim->img);

    // the animation ends when the next frame has time < 0
    if (anim->cur_frame->time < 0) {
      anim->time = -1;
      continue;
    }
    has_anims = true;
    
    // advance the time and interpolate values
    anim->time += duration;
    while (anim->time > anim->next_frame->time && anim->next_frame->time >= 0) {
      anim->cur_frame++;
      anim->next_frame++;
    }
    anim->x = anim->cur_frame->x;
    anim->y = anim->cur_frame->y;
    anim->visible = anim->cur_frame->visible;
    if (anim->cur_frame->img != NULL)
      anim->img = anim->cur_frame->img;
    
    // next render will render the last frame
    if (anim->next_frame->time < 0) {
      anim->cur_frame = anim->next_frame;
    } 
    else {
      float t = anim->time - anim->cur_frame->time;
      float dt = anim->next_frame->time - anim->cur_frame->time;
      if(dt != 0) {
        if (anim->cur_frame->easing == NULL)
          anim->cur_frame->easing = &easeInLinear;
        float diff = anim->next_frame->x - anim->cur_frame->x;
        if (diff != 0)
          anim->x += diff * anim->cur_frame->easing(t / dt);
        diff = anim->next_frame->y - anim->cur_frame->y;
        if (diff != 0)
          anim->y += diff * anim->cur_frame->easing(t / dt);
      }
    }
  }
  render_buffer();
  
  // when all animations are done we can stop the timer
  if (!has_anims)
    Timer1.stop();
}

void anim_add(uint8_t* img, struct KFRAME* frames) {
  if (img == NULL || frames == NULL || frames[0].time < 0)
    return;
  // find the first free animation slot
  for (uint8_t i = 0; i < MAX_ANIMS; i++) {
    struct ANIMATION* anim = &anims[i];
    if (anim->time > -1)
      continue;
    anim->time = 0;
    anim->img = img;
    anim->frames = frames;
    anim->cur_frame = &frames[0];
    anim->next_frame = &frames[1];
    anim->x = frames[0].x;
    anim->y = frames[0].y;
    anim->visible = frames[0].visible;
    if (frames[0].img != NULL)
      anim->img = frames[0].img;
    has_anims = true;
    break;
  }
  
  if (has_anims && !timer_running) {
    Timer1.initialize(100000);          // set a timer of length 100000 microseconds (or 0.1 sec - or 10Hz)
    Timer1.attachInterrupt( animIsr ); // attach the service routine here
  }
}


/*
  Render one image
*/
void render(int8_t x, uint8_t* data) {
  // outside of left or right side of screen
  if (data == NULL || x + data[0] < 0 || x > 7)
    return;
  uint8_t len = (x + data[0] < 8 ? data[0] : 8 - x);
  for (int8_t i = (x < 0 ? -x : 0); i < len; i++) {
    lc.setRow(0, x + i, data[i + 1]);
  }
}

void clear_buffer() {
  memset(buffer, 0, sizeof(buffer));
}
void render(int8_t x, int8_t y, uint8_t* data) {
  // outside of left or right side of screen
  if (data == NULL || x + data[0] < 0 || x > 7 || y + 8 < 0 || y > 7)
    return;
  uint8_t len = (x + data[0] < 8 ? data[0] : 8 - x);
  // special case when needing to shift
  if (y > 0) {
    for (int8_t i = (x < 0 ? -x : 0); i < len; i++)
      buffer[x + i] |= data[i + 1] << y;
  } else if (y < 0) {
    uint8_t shift = (-y);
    for (int8_t i = (x < 0 ? -x : 0); i < len; i++)
      buffer[x + i] |= data[i + 1] >> shift;
  } else {
    for (int8_t i = (x < 0 ? -x : 0); i < len; i++)
      buffer[x + i] |= data[i + 1];
  }
}
void render_buffer() {
  for (int8_t i = 0; i < 8; i++) {
    lc.setRow(0, i, buffer[i]);
  }
}


/*
  the setup routine runs once when you press reset:
*/
void setup() {
  wdt_enable(WDTO_2S);
  
  // initialize serial communication at 57600 bits per second:
  Serial.begin(57600);
  
  // this makes it so that the arduino read function returns
  // immediatly if there are no less bytes than asked for.
  Serial.setTimeout(25);
  
  Serial.print("Listening...");

  // Digital pins, use analog pins as digital inputs
  for (int i = 0; i < sizeof(pinMap); i++) {
    pinMode(pinMap[i], INPUT_PULLUP);  
    //Bean.attachChangeInterrupt(pinMap[i], digitalChanged);
  }

  /*
   The MAX72XX is in power-saving mode on startup,
   we have to do a wakeup call
   */
  lc.shutdown(0, false);
  /* Set the brightness to a medium values */
  lc.setIntensity(0, 8);
  /* and clear the display */
  lc.clearDisplay(0);

//  render(0, img_smiley);
  anim_init();
  //anim_add(img_smiley, ani_Bounce);
  anim_add(img_smiley, ani_smile);
  anim_add(img_smiley, ani_start);
}

void animIsr() {
  anim_tick(0.1);
}

void pause(uint32_t duration) {
  // use Bean.sleep if there are no animations to save power but delay if we have to animate so the timings work
  if (!has_anims)
    Bean.sleep(duration);
  else
    delay(duration);
}

void serialEvent() {
  char buffer[64];
  size_t length = 64; 
  
  while (Serial.available()) {
    length = Serial.readBytes(buffer, length);
  
    // read an input pin
    if (length > 0)
    {
      if (buffer[0] == msg_button) {
        sendButtons();
      }
      else if (buffer[0] == msg_voting) {
      }
      else {
        // blink green to acknowledge read
        Bean.setLed(0,255,0);
        Serial.write((uint8_t*)buffer, length); 
        pause(250);
        Bean.setLed(0, 0, 0);
      }
    }
  }
}


/*
  the loop routine runs over and over again forever:
*/
void loop() {
        Bean.setLed(255,255,255);
        pause(250);
        Bean.setLed(0, 0, 0);
        pause(250);

  wdt_reset();
  
  serialEvent();

  pause(1000);
  stateTicks++;
  
  // if after button press get back to start animation
  if (stateTicks > 10) {
    if (state != 2 && state != 0)
      anim_add(img_smiley, ani_Bounce);
    else
      anim_add(img_smiley, ani_wink);
    setState(2);
  }
}


void sendButtons() {
  char buffer[sizeof(buttons) + 1];
  buffer[0] = msg_button;
  memcpy(buffer + 1, buttons, sizeof(buttons));
  int written = Serial.write((uint8_t*) buffer, sizeof(buffer)); 
  if (written > 0)
    memset(buttons, 0, sizeof(buttons));
}


void digitalChanged() {
  bool notify = false;
  for (int i = 0; i < sizeof(pinMap); i++)
  {
    uint8_t pinState = digitalRead(pinMap[i]);
    if (pinState == pinValues[i])
      continue;
    // if switching from 1 to 0 then the button was just pressed
    if (pinValues[i] == 1 && pinState == 0) {
      buttons[i]++;
      anim_add(img_letter[i], ani_Blink);
      notify = true; 
    }
    pinValues[i] = pinState;
  }
  
  if (notify)
  {
    sendButtons();
    setState(1);
  }
}

BeanBrowser

This application can load a static webpage and link it to button presses on the Bean through a JavaScript bridge.

Credits

Jens Elstner

Jens Elstner

1 project • 0 followers
Tinkerer and coder

Comments