Story

When my Dad's 60th birthday was coming up, I knew that I had to create something with tech. In our family we usually do something to keep the crowd entertained and each member of the family comes up with their own thing. Since I had some PunchThrough Bean lying around and a 3d printer that I rarely used I saw a great opportunity to use all of those.

So the idea was to create a quiz that we could project onto a wall but instead of some manual entry, have the audience vote on the right answer from the comfort of their chairs. For this purpose I wanted to create multiple "boxes" or devices with 4 buttons and a screen which would show the choice they voted for.

Here is a view of what those boxes looked like in their final iteration:

Design process

External parts:

on/off switch

empty space for and 8x8 led matrix

4 buttons

mini-usb charging port

To arrive at this final design I went through 4 major iterations on the box design, changing the size and adding extra spaces to hold the various components inside:

You can see the square area for the screen which simply shines through the plastic, the cutouts for the buttons and the space on the right side for the on/off switch. The Bean goes into the top of the box, just above the screen.

Software

To bring all this functionality together I wrote a small windows program that checks for Bean Bluetooth devices and allows you to connect to them. A javascript bridge then sends any button press to a webpage to interpret.

The voting part was done completely as a static webpage and can easily be updated with pictures and quiz questions.

On the Bean side I wrote a small custom program to interpret the button presses and send out a specific event via Bluetooth to any connected device. It would also play custom animations on the screen for some extra fun :-)

Lid

Schematics

Schematic

