Things used in this project

Hardware components:
Cutout cortado grande
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:
Vs2015logo
Microsoft Visual Studio 2015
Ide web
Arduino IDE
Hand tools and fabrication machines:
3drag
3D Printer (generic)

Custom parts and enclosures

The box with the space for display, buttons and circuit boards
The lid to close the box and with extra space for the charging circuit board

Schematics

Schematic
voting_box_BBH4bEU9mW.fzz
Schematic
Voting box schem kdm6elm7l9

Code

Arduino (Bean) codeC/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
Jens Elstner

Tinkerer and coder

Replications

Did you replicate this project? Share it!

Love this project? Think it could be improved? Tell us what you think!

Give feedback

Comments

Similar projects you might like

End-Effector and Control Logic for Robot
Intermediate
  • 3,157
  • 24

For my thesis, I have developed the control system and end-effector for a robot, easily implementable and economic.

Theremino Adapter for CNC.
Intermediate
  • 284
  • 7

Full instructions

Attention: This adapter is used to replace the parallel port with the USB, and not to operate Mach3 CNC or Linux.

Theremino Logger
Intermediate
  • 162
  • 4

Datalogger is simple to use but with great performance. The base, concise and intuitive.

Theremino - Access to Slots “WriteSlot”  “ReadSlot”.
Intermediate
  • 44
  • 3

Full instructions

A description of how to communicate with the slots of Theremino System by simply calling the functions: “WriteSlot” and “ReadSlot”.

Theremino - Use the serial port to write data to Slots.
Intermediate
  • 80
  • 3

Theremino_SeriHAL lets you send data to slots from a serial port.

Pulse Train HAT Pick & Place Example
Intermediate
  • 418
  • 3

Protip

With this example, we show how to use the PTHAT and Raspberry Pi being used to learn commands for pick and place applications.

Add projectSign up / Login
Respect project