Schematic

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 changesuint8_tpinMap[]={BUTTON_A,BUTTON_B,BUTTON_C,BUTTON_D};uint8_tpinValues[]={0,0,0,0};uint8_tbuttons[]={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_tmsg_status=0x01;uint8_tmsg_button=0x02;uint8_tmsg_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.*/LedControllc=LedControl(LED_DI,LED_CLK,LED_LOAD,1);typedefstructKFRAME{floattime;int8_tx,y;boolvisible;float(*easing)(float);uint8_t*img;};typedefstructANIMATION{floattime;floatx,y;boolvisible;structKFRAME*cur_frame,*next_frame;uint8_t*img;structKFRAME*frames;};uint8_tbuffer[8];uint8_timg_A[]={5,B01111110,B00010001,B00010001,B00010001,B01111110};uint8_timg_smiley[]={8,B00100001,B01000000,B10000000,B10000000,B10000000,B10000000,B01000000,B00100001};uint8_timg_smiley_wink[]={8,B00100000,B01000000,B10000000,B10000000,B10000000,B10000000,B01000000,B00100000};uint8_timg_smiley_small[]={8,B00000000,B01000010,B10000000,B10000000,B10000000,B10000000,B01000010,B00000000};uint8_timg_cross[]={8,B10000001,B01000010,B00100100,B00011000,B00011000,B00100100,B01000010,B10000001};uint8_timg_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_timg_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_timg_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}};structKFRAMEani_RtoL[]={{0.0,8,0,true,&easeInLinear},{2.0,-8,0,true},{-1}};structKFRAMEani_Bounce[]={{0.0,0,-12,true,&easeOutBounce},{2.0,0,0,true},{-1}};structKFRAMEani_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}};structKFRAMEani_Move[]={{0.0,0,0,true,&easeInLinear},{0.2,0,0,true},{0.3,-1,0,true},{0.6,8,0,true},{-1}};structKFRAMEani_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}};structKFRAMEani_smile[]={{0.0,0,0,false},{1.0,0,0,true,0,img_smiley},{1.6,0,0,true,0,img_smiley},{-1}};structKFRAMEani_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}};structANIMATIONanims[MAX_ANIMS];boolhas_anims=false,timer_running=false;voidrender(int8_tx,uint8_t*data);voidrender_buffer(int8_tx,uint8_ty,uint8_t*data);// 0: start, 1: after buttonuint8_tstate=0;uint16_tstateTicks=0;voidsetState(uint8_ts){state=s;stateTicks=0;}/* Animations*/voidanim_init(){has_anims=false;for(uint8_ti=0;i<MAX_ANIMS;i++)anims[i].time=-1;}floateaseInLinear(floattime){returntime;}floateaseOutBounce(floattime){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);}voidanim_tick(floatduration){// little speed-up for when there are no animationsif(!has_anims||duration<0)return;clear_buffer();Bean.setLed(random(256),random(256),random(256));has_anims=false;for(uint8_ti=0;i<MAX_ANIMS;i++){structANIMATION*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 settingsif(anim->visible)render(round(anim->x),round(anim->y),anim->img);// the animation ends when the next frame has time < 0if(anim->cur_frame->time<0){anim->time=-1;continue;}has_anims=true;// advance the time and interpolate valuesanim->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 frameif(anim->next_frame->time<0){anim->cur_frame=anim->next_frame;}else{floatt=anim->time-anim->cur_frame->time;floatdt=anim->next_frame->time-anim->cur_frame->time;if(dt!=0){if(anim->cur_frame->easing==NULL)anim->cur_frame->easing=&easeInLinear;floatdiff=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 timerif(!has_anims)Timer1.stop();}voidanim_add(uint8_t*img,structKFRAME*frames){if(img==NULL||frames==NULL||frames[0].time<0)return;// find the first free animation slotfor(uint8_ti=0;i<MAX_ANIMS;i++){structANIMATION*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*/voidrender(int8_tx,uint8_t*data){// outside of left or right side of screenif(data==NULL||x+data[0]<0||x>7)return;uint8_tlen=(x+data[0]<8?data[0]:8-x);for(int8_ti=(x<0?-x:0);i<len;i++){lc.setRow(0,x+i,data[i+1]);}}voidclear_buffer(){memset(buffer,0,sizeof(buffer));}voidrender(int8_tx,int8_ty,uint8_t*data){// outside of left or right side of screenif(data==NULL||x+data[0]<0||x>7||y+8<0||y>7)return;uint8_tlen=(x+data[0]<8?data[0]:8-x);// special case when needing to shiftif(y>0){for(int8_ti=(x<0?-x:0);i<len;i++)buffer[x+i]|=data[i+1]<<y;}elseif(y<0){uint8_tshift=(-y);for(int8_ti=(x<0?-x:0);i<len;i++)buffer[x+i]|=data[i+1]>>shift;}else{for(int8_ti=(x<0?-x:0);i<len;i++)buffer[x+i]|=data[i+1];}}voidrender_buffer(){for(int8_ti=0;i<8;i++){lc.setRow(0,i,buffer[i]);}}/* the setup routine runs once when you press reset:*/voidsetup(){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 inputsfor(inti=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);}voidanimIsr(){anim_tick(0.1);}voidpause(uint32_tduration){// use Bean.sleep if there are no animations to save power but delay if we have to animate so the timings workif(!has_anims)Bean.sleep(duration);elsedelay(duration);}voidserialEvent(){charbuffer[64];size_tlength=64;while(Serial.available()){length=Serial.readBytes(buffer,length);// read an input pinif(length>0){if(buffer[0]==msg_button){sendButtons();}elseif(buffer[0]==msg_voting){}else{// blink green to acknowledge readBean.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:*/voidloop(){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 animationif(stateTicks>10){if(state!=2&&state!=0)anim_add(img_smiley,ani_Bounce);elseanim_add(img_smiley,ani_wink);setState(2);}}voidsendButtons(){charbuffer[sizeof(buttons)+1];buffer[0]=msg_button;memcpy(buffer+1,buttons,sizeof(buttons));intwritten=Serial.write((uint8_t*)buffer,sizeof(buffer));if(written>0)memset(buttons,0,sizeof(buttons));}voiddigitalChanged(){boolnotify=false;for(inti=0;i<sizeof(pinMap);i++){uint8_tpinState=digitalRead(pinMap[i]);if(pinState==pinValues[i])continue;// if switching from 1 to 0 then the button was just pressedif(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.