Attiny85 UFO Escape Keychain Game

I finally had some free time to make something fun and decided to write a new game for the Attiny85 ssd1306 keychain.  I reused the sleep, display and interrupts code from the “Breakout” game I created a while ago.

This time I wanted a more addictive game. The game had to be playable using just the two buttons. I thought a racing game might be a good choice but the landscape screen is more suitable for a side-scroller. So I settled for an obstacle avoiding side scroller. Similar games include the recent Flappy Bird, Nyan Cat and many variations of the Helicopter game.

The flying saucer is propelled with the left button while the right button is used to shoot through force fields. The game gets harder as you play:

 

/* 2015
 * UFO Escape game by Ilya Titov. Find building instructions on http://webboggles.com/
 * The code that does not fall under the licenses of sources listed below can be used non commercially with attribution.
 *
 * If you have problems uploading this sketch, this is probably due to sketch size - you need to update ld.exe in arduino\hardware\tools\avr\avr\bin
 * https://github.com/TCWORLD/ATTinyCore/tree/master/PCREL%20Patch%20for%20GCC
 *
 * This sketch is using the screen control and font functions written by Neven Boyanov for the http://tinusaur.wordpress.com/ project
 * Source code and font files available at: https://bitbucket.org/tinusaur/ssd1306xled
 * 
 * Sleep code is based on this blog post by Matthew Little:
 * http://www.re-innovation.co.uk/web12/index.php/en/blog-75/306-sleep-modes-on-attiny85
*/
#include <EEPROM.h>
#include "font6x8.h"
#include <avr/sleep.h>
#include <avr/interrupt.h> // needed for the additional interrupt


boolean stopAnimate = 0; // this is set to 1 when a collision is detected
int maxObstacles = 1; // this defines the max number of in game obstacles
int obstacleStep = 1; // pixel step of obstacles per frame
int obstacle[9] = {-50,-50,-50,-50,-50,-50,-50,-50,-50}; // x offset of the obstacle default position, out of view
int gapOffset[9] = {0,0,0,0,0,0,0,0,0}; // y offset of the fly-through gap
int gapSize[9]; // y height of the gap
byte maxGap = 60; // max height of the gap
int stepsSinceLastObstacle = 0; // so obstacles are not too close
byte gapBlock[9] = {0,0,0,0,0,0,0,0,0}; // if the fly-through gap is closed
byte blockChance = 0; // this higher value decreases the likelihood of gap being closed 
boolean fire = 0; // set to 1 when the fire interrupt is triggered
byte fireCount = 0; // the shot is persistent for several frames
byte playerOffset = 0; // y offset of the top of the player
byte flames = 0; // this is set to 1 when the move up interrupt is triggered

byte flameMask[2]={B00111111,B11111111}; // this is used to only show the flame part of the icon when moving up


int score = 0; // score - this affects the difficulty of the game
ISR(PCINT0_vect){ // PB0 pin button interrupt			     
   //if (playerOffset >1&&stopAnimate==0){playerOffset-=1;} // for debounce, the movement is in the main loop//
   return;
}
void playerInc(){ // PB2 pin button interrupt
   fire = 1;
   fireCount = 5; // number of frames the shot will persist
}

void setup() {
  resetGame();
  DDRB = 0b00000010;  	// set PB1 as output (for the speaker)
  PCMSK = 0b00000001;	// pin change mask: listen to portb bit 1
  GIMSK |= 0b00100000;	// enable PCINT interrupt 
  sei();	        // enable all interrupts
  attachInterrupt(0,playerInc,RISING);
}
void loop() { 
      delay(40);
      noInterrupts();
      ssd1306_init();
      ssd1306_fillscreen(0x00);
      ssd1306_char_f6x8(16, 4, "U F O  E S C A P E");
      ssd1306_char_f6x8(20, 6, "webboggles.com");
      beep(200,600);          beep(300,200);          beep(400,300);
      
      delay(2000);

	while (1==1) {

              //update game vars to make it harder to play
              if (score < 500){blockChance = 11-score/50;  maxObstacles=score/70+1;}
              if (score > 130){obstacleStep = 2;}
              if (score < 2000){maxGap = 60-score/100;}
              if (fire == 1){score--;}
              if (fireCount>0){fireCount--;}
              
                if (digitalRead(0)==1){if (playerOffset >0 && stopAnimate==0){playerOffset--; flames = 1; // move player up
                  for (int i = 0; i<2; i++){
                    beep(1,random(0,i*2));
                  }
                }} 
                if (digitalRead(0)==1){if (playerOffset >0 && stopAnimate==0){playerOffset--; flames = 1; // move player up
                  for (int i = 0; i<2; i++){
                    beep(1,random(0,i*2));
                  }
                }}
                if (digitalRead(0)==1){if (playerOffset >0 && stopAnimate==0){playerOffset--; flames = 1; // move player up
                  for (int i = 0; i<2; i++){
                    beep(1,random(0,i*2));
                  }
                }}
                stepsSinceLastObstacle += obstacleStep;
                for (byte i = 0; i<maxObstacles;i++){ // fly obstacles
                  if (obstacle[i] >= 0 && obstacle[i] <= 128 && stopAnimate==0){
                    obstacle[i] -= obstacleStep;
                    if (gapBlock[i]>0 && obstacle[i] < 36  && playerOffset>gapOffset[i] && playerOffset+5<gapOffset[i]+gapSize[i] && fireCount > 0){//
                       gapBlock[i] = 0;
                       score += 5; 
                       for (byte cp = 400; cp>0; cp--){
                         beep(1,cp);
                       }
                    }
                  } 
                  
                  if (obstacle[i]<=4 && stepsSinceLastObstacle>=random(30,100)){ // generate new obstacles
                    obstacle[i] = 123;
                    gapSize[i] = random(25,maxGap);
                    gapOffset[i] = random(0,64-gapSize[i]);
                    if (random(0,blockChance)==0){gapBlock[i] = 1;}else {gapBlock[i] = 0;}
                    stepsSinceLastObstacle = 0;
                    score+=1;
                  }
                }
                
                if (playerOffset < 56 && stopAnimate==0){playerOffset++;} // player gravity
                
                delay(20/maxObstacles); // controls game speed, more obstacles take longer to be sent to the screen
                if (stopAnimate==0){ssd1306_clearscreen();}
              

              // update whats on the screen
                 
                  noInterrupts();
                  // Send Obstacle
                  for (byte i = 0; i<maxObstacles;i++){
                    if (obstacle[i] >= -5 && obstacle[i] <= 128){ // only deal with visible obstacles
                      if (obstacle[i] > 8 && obstacle[i] <16){ // look for collision if obstacle is near the player
                        if (playerOffset < gapOffset[i] || playerOffset+5 > gapOffset[i]+gapSize[i] || gapBlock[i] != 0){
                          // collision!
                          stopAnimate = 1; 
                          int topScore = EEPROM.read(0);
                          topScore = topScore << 8;
                          topScore = topScore |  EEPROM.read(1);
                          if (score>topScore){topScore = score; EEPROM.write(1,topScore & 0xFF); EEPROM.write(0,(topScore>>8) & 0xFF); }

                          ssd1306_char_f6x8(32, 3, "Game Over");
                          ssd1306_char_f6x8(32, 5, "score:");
                          char temp[10] = {0,0,0,0,0,0,0,0,0,0};
                          itoa(score,temp,10);
                          ssd1306_char_f6x8(70, 5, temp);
                          ssd1306_char_f6x8(32, 6, "top score:");
                          itoa(topScore,temp,10);
                          ssd1306_char_f6x8(90, 6, temp);
                          for (int i = 0; i<1000; i++){
                            beep(1,random(0,i*2));
                          }
                          delay(2000);
                          interrupts();
                          system_sleep();
                          resetGame();
                          noInterrupts(); 
                        }
                      }                      
                      
                      for (byte row = 0; row <8; row++){
                        
                          ssd1306_setpos(obstacle[i],row);
                          ssd1306_send_data_start();
                          
                          if (obstacle[i]>0&&obstacle[i] < 128){
                             
                             if ((row+1)*8 - gapOffset[i] <= 8){ // generate obstacle : top and transition
                                byte temp = B11111111>>((row+1)*8 - gapOffset[i]); 
                                byte tempB = B00000000; 
                                if (gapBlock[i]>0){tempB=B10101010;}
                                ssd1306_send_byte(temp);
                                ssd1306_send_byte(temp|tempB>>1);
                                ssd1306_send_byte(temp|tempB);
                                ssd1306_send_byte(temp);
                                
                             }else if (row*8>=gapOffset[i] && (row+1)*8<=gapOffset[i]+gapSize[i]){ // middle gap
                                byte tempB = B00000000; 
                                if (gapBlock[i]>0){tempB=B10101010;}
                                ssd1306_send_byte(B00000000);
                                ssd1306_send_byte(B00000000|tempB>>1);
                                ssd1306_send_byte(B00000000|tempB);
                                ssd1306_send_byte(B00000000);

                             }else if ((gapOffset[i] +gapSize[i]) >= row*8 && (gapOffset[i] +gapSize[i]) <= (row+1)*8){ // bottom transition
                                //}else if ((gapOffset[i] +gapSize[i]) >= row*8 && (gapOffset[i] +gapSize[i]) <= (row+1)*8){ // bottom transition
                                //byte temp = B11111111<<((gapOffset[i] + gapSize[i])%8); 
                                
                                byte temp = B11111111<<((gapOffset[i] + gapSize[i])%8); 
                                byte tempB = B00000000; 
                                if (gapBlock[i]>0){tempB=B10101010;}
                                ssd1306_send_byte(temp);
                                ssd1306_send_byte(temp|tempB>>1);
                                ssd1306_send_byte(temp|tempB);
                                ssd1306_send_byte(temp);
                                
                             }else { // fill rest of obstacle
                                ssd1306_send_byte(B11111111);
                                ssd1306_send_byte(B11111111);
                                ssd1306_send_byte(B11111111);
                                ssd1306_send_byte(B11111111);
                             }
                          ssd1306_send_data_stop();
                          }
                        }
                   
                    }
                  }
                  
                 
                 
                 if (playerOffset%8!=0){ // overflow the player icon into the next screen row if split
                      ssd1306_setpos(8,playerOffset/8);
                      ssd1306_send_data_start();
                        if (stopAnimate==0){
                            ssd1306_send_byte((B00001100&flameMask[flames])<<playerOffset%8);
                            ssd1306_send_byte((B01011110&flameMask[flames])<<playerOffset%8);
                            ssd1306_send_byte((B10010111&flameMask[flames])<<playerOffset%8);
                            ssd1306_send_byte((B01010011&flameMask[flames])<<playerOffset%8);
                            ssd1306_send_byte((B01010011&flameMask[flames])<<playerOffset%8);
                            ssd1306_send_byte((B10010111&flameMask[flames])<<playerOffset%8);
                            ssd1306_send_byte((B01011110&flameMask[flames])<<playerOffset%8);
                            ssd1306_send_byte((B00001100&flameMask[flames])<<playerOffset%8);
                            if (fireCount >0){
                                for (byte f = 0; f<=24; f++){
                                    ssd1306_send_byte(B00000100<<playerOffset%8);
                                }
                                ssd1306_send_byte(B00010101<<playerOffset%8);
                                ssd1306_send_byte(B00001010<<playerOffset%8);
                                ssd1306_send_byte(B00010101<<playerOffset%8);
                                if (fire==1){beep(50,100);}
                                fire = 0;
                              
                            }
                        }else {
                            ssd1306_send_byte((B00001100&flameMask[flames] | random(0,255))<<playerOffset%8);
                            ssd1306_send_byte((B01011110&flameMask[flames] | random(0,255))<<playerOffset%8);
                            ssd1306_send_byte((B10010111&flameMask[flames] | random(0,255))<<playerOffset%8);
                            ssd1306_send_byte((B01010011&flameMask[flames] | random(0,255))<<playerOffset%8);
                            ssd1306_send_byte((B01010011&flameMask[flames] | random(0,255))<<playerOffset%8);
                            ssd1306_send_byte((B10010111&flameMask[flames] | random(0,255))<<playerOffset%8);
                            ssd1306_send_byte((B01011110&flameMask[flames] | random(0,255))<<playerOffset%8);
                            ssd1306_send_byte((B00001100&flameMask[flames] | random(0,255))<<playerOffset%8);
                        }
                        
                      ssd1306_send_data_stop();
                      ssd1306_setpos(8,playerOffset/8+1);
                      ssd1306_send_data_start();
                        if (stopAnimate==0){
                            ssd1306_send_byte((B00001100&flameMask[flames])>>8-playerOffset%8);
                            ssd1306_send_byte((B01011110&flameMask[flames])>>8-playerOffset%8);
                            ssd1306_send_byte((B10010111&flameMask[flames])>>8-playerOffset%8);
                            ssd1306_send_byte((B01010011&flameMask[flames])>>8-playerOffset%8);
                            ssd1306_send_byte((B01010011&flameMask[flames])>>8-playerOffset%8);
                            ssd1306_send_byte((B10010111&flameMask[flames])>>8-playerOffset%8);
                            ssd1306_send_byte((B01011110&flameMask[flames])>>8-playerOffset%8);
                            ssd1306_send_byte((B00001100&flameMask[flames])>>8-playerOffset%8);
                            if (fireCount >0){
                                for (byte f = 0; f<=24; f++){
                                    ssd1306_send_byte(B00000100>>8-playerOffset%8);
                                }
                                ssd1306_send_byte(B00010101>>8-playerOffset%8);
                                ssd1306_send_byte(B00001010>>8-playerOffset%8);
                                ssd1306_send_byte(B00010101>>8-playerOffset%8);
                                if (fire==1){beep(50,100);}
                                fire = 0;
                              
                            }
                        }else {
                            ssd1306_send_byte((B00001100&flameMask[flames] | random(0,255))>>8-playerOffset%8);
                            ssd1306_send_byte((B01011110&flameMask[flames] | random(0,255))>>8-playerOffset%8);
                            ssd1306_send_byte((B10010111&flameMask[flames] | random(0,255))>>8-playerOffset%8);
                            ssd1306_send_byte((B01010011&flameMask[flames] | random(0,255))>>8-playerOffset%8);
                            ssd1306_send_byte((B01010011&flameMask[flames] | random(0,255))>>8-playerOffset%8);
                            ssd1306_send_byte((B10010111&flameMask[flames] | random(0,255))>>8-playerOffset%8);
                            ssd1306_send_byte((B01011110&flameMask[flames] | random(0,255))>>8-playerOffset%8);
                            ssd1306_send_byte((B00001100&flameMask[flames] | random(0,255))>>8-playerOffset%8);
                        }
                      ssd1306_send_data_stop();
                      }else {
                          ssd1306_setpos(8,playerOffset/8);
                          ssd1306_send_data_start();
                          if (stopAnimate == 0){
                            ssd1306_send_byte(B00001100&flameMask[flames]);
                            ssd1306_send_byte(B01011110&flameMask[flames]);
                            ssd1306_send_byte(B10010111&flameMask[flames]);
                            ssd1306_send_byte(B01010011&flameMask[flames]);
                            ssd1306_send_byte(B01010011&flameMask[flames]);
                            ssd1306_send_byte(B10010111&flameMask[flames]);
                            ssd1306_send_byte(B01011110&flameMask[flames]);
                            ssd1306_send_byte(B00001100&flameMask[flames]);
                            if (fireCount >0){
                                for (byte f = 0; f<=24; f++){
                                    ssd1306_send_byte(B00000100);
                                }
                                ssd1306_send_byte(B00010101);
                                ssd1306_send_byte(B00001010);
                                ssd1306_send_byte(B00010101);
                                if (fire==1){beep(50,100);}
                                fire = 0;
                              
                            }
                          }else {
                            ssd1306_send_byte(B00001100&flameMask[flames] | random(0,255));
                            ssd1306_send_byte(B01011110&flameMask[flames] | random(0,255));
                            ssd1306_send_byte(B10010111&flameMask[flames] | random(0,255));
                            ssd1306_send_byte(B01010011&flameMask[flames] | random(0,255));
                            ssd1306_send_byte(B01010011&flameMask[flames] | random(0,255));
                            ssd1306_send_byte(B10010111&flameMask[flames] | random(0,255));
                            ssd1306_send_byte(B01011110&flameMask[flames] | random(0,255));
                            ssd1306_send_byte(B00001100&flameMask[flames] | random(0,255)); 
                          }
                          ssd1306_send_data_stop();
                      }
                      
                      // display score
                      
                      //ssd1306_char_f6x8(32, 8, "score:");
                      
                      char temp[10] = {0,0,0,0,0,0,0,0,0,0};
                      itoa(score,temp,10);
                      ssd1306_char_f6x8(92, 0, temp);
                          
                          
                      flames = 0;
          	  interrupts();
                  
             //    
                     
              
              
              
              if (stopAnimate == 1){
                  for (int i = 0; i<1000; i++){
                    beep(1,random(0,i*2));
                  }
                 delay(2000);
                 system_sleep(); 
              }
       }   
}
void resetGame(){
  ssd1306_char_f6x8(16, 4, "U F O  E S C A P E");
  ssd1306_char_f6x8(20, 6, "webboggles.com");
  beep(200,600);          beep(300,200);          beep(400,300);
  delay(2000);

  stopAnimate = 0;
  score = 0;

  maxObstacles = 3;
  obstacleStep = 2;
  for (byte i = 0; i<9; i++){
    obstacle[i] = -50;
    gapOffset[i]=0;
  }
  stepsSinceLastObstacle = 0;
  playerOffset = 0; // y offset of the top of the player
}



void beep(int bCount,int bDelay){
  for (int i = 0; i<=bCount; i++){digitalWrite(1,HIGH);for(int i2=0; i2<bDelay; i2++){__asm__("nop\n\t");}digitalWrite(1,LOW);for(int i2=0; i2<bDelay; i2++){__asm__("nop\n\t");}}
}
#define DIGITAL_WRITE_HIGH(PORT) PORTB |= (1 << PORT)
#define DIGITAL_WRITE_LOW(PORT) PORTB &= ~(1 << PORT)

// Some code based on "IIC_wtihout_ACK" by http://www.14blog.com/archives/1358
#ifndef SSD1306XLED_H
#define SSD1306XLED_H
// ---------------------	// Vcc,	Pin 1 on SSD1306 Board
// ---------------------	// GND,	Pin 2 on SSD1306 Board
#ifndef SSD1306_SCL
#define SSD1306_SCL		PB3	// SCL,	Pin 3 on SSD1306 Board
#endif
#ifndef SSD1306_SDA
#define SSD1306_SDA		PB4	// SDA,	Pin 4 on SSD1306 Board
#endif
#ifndef SSD1306_SA
#define SSD1306_SA		0x78	// Slave address
#endif
// ----------------------------------------------------------------------------
void ssd1306_init(void);
void ssd1306_xfer_start(void);
void ssd1306_xfer_stop(void);
void ssd1306_send_byte(uint8_t byte);
void ssd1306_send_command(uint8_t command);
void ssd1306_send_data_start(void);
void ssd1306_send_data_stop(void);
void ssd1306_setpos(uint8_t x, uint8_t y);
void ssd1306_fillscreen(uint8_t fill_Data);
void ssd1306_char_f6x8(uint8_t x, uint8_t y, const char ch[]);
//void ssd1306_char_f8x16(uint8_t x, uint8_t y,const char ch[]);
//void ssd1306_char_f16x16(uint8_t x, uint8_t y, uint8_t N);
void ssd1306_draw_bmp(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t bitmap[]);
// ----------------------------------------------------------------------------
#endif
void ssd1306_init(void){
	DDRB |= (1 << SSD1306_SDA);	// Set port as output
	DDRB |= (1 << SSD1306_SCL);	// Set port as output

	ssd1306_send_command(0xAE); // display off
	ssd1306_send_command(0x00); // Set Memory Addressing Mode
	ssd1306_send_command(0x10); // 00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
	ssd1306_send_command(0x40); // Set Page Start Address for Page Addressing Mode,0-7
	ssd1306_send_command(0x81); // Set COM Output Scan Direction
	ssd1306_send_command(0xCF); // ---set low column address
	ssd1306_send_command(0xA1); // ---set high column address
	ssd1306_send_command(0xC8); // --set start line address
	ssd1306_send_command(0xA6); // --set contrast control register
	ssd1306_send_command(0xA8);
	ssd1306_send_command(0x3F); // --set segment re-map 0 to 127
	ssd1306_send_command(0xD3); // --set normal display
	ssd1306_send_command(0x00); // --set multiplex ratio(1 to 64)
	ssd1306_send_command(0xD5); // 
	ssd1306_send_command(0x80); // 0xa4,Output follows RAM content;0xa5,Output ignores RAM content
	ssd1306_send_command(0xD9); // -set display offset
	ssd1306_send_command(0xF1); // -not offset
	ssd1306_send_command(0xDA); // --set display clock divide ratio/oscillator frequency
	ssd1306_send_command(0x12); // --set divide ratio
	ssd1306_send_command(0xDB); // --set pre-charge period
	ssd1306_send_command(0x40); // 
	ssd1306_send_command(0x20); // --set com pins hardware configuration
	ssd1306_send_command(0x02);
	ssd1306_send_command(0x8D); // --set vcomh
	ssd1306_send_command(0x14); // 0x20,0.77xVcc
	ssd1306_send_command(0xA4); // --set DC-DC enable
	ssd1306_send_command(0xA6); // 
	ssd1306_send_command(0xAF); // --turn on oled panel 
}

void ssd1306_xfer_start(void){
	DIGITAL_WRITE_HIGH(SSD1306_SCL);	// Set to HIGH
	DIGITAL_WRITE_HIGH(SSD1306_SDA);	// Set to HIGH
	DIGITAL_WRITE_LOW(SSD1306_SDA);		// Set to LOW
	DIGITAL_WRITE_LOW(SSD1306_SCL);		// Set to LOW
}

void ssd1306_xfer_stop(void){
	DIGITAL_WRITE_LOW(SSD1306_SCL);		// Set to LOW
	DIGITAL_WRITE_LOW(SSD1306_SDA);		// Set to LOW
	DIGITAL_WRITE_HIGH(SSD1306_SCL);	// Set to HIGH
	DIGITAL_WRITE_HIGH(SSD1306_SDA);	// Set to HIGH
}

void ssd1306_send_byte(uint8_t byte){
	uint8_t i;
	for(i=0; i<8; i++)
	{
		if((byte << i) & 0x80)
			DIGITAL_WRITE_HIGH(SSD1306_SDA);
		else
			DIGITAL_WRITE_LOW(SSD1306_SDA);
		
		DIGITAL_WRITE_HIGH(SSD1306_SCL);
		DIGITAL_WRITE_LOW(SSD1306_SCL);
	}
	DIGITAL_WRITE_HIGH(SSD1306_SDA);
	DIGITAL_WRITE_HIGH(SSD1306_SCL);
	DIGITAL_WRITE_LOW(SSD1306_SCL);
}

void ssd1306_send_command(uint8_t command){
	ssd1306_xfer_start();
	ssd1306_send_byte(SSD1306_SA);  // Slave address, SA0=0
	ssd1306_send_byte(0x00);	// write command
	ssd1306_send_byte(command);
	ssd1306_xfer_stop();
}

void ssd1306_send_data_start(void){
	ssd1306_xfer_start();
	ssd1306_send_byte(SSD1306_SA);
	ssd1306_send_byte(0x40);	//write data
}

void ssd1306_send_data_stop(void){
	ssd1306_xfer_stop();
}

void ssd1306_setpos(uint8_t x, uint8_t y)
{
	ssd1306_xfer_start();
	ssd1306_send_byte(SSD1306_SA);  //Slave address,SA0=0
	ssd1306_send_byte(0x00);	//write command

	ssd1306_send_byte(0xb0+y);
	ssd1306_send_byte(((x&0xf0)>>4)|0x10); // |0x10
	ssd1306_send_byte((x&0x0f)|0x01); // |0x01

	ssd1306_xfer_stop();
}

void ssd1306_fillscreen(uint8_t fill_Data){
	uint8_t m,n;
	for(m=0;m<8;m++)
	{
		ssd1306_send_command(0xb0+m);	//page0-page1
		ssd1306_send_command(0x00);		//low column start address
		ssd1306_send_command(0x10);		//high column start address
		ssd1306_send_data_start();
		for(n=0;n<128;n++)
		{
			ssd1306_send_byte(fill_Data);
		}
		ssd1306_send_data_stop();
	}
}
void ssd1306_clearscreen(){

	for(byte m=0;m<8;m++){
		ssd1306_send_command(0xb0+m);	//page0-page1
		ssd1306_send_command(0x00);		//low column start address
		ssd1306_send_command(0x10);		//high column start address
		ssd1306_send_data_start();
		for(byte n=0;n<128;n++){
                	        DIGITAL_WRITE_LOW(SSD1306_SDA);
                		DIGITAL_WRITE_HIGH(SSD1306_SCL);
                		DIGITAL_WRITE_LOW(SSD1306_SCL);
                                	
                	        DIGITAL_WRITE_LOW(SSD1306_SDA);
                		DIGITAL_WRITE_HIGH(SSD1306_SCL);
                		DIGITAL_WRITE_LOW(SSD1306_SCL);
                                	
                	        DIGITAL_WRITE_LOW(SSD1306_SDA);
                		DIGITAL_WRITE_HIGH(SSD1306_SCL);
                		DIGITAL_WRITE_LOW(SSD1306_SCL);
                                	
                	        DIGITAL_WRITE_LOW(SSD1306_SDA);
                		DIGITAL_WRITE_HIGH(SSD1306_SCL);
                		DIGITAL_WRITE_LOW(SSD1306_SCL);
                                	
                	        DIGITAL_WRITE_LOW(SSD1306_SDA);
                		DIGITAL_WRITE_HIGH(SSD1306_SCL);
                		DIGITAL_WRITE_LOW(SSD1306_SCL);
                                	
                	        DIGITAL_WRITE_LOW(SSD1306_SDA);
                		DIGITAL_WRITE_HIGH(SSD1306_SCL);
                		DIGITAL_WRITE_LOW(SSD1306_SCL);
                                	
                	        DIGITAL_WRITE_LOW(SSD1306_SDA);
                		DIGITAL_WRITE_HIGH(SSD1306_SCL);
                		DIGITAL_WRITE_LOW(SSD1306_SCL);
                                	
                	        DIGITAL_WRITE_LOW(SSD1306_SDA);
                		DIGITAL_WRITE_HIGH(SSD1306_SCL);
                		DIGITAL_WRITE_LOW(SSD1306_SCL);
                	
                	DIGITAL_WRITE_HIGH(SSD1306_SDA);
                	DIGITAL_WRITE_HIGH(SSD1306_SCL);
                	DIGITAL_WRITE_LOW(SSD1306_SCL);
		}
		ssd1306_send_data_stop();
	}
}


void ssd1306_char_f6x8(uint8_t x, uint8_t y, const char ch[]){
	uint8_t c,i,j=0;
	while(ch[j] != '\0')
	{
		c = ch[j] - 32;
		if(x>126)
		{
			x=0;
			y++;
		}
		ssd1306_setpos(x,y);
		ssd1306_send_data_start();
		for(i=0;i<6;i++)
		{
			ssd1306_send_byte(pgm_read_byte(&ssd1306xled_font6x8[c*6+i]));
		}
		ssd1306_send_data_stop();
		x += 6;
		j++;
	}
}

// Routines to set and clear bits (used in the sleep code)
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

void system_sleep() {
  ssd1306_fillscreen(0x00);
  ssd1306_send_command(0xAE);
  cbi(ADCSRA,ADEN);                    // switch Analog to Digitalconverter OFF
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
  sleep_enable();
  sleep_mode();                        // System actually sleeps here
  sleep_disable();                     // System continues execution here when watchdog timed out 
  sbi(ADCSRA,ADEN);                    // switch Analog to Digitalconverter ON  
  ssd1306_send_command(0xAF);0
}

Attiny85 Charlieplexed Makerbot Snow Shades

I decided to light up the Snow Shades by MakerBot for a party using an Attiny and some LEDs.

I mocked up LED position and animation:charlieplex14

Then worked out the wiring using the Attiny’s 5 pins.charlieplex14

Modified the model in Blender to allow space for the microchip and LEDs.
cutModel files on Thingiverse.

Using strands of an IDE ribbon cable I connected the LEDs.CWCsPxfXAAAfQmM.jpg large

After connecting the dip socket and batteries I sealed the top with more plastic.IMG_20151218_004224

With Charlieplexed wiring all LEDs can’t be powered on at the same time. The code below pulses the LEDs one after another for different lengths of time to achieve the desired perceived brightness.

 

#include <EEPROM.h>

byte mode = 0;
byte portB=0; byte portMask = 0;
byte dimCount = 0;
byte cBr[14];
int frame = 0;
int frameDuration = 30;
unsigned long lastFrame = 0;
void setup() {                
  mode = EEPROM.read(0);
  mode = mode+1;
  if (mode >= 3){mode = 0;}
  EEPROM.write(0, mode);
}

void loop() {
  while (1==1){
  dimCount++;
  if (dimCount>9){dimCount=0;}
     
      switch (mode) { 
          case 0: //
          frameDuration = 300;
            switch (frame) {
                case 0: //
                  setLED(9,9,9, 9,9,9, 9,9,9,9 ,9,9,9,9); //this sets the led brightness for the duration of one frame
                break;
                case 1: //
                  setLED(0,0,0 ,0,0,0 ,0,0,0,0, 0,0,0,0);
                break;
                case 2: //
                  setLED(9,9,9, 9,9,9, 9,9,9,9 ,9,9,9,9);
                break;
                case 3: //
                  setLED(0,0,0 ,0,0,0 ,0,0,0,0, 0,0,0,0);
                break;
                case 4: //
                  setLED(9,9,9, 9,9,9, 9,9,9,9 ,9,9,9,9);
                break;
                case 5: //
                  setLED(0,0,0, 0,0,0, 9,9,9,9, 9,9,9,9);
                break;
                case 6:
                  setLED(0,0,0, 0,0,0, 9,9,9,0, 0,9,9,9);
                break;
                case 7:
                  setLED(0,0,0, 0,0,0, 9,0,0,0, 0,0,0,9);
                break;
                case 8:
                  setLED(0,0,0, 0,0,0, 0,9,0,0, 0,0,9,0);
                break;
                case 9: //
                  setLED(0,0,0, 0,0,0, 0,0,9,0, 0,9,0,0);
                break;
                case 10: //
                  setLED(0,0,0, 0,0,0, 0,0,0,9, 9,0,0,0);
                break;
                case 11: //
                  setLED(0,0,0, 0,0,0, 0,0,9,0, 0,9,0,0);
                break;
                case 12: //
                  setLED(0,0,0, 0,0,0, 0,9,0,0, 0,0,9,0);
                break;
                case 13: //
                  setLED(0,0,0, 0,0,0, 9,0,0,0, 0,0,0,9);
                break;
                case 14: //
                  setLED(0,0,9, 0,0,9, 0,0,0,0, 0,0,0,0);
                break;
                case 15: //
                  setLED(0,9,0, 0,9,0, 0,0,0,0, 0,0,0,0);
                break;
                case 16: //
                  setLED(9,0,0, 9,0,0, 0,0,0,0, 0,0,0,0);
                break;
                case 17: //
                  setLED(0,0,9, 0,0,9, 0,0,0,0, 0,0,0,0);
                break;
                case 18: //
                  setLED(0,9,0, 0,9,0, 0,0,0,0, 0,0,0,0);
                break;
                case 19: //
                  setLED(9,0,0, 9,0,0, 0,0,0,0, 0,0,0,0);
                break;
                default:
                frame = 0;
                break;
            }
            if (lastFrame+frameDuration<millis()){lastFrame=millis(); frame++;}
          break;
          case 1: //
            switch (frame) {
                case 0: 
                  cBr[0]=0; cBr[1]=3; cBr[2]=7; cBr[3]=7; cBr[4]=3; cBr[5]=0; cBr[6]=0; cBr[7]=3; cBr[8]=7; cBr[9]=9; cBr[10]=9; cBr[11]=7; cBr[12]=3; cBr[13]=0;
                break;
                case 1: //
                  cBr[0]=0; cBr[1]=2; cBr[2]=6; cBr[3]=6; cBr[4]=2; cBr[5]=0; cBr[6]=0; cBr[7]=2; cBr[8]=6; cBr[9]=8; cBr[10]=8; cBr[11]=6; cBr[12]=2; cBr[13]=0;
                break;
                case 2: //
                  cBr[0]=1; cBr[1]=1; cBr[2]=5; cBr[3]=5; cBr[4]=1; cBr[5]=1; cBr[6]=1; cBr[7]=1; cBr[8]=5; cBr[9]=7; cBr[10]=7; cBr[11]=5; cBr[12]=1; cBr[13]=1;
                break;
                case 3:
                  cBr[0]=2; cBr[1]=0; cBr[2]=4; cBr[3]=4; cBr[4]=0; cBr[5]=2; cBr[6]=2; cBr[7]=0; cBr[8]=4; cBr[9]=6; cBr[10]=6; cBr[11]=4; cBr[12]=0; cBr[13]=2;
                break;
                case 4:
                  cBr[0]=3; cBr[1]=0; cBr[2]=3; cBr[3]=3; cBr[4]=0; cBr[5]=3; cBr[6]=3; cBr[7]=0; cBr[8]=3; cBr[9]=5; cBr[10]=5; cBr[11]=3; cBr[12]=0; cBr[13]=3;
                break;
                case 5:
                  cBr[0]=4; cBr[1]=1; cBr[2]=2; cBr[3]=2; cBr[4]=1; cBr[5]=4; cBr[6]=4; cBr[7]=1; cBr[8]=2; cBr[9]=4; cBr[10]=4; cBr[11]=2; cBr[12]=1; cBr[13]=4;
                break;
                case 6:
                  cBr[0]=5; cBr[1]=2; cBr[2]=1; cBr[3]=1; cBr[4]=2; cBr[5]=5; cBr[6]=5; cBr[7]=2; cBr[8]=1; cBr[9]=3; cBr[10]=3; cBr[11]=1; cBr[12]=2; cBr[13]=5;
                break;
                case 7:
                  cBr[0]=6; cBr[1]=3; cBr[2]=0; cBr[3]=0; cBr[4]=3; cBr[5]=6; cBr[6]=6; cBr[7]=3; cBr[8]=0; cBr[9]=2; cBr[10]=2; cBr[11]=0; cBr[12]=3; cBr[13]=6;
                break;
                case 8:
                  cBr[0]=7; cBr[1]=4; cBr[2]=0; cBr[3]=0; cBr[4]=4; cBr[5]=7; cBr[6]=7; cBr[7]=4; cBr[8]=0; cBr[9]=1; cBr[10]=1; cBr[11]=0; cBr[12]=4; cBr[13]=7;
                break;
                case 9:
                  cBr[0]=8; cBr[1]=5; cBr[2]=1; cBr[3]=1; cBr[4]=5; cBr[5]=8; cBr[6]=8; cBr[7]=5; cBr[8]=1; cBr[9]=0; cBr[10]=0; cBr[11]=1; cBr[12]=5; cBr[13]=8;
                break;
                case 10:
                  cBr[0]=9; cBr[1]=6; cBr[2]=2; cBr[3]=2; cBr[4]=6; cBr[5]=9; cBr[6]=9; cBr[7]=6; cBr[8]=2; cBr[9]=0; cBr[10]=0; cBr[11]=2; cBr[12]=6; cBr[13]=9;
                break;
                case 11:
                  cBr[0]=9; cBr[1]=7; cBr[2]=3; cBr[3]=3; cBr[4]=7; cBr[5]=9; cBr[6]=9; cBr[7]=7; cBr[8]=3; cBr[9]=1; cBr[10]=1; cBr[11]=3; cBr[12]=7; cBr[13]=9;
                break;
                case 12:
                  cBr[0]=8; cBr[1]=8; cBr[2]=4; cBr[3]=4; cBr[4]=8; cBr[5]=8; cBr[6]=8; cBr[7]=8; cBr[8]=4; cBr[9]=2; cBr[10]=2; cBr[11]=4; cBr[12]=8; cBr[13]=8;
                break;
                case 13:
                  cBr[0]=7; cBr[1]=9; cBr[2]=5; cBr[3]=5; cBr[4]=9; cBr[5]=7; cBr[6]=7; cBr[7]=9; cBr[8]=5; cBr[9]=3; cBr[10]=3; cBr[11]=5; cBr[12]=9; cBr[13]=7;
                break;
                case 14:
                  cBr[0]=6; cBr[1]=9; cBr[2]=6; cBr[3]=6; cBr[4]=9; cBr[5]=6; cBr[6]=6; cBr[7]=9; cBr[8]=6; cBr[9]=4; cBr[10]=4; cBr[11]=6; cBr[12]=9; cBr[13]=6;
                break;
                case 15:
                  cBr[0]=5; cBr[1]=8; cBr[2]=7; cBr[3]=7; cBr[4]=8; cBr[5]=5; cBr[6]=5; cBr[7]=8; cBr[8]=7; cBr[9]=5; cBr[10]=5; cBr[11]=7; cBr[12]=8; cBr[13]=5;
                break;
                case 16:
                  cBr[0]=4; cBr[1]=7; cBr[2]=8; cBr[3]=8; cBr[4]=7; cBr[5]=4; cBr[6]=4; cBr[7]=7; cBr[8]=8; cBr[9]=6; cBr[10]=6; cBr[11]=8; cBr[12]=7; cBr[13]=4;
                break;
                case 17:
                  cBr[0]=3; cBr[1]=6; cBr[2]=9; cBr[3]=9; cBr[4]=6; cBr[5]=3; cBr[6]=3; cBr[7]=6; cBr[8]=9; cBr[9]=7; cBr[10]=7; cBr[11]=9; cBr[12]=6; cBr[13]=3;
                break;
                case 18:
                  cBr[0]=2; cBr[1]=5; cBr[2]=9; cBr[3]=9; cBr[4]=5; cBr[5]=2; cBr[6]=2; cBr[7]=5; cBr[8]=9; cBr[9]=8; cBr[10]=8; cBr[11]=9; cBr[12]=5; cBr[13]=2;
                break;
                case 19:
                  cBr[0]=1; cBr[1]=4; cBr[2]=8; cBr[3]=8; cBr[4]=4; cBr[5]=1; cBr[6]=1; cBr[7]=4; cBr[8]=8; cBr[9]=9; cBr[10]=9; cBr[11]=8; cBr[12]=4; cBr[13]=1;
                break;
                default:
                frame = 19;
                break;
            }
            if (lastFrame+frameDuration<millis()){lastFrame=millis(); frame--;}
          break;
          case 2: //
            switch (frame) {
                case 0: //
                  setLED(9,3,0, 4,0,8, 9,5,9,2, 6,6,3,1);
                break;
                case 1: //
                  setLED(8,2,1, 3,1,7, 8,6,8,3, 7,5,4,0);
                break;
                case 2: //
                  setLED(7,1,2, 2,2,6, 7,7,7,4, 8,4,5,0);
                break;
                case 3: //
                  setLED(6,0,3 ,1,3,5 ,6,8,6,5, 9,3,6,1);
                break;
                case 4: //
                  setLED(5,0,4 ,0,4,4 ,5,9,5,6, 9,2,7,2);
                break;
                case 5: //
                  setLED(4,1,5 ,0,5,3 ,4,9,4,7, 8,1,8,3);
                break;
                case 6:
                  setLED(3,2,6, 1,6,2, 3,8,3,8, 7,0,9,4);
                break;
                case 7:
                  setLED(2,3,7, 2,7,1, 2,7,2,9, 6,0,9,5);
                break;
                case 8:
                  setLED(1,4,8, 3,8,0, 1,6,1,9, 5,1,8,6);
                break;
                case 9: //
                  setLED(0,5,9, 4,9,0, 0,5,0,8, 4,2,7,7);
                break;
                case 10: //
                  setLED(0,6,9, 5,9,1, 0,4,0,7, 3,3,6,8);
                break;
                case 11: //
                  setLED(1,7,8, 6,8,2, 1,3,1,6, 2,4,5,9);
                break;
                case 12: //
                  setLED(2,8,7, 7,7,3, 2,2,2,5, 1,5,4,9);
                break;
                case 13: //
                  setLED(3,9,6, 8,6,4, 3,1,3,4, 0,6,3,8);
                break;
                case 14: //
                  setLED(4,9,5, 9,5,5, 4,0,4,3, 0,7,2,7);
                break;
                case 15: //
                  setLED(5,8,4, 9,4,6, 5,0,5,2, 1,8,1,6);
                break;
                case 16: //
                  setLED(6,7,3, 8,3,7, 6,1,6,1, 2,9,0,5);
                break;
                case 17: //
                  setLED(7,6,2, 7,2,8, 7,2,7,0, 3,9,0,4);
                break;
                case 18: //
                  setLED(8,5,1, 6,1,9, 8,3,8,0, 4,8,1,3);
                break;
                case 19: //
                  setLED(9,4,0, 5,0,9, 9,4,9,1, 5,7,2,2);
                break;
                default:
                frame = 0;
                break;
            }
            if (lastFrame+frameDuration<millis()){lastFrame=millis(); frame++;}
          break;
          
          case 3:
          
          break;
          case 4:
            
          break;
      }
      pull_port();  
  }
}

void pull_port (){
//  byte portOn = B00000000; byte mortMode = B00000000;
  if (cBr[0]>dimCount){PORTB = B00010000; DDRB = B00010100;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[1]>dimCount){PORTB = B00000100; DDRB = B00010100;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[2]>dimCount){PORTB = B00000100; DDRB = B00001100;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[3]>dimCount){PORTB = B00001000; DDRB = B00001100;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[4]>dimCount){PORTB = B00000100; DDRB = B00000101;}else {dimAll();}  
  delayMicroseconds(1);
  if (cBr[5]>dimCount){PORTB = B00000001; DDRB = B00000101;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[6]>dimCount){PORTB = B00000010; DDRB = B00001010;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[7]>dimCount){PORTB = B00001000; DDRB = B00001010;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[8]>dimCount){PORTB = B00010000; DDRB = B00011000;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[9]>dimCount){PORTB = B00001000; DDRB = B00011000;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[10]>dimCount){PORTB = B00000001; DDRB = B00000011;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[11]>dimCount){PORTB = B00000010; DDRB = B00000011;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[12]>dimCount){PORTB = B00000100; DDRB = B00000110;}else {dimAll();}
  delayMicroseconds(1);
  if (cBr[13]>dimCount){PORTB = B00000010; DDRB = B00000110;}else {dimAll();}
  delayMicroseconds(1);
  dimAll(); // to make sure the last led has the same brightness
}
void setLED(byte L1, byte L2, byte L3, byte L4, byte L5, byte L6, byte L7, byte L8, byte L9, byte L10, byte L11, byte L12, byte L13, byte L14 ){
  cBr[0]=L1; cBr[1]=L2; cBr[2]=L3; cBr[3]=L4; cBr[4]=L5; cBr[5]=L6; cBr[6]=L7; cBr[7]=L8; cBr[8]=L9; cBr[9]=L10; cBr[10]=L11; cBr[11]=L12; cBr[12]=L13; cBr[13]=L14;
}
void dimAll(){
   PORTB = B00000000; DDRB = B00000000; // dim all 
}

 
Merry Christmas!

IMG_20151218_014039

Loading CSV markers data into Google Maps API

Here is a nifty solution designed to populate a google map with geo location markers and infowindow popup boxes.

First you’ll need to get a Google Maps API developer key.

Then we need to prep the data — we need a csv table with a title, content, latitude and longtitude values:
gmaps_data

If you open the file with a text editor it will look like this:

Title,Content,Lat,Long
Stonehenge,Stonehenge is a prehistoric monument located in Wiltshire,51.1788823,-1.8262155
Bodiam Castle,Bodiam Castle is a 14th-century moated castle near Robertsbridge in East Sussex,51.002266,0.543549
Eden Project,The Eden Project is an artificial biodome visitor attraction in Cornwall,50.3601344,-4.7447178

Next we need the code that will read the csv file line by line and generate the javascript to feed the data to Google Maps API: https://drive.google.com/folderview?id=0B3bO_ZNaxxnlc2FiLWxfbTZCeFU&usp=sharing

<?php
$data = file_get_contents('google_map_data.csv');
$api_key = 'your key here';
$lines = explode("\n",$data);

foreach ($lines as $key => $value){
	if ($key>0&&strlen($value)>20){ // skip csv header row and blank rows
		$line = explode(",",$value);
		$markers[$key] = trim($line[0]).','.trim($line[1]).','.trim($line[2]).','.trim($line[3]); // title,content,lat,long
	}
}
?>
<!DOCTYPE html>
<html>
  <head>
    <style type="text/css">
      html, body, #map-canvas { height: 100%; margin: 0; padding: 0;}
    </style>
    <script type="text/javascript"
      src="https://maps.googleapis.com/maps/api/js?key=<?=$api_key?>">
    </script>
    <script type="text/javascript">
		var map;
		var marker = {};
		function initialize() {
			var mapOptions = {
			center: { lat: 51.1788823, lng: -1.8262155},
			zoom: 5
		};
		map = new google.maps.Map(document.getElementById('map-canvas'),mapOptions);
		var markers = [];
		<?php 
			$counter = 0;
			foreach ($markers as $index => $list){
				   $marker_details = explode(',',$list);
				   echo 'markers["m'.($index-1).'"] = {};'."\n";
				   echo "markers['m".($index-1)."'].lat = '".$marker_details[2]."';\n";
				   echo "markers['m".($index-1)."'].lon = '".$marker_details[3]."';\n";
				   echo "markers['m".($index-1)."'].name = '".$marker_details[0]."';\n";
				   echo "markers['m".($index-1)."'].content = '".$marker_details[1]."';\n";
				   $counter++;
		   }
		?>
		var totalMarkers = <?=$counter?>;
		var i = 0;
		var infowindow;
		var contentString;
		for (var i = 0; i<totalMarkers; i++){
			
			contentString = '<div class="content">'+
				  '<h1 class="firstHeading">'+markers['m'+i].name+'</h1>'+
				  '<div class="bodyContent">'+
				  '<p>'+markers['m'+i].content+'</p>'+
				  '</div>'+
				  '</div>';
			
			
			infowindow = new google.maps.InfoWindow({
				  content: contentString
			});

			marker['c'+i] = new google.maps.Marker({
					position: new google.maps.LatLng(markers['m'+i].lat,markers['m'+i].lon),
					map: map,
					title: markers['m'+i].name,
					infowindow: infowindow
			  });
			//console.log(markers['m'+i].lat+','+markers['m'+i].lon);
			google.maps.event.addListener(marker['c'+i], 'click', function() {
					for (var key in marker){
						marker[key].infowindow.close();
					}
					this.infowindow.open(map, this);
					
			});
		}

      }
	  function panMap(la,lo){
			map.panTo(new google.maps.LatLng(la,lo));
			
	  }
	  function openMarker(mName){
		  //console.log(marker);
		  for (var key in marker){
			  marker[key].infowindow.close();
		  }
		  for (var key in marker){
			  
			if (marker[key].title.search(mName) != -1){
				marker[key].infowindow.open(map,marker[key]);
			}
		  }
	  }
      google.maps.event.addDomListener(window, 'load', initialize);
    </script>
  </head>
  <body>
<div id="map-canvas"></div>
  </body>
</html>

Thats it, when you run the php file you will be presented with a google map that can be embedded using an iframe tag.

You can extend the script with custom icons by adding another column to the csv file with the icon file path and adding an “icon:markers[‘m’+i].icon” option to the google.maps.Marker function.

Blue Screen of Death Caused by Wacom Driver

So if you get a blue screen of death when using a Wacom Intuos Pro tablet on your desktop, the Wacom driver may be to blame. The error message reads DRIVER_POWER_STATE_FAILURE 0x0000009f

In my case the issue was preceeded by the tablet switching off when I plugged the usb cable in.

To fix the issue, unistall the Wacom tablet in Control Panel > Applications and Features, then restart and reinstall. The issue should go away.

Virtual Tour Online Embedder


There are plenty of ways to use your equirectangular panoramas online. The main downside of the programs offering this functionality is the fiddly time consuming process you have to go through to publish the panoramas online, whether they have been saved as an .swf or as an html package. If you want compatibility you’d go with html and get a folder with half a dozen files that you have to upload to the website. If you want to have a blog then you would have to use ftp to upload the files and then work out a way of making it all work. I wanted a simple to use tool that would take care of the embedding of the panoramas and minimise the work involved. Here is how it goes:

  1. Create your 360 panorama (using photoshop, hugin, pano tools)
  2. Upload the jpeg of the panorama to http://orb.photo/embed_panoramas/
  3. Tweak the size and the embed options
  4. Copy the code and paste it into the html/text of your website or blog.

Thats it, it does the fiddly work for you. You can embed photospheres right from the google nexus phone if you wanted and, the plugin has a “Page background” option where the panorama fills the complete screen and is floating behind any content you’ve got. (Make sure that you don’t have non-transparent elements above z-index:-1 covering the panorama if nothing shows up.) The panorama embed code looks like this:

<iframe src="http://orb.photo/embedded_player.php?view=2015021387f3d2139222ad5ddcd26a6c69c4cd38" width="900" height="600" frameborder="0" scrolling="no">Please enable iframes to view content.</iframe>

The tool is available on the orb.photo website   The script uses webgl and javascript to place the equirectangular texture over a sphere which is then animated for the immersive experience. Javascript implementation ensures the panoramas will work on the mobile devices.

Hover compatibility of dropdown menus on touch devices.

I’ve seen different attempts to make the dropdown menus work intuitively on touch devices and while some are trying to utilise the double tap (not the best as this triggers zoom) and various jquery contraptions, all you need is for the top level link to ignore the first tap and let the user see the dropdown.

This can be done by placing a bit of javascript into the onclick property of the anchor tag that prevents location change and self destructs so the link becomes clickable on the second interaction:

<a onclick="if ('ontouchstart' in document.documentElement){this.onclick=''; return false;}" href="/news/">News</a>

Use Wacom tablet’s buttons to streamline your workflow

Design software is becoming more functional and sophisticated but as far as productivity goes — us humans still have to press the buttons to make stuff happen.IMG_20150103_221634

Don’t get me wrong, if you are just starting out in design college it is ok to mouse through the menus to get to a function for those 20 objects you are working on. But when you are trying to get the job done as quick as possible, nothing beats the speed and satisfaction of using keyboard shortcuts and pre-defined macros.

Adobe software is generally great with keyboard shortcuts and in this example scenario I will go through using the Wacom Intuos Pro’s ExpressKeys to improve on built in shortcuts by applying custom text heading styles to an Indesign project using the physical buttons of a Wacom tablet.

You can of course apply the custom styles via the character style menu
styles

Or you could press Ctrl+Enter and use the quick apply to look up the style by typing your custom style name
ctrlenter

But if you have a Wacom tablet you can program the tablet’s physical buttons that will apply up to 8 commonly used styles and boost your performance. To do so, you will need to open the Wacom Tablet Properties, click on the Functions icon and pick the application you want to define the functions for.
express-keys

Pick the Keystroke option for the button you are programming and type Ctrl+Enter followed by the custom style name and then enter again exactly as if you were doing a quick-apply:
macros

That’s it, you’ve just created a physical button to save yourself over a dozen keypresses to get to the page title style. Repeat this for all the actions you will be using and enjoy the quicker and easier workflow.
screen

Attiny85 Breakout Keychain Game

attiny So, what can you do with Attiny’s 5 i/o pins?

UPDATE: New game, “UFO Escape” side scroller

IMG_20140923_152553

I saw this great Attiny OLED project on http://tinusaur.wordpress.com/ and decided to try out the screen (It will be very handy as a mode display for a remote for my http://orb.photo/ project)

So I set out to make myself familiar with the screen and make something fun in the process. I made a remake of the classic breakout game, here is a video of the gameplay:

Parts required:

  • Attiny85 + dip8 socket
  • SSD1306 OLED screen
  • 2x push buttons
  • 2x resistors (10kOhm optimal)
  • 3x4cm copper clad board
  • Piezo speaker
  • 3V 2032 coin cell battery
  • Paper clip

So first of all you want to create the PCB, to do this just design the layout (printable pcb pdf)

Print the design on a page from a glossy magazine with a laser printer and use an iron or a laminator to transfer the design to the copper. Put the PCB into ferric chloride until the copper is etched away

IMG_20140921_141441

Clean off the toner with acetone:

IMG_20140921_141543

Drill the holes and solder the parts. The speaker is soldered to PB1 pin on the attiny and ground. Connect the large ground pads with a bit of wire. Bend and solder the paper clip to keep the battery in place.

IMG_20140923_150658

3D print the case from Thingiverse

IMG_20140923_133214

Program the attiny85 with the code and enjoy:

/* 2014
 * Breakout game by Ilya Titov. Find building instructions on http://webboggles.com/
 * The code that does not fall under the licenses of sources listed below can be used non commercially with attribution.
 *
 * If you have problems uploading this sketch, this is probably due to sketch size - you need to update ld.exe in arduino\hardware\tools\avr\avr\bin
 * https://github.com/TCWORLD/ATTinyCore/tree/master/PCREL%20Patch%20for%20GCC
 *
 * This sketch is using the screen control and font functions written by Neven Boyanov for the http://tinusaur.wordpress.com/ project
 * Source code and font files available at: https://bitbucket.org/tinusaur/ssd1306xled
 * 
 * Sleep code is based on this blog post by Matthew Little:
 * http://www.re-innovation.co.uk/web12/index.php/en/blog-75/306-sleep-modes-on-attiny85
*/
#include <EEPROM.h>
#include "font6x8.h"
#include <avr/sleep.h>
#include <avr/interrupt.h> // needed for the additional interrupt

volatile byte player = 0; //0 to 128-platformWidth  - this is the position of the bounce platform
byte platformWidth = 16; 
byte ballx = 62; // coordinate of the ball
byte bally = 50; // coordinate of the ball
int vdir = -1; // vertical direction and step  distance
int hdir = -1; // horizontal direction and step distance
long lastFrame = 0; // time since the screen was updated last
boolean row1[16]; // on-off array of blocks
boolean row2[16];
boolean row3[16];
int score = 0; // score - counts the number of blocks hit and resets the array above when devisible by 48(total blocks)
ISR(PCINT0_vect){ // PB0 pin button interrupt			     
   if (player >0){player--;} 
   return;
}
void playerInc(){ // PB2 pin button interrupt
  if (player <128-platformWidth){player++;}
}

void setup() {
  resetGame();
  DDRB = 0b00000010;  	// set PB1 as output (for the speaker)
  PCMSK = 0b00000001;	// pin change mask: listen to portb bit 1
  GIMSK |= 0b00100000;	// enable PCINT interrupt 
  sei();			// enable all interrupts
  attachInterrupt(0,playerInc,CHANGE);
  lastFrame = millis();
}
void loop() { 
  delay(40);
      noInterrupts();
      ssd1306_init();
      ssd1306_fillscreen(0x00);
      ssd1306_char_f6x8(16, 4, "B R E A K O U T");
      ssd1306_char_f6x8(20, 6, "webboggles.com");
      beep(200,600);          beep(300,200);          beep(400,300);
      delay(2000);
	while (1==1) {
              // continue moving after the interrupt
              if (digitalRead(2)==1){if (player <128-platformWidth){player++;} if (player <128-platformWidth){player++;} if (player <128-platformWidth){player++;}}               if (digitalRead(0)==1){if (player >0){player--;} if (player >0){player--;} if (player >0){player--;}}

              // bounce off the sides of the screen
              if ((bally+vdir<54&&vdir==1)||(bally-vdir>1&&vdir==-1)){bally+=vdir;}else {vdir = vdir*-1;}
              if ((ballx+hdir<127&&hdir==1)||(ballx-hdir>1&&hdir==-1)){ballx+=hdir;}else {hdir = hdir*-1;}

              // frame actions
              if (lastFrame+10<millis()){                 if(bally>10&&bally+vdir>=54&&(ballx<player||ballx>player+platformWidth)){ // game over if the ball misses the platform
                  int topScore = EEPROM.read(0);
                  topScore = topScore << 8;                   topScore = topScore |  EEPROM.read(1);                                      if (score>topScore){topScore = score; EEPROM.write(1,topScore & 0xFF); EEPROM.write(0,(topScore>>8) & 0xFF); }
                  ssd1306_fillscreen(0x00);                
                  ssd1306_char_f6x8(32, 3, "Game Over");
                  ssd1306_char_f6x8(32, 5, "score:");
                  char temp[4] = {0,0,0,0};
                  itoa(score,temp,10);
                  ssd1306_char_f6x8(70, 5, temp);
                  ssd1306_char_f6x8(32, 6, "top score:");
                  itoa(topScore,temp,10);
                  ssd1306_char_f6x8(90, 6, temp);
                  for (int i = 0; i<1000; i++){
                    beep(1,random(0,i*2));
                  }
                  delay(1000);
                  system_sleep();
                  resetGame();
                }else if (ballx<player+platformWidth/2&&bally>10&&bally+vdir>=54){ // if the ball hits left of the platform bounce left
                  hdir=-1; beep(20,600);
                }else if (ballx>player+platformWidth/2&&bally>10&&bally+vdir>=54){  // if the ball hits right of the platform bounce right
                  hdir=1; beep(20,600);
                }else if (bally+vdir>=54){
                  hdir=1; beep(20,600);
                }

                collisionCheck: // go back to here if a collision was detected to prevent flying through a rigid
                if (floor((bally+vdir)/8)==2){
                  if (row3[ballx/8]==1){row3[ballx/8]=0; score++;  
                      collision(); goto collisionCheck; // check collision for the new direction to prevent flying through a rigid
                  }
                }else if (floor((bally+vdir)/8)==1){
                  if (row2[ballx/8]==1){row2[ballx/8]=0; score++; 
                      collision(); goto collisionCheck;
                  }
                }else if (floor((bally+vdir)/8)==0){
                  if (row1[ballx/8]==1){row1[ballx/8]=0; score++;
                      collision(); goto collisionCheck;
                  }
                }

                // reset blocks if all have been hit
                if (score%48==0){ 
                  for (byte i =0; i<16;i++){
                   row1[i]=1; row2[i]=1; row3[i]=1;
                  } 
                }
              }

              // update whats on the screen
                  noInterrupts();

                  // blocks
                  ssd1306_setpos(0,0);
                  ssd1306_send_data_start();
                  for (int bl = 0; bl <16; bl++){
                    if(row1[bl]==1){
                      sendBlock(1);
                    }else {
                      sendBlock(0);
                    }
                   }   
                  ssd1306_send_data_stop();
                  ssd1306_setpos(0,1);
                  ssd1306_send_data_start();
                  for (int bl = 0; bl <16; bl++){
                    if(row2[bl]==1){
                      sendBlock(1);
                    }else {
                      sendBlock(0);
                    }
                   }   
                  ssd1306_send_data_stop();
                  ssd1306_setpos(0,2);
                  ssd1306_send_data_start();
                  for (int bl = 0; bl <16; bl++){
                    if(row3[bl]==1){
                      sendBlock(1);
                    }else {
                      sendBlock(0);
                    }
                   }   
                  ssd1306_send_data_stop();

                  // clear area below the blocks
                  ssd1306_setpos(0,3);
                  ssd1306_send_data_start();
                  for (byte i =0; i<128; i++){
                     ssd1306_send_byte(B00000000);
                  }
                  ssd1306_send_data_stop();
                  ssd1306_setpos(0,4);
                  ssd1306_send_data_start();
                  for (byte i =0; i<128; i++){
                     ssd1306_send_byte(B00000000);
                  }
                  ssd1306_send_data_stop();
                  ssd1306_setpos(0,5);
                  ssd1306_send_data_start();
                  for (byte i =0; i<128; i++){
                     ssd1306_send_byte(B00000000);
                  }
                  ssd1306_send_data_stop();
                  ssd1306_setpos(0,6);
                  ssd1306_send_data_start();
                  for (byte i =0; i<128; i++){
                     ssd1306_send_byte(B00000000);
                  }
                  ssd1306_send_data_stop();
                  ssd1306_setpos(0,7);
                  ssd1306_send_data_start();
                  for (byte i =0; i<128; i++){
                     ssd1306_send_byte(B00000000);
                  }
                  ssd1306_send_data_stop();

                  // draw ball
                  ssd1306_setpos(ballx,bally/8);
                  uint8_t temp = B00000001;
                  ssd1306_send_data_start();
                  temp = temp << bally%8+1;
                  ssd1306_send_byte(temp);  
                  ssd1306_send_data_stop();

                  drawPlatform();
          	  interrupts();
             //     

	}
}
void resetGame(){
  ssd1306_char_f6x8(16, 4, "B R E A K O U T");
  ssd1306_char_f6x8(20, 6, "webboggles.com");
  beep(200,600);          beep(300,200);          beep(400,300);
  delay(2000);
  for (byte i =0; i<16;i++){ // reset blocks
   row1[i]=1; row2[i]=1; row3[i]=1;
  } 
  platformWidth = 16;
  ballx = 64;
  bally = 50;
  hdir = -1;
  vdir = -1;
  score = 0;
  player = random(0,128-platformWidth);
  ballx = player+platformWidth/2;
}

void collision(){ // the collsision check is actually done befor this is called, this code works out where the ball will bounce
  if ((bally+vdir)%8==7&&(ballx+hdir)%8==7){ // bottom right corner
      if (vdir==1){hdir=1;}else if(vdir==-1&&hdir==1){vdir=1;}else {hdir=1;vdir=1;}
    }else if ((bally+vdir)%8==7&&(ballx+hdir)%8==0){ // bottom left corner
      if (vdir==1){hdir=-1;}else if(vdir==-1&&hdir==-1){vdir=1;}else {hdir=-1;vdir=1;}
    }else if ((bally+vdir)%8==0&&(ballx+hdir)%8==0){ // top left corner
      if (vdir==-1){hdir=-1;}else if(vdir==1&&hdir==-1){vdir=-1;}else {hdir=-1;vdir=-1;}
    }else if ((bally+vdir)%8==0&&(ballx+hdir)%8==7){ // top right corner
      if (vdir==-1){hdir=1;}else if(vdir==1&&hdir==1){vdir=-1;}else {hdir=1;vdir=-1;}
    }else if ((bally+vdir)%8==7){ // bottom side
      vdir = 1;
    }else if ((bally+vdir)%8==0){ // top side
      vdir = -1;
    }else if ((ballx+hdir)%8==7){ // right side
      hdir = 1;
    }else if ((ballx+hdir)%8==0){ // left side
      hdir = -1;
    }else {
      hdir = hdir*-1; vdir = vdir*-1;
  }

  beep(30,300);

}
void drawPlatform(){
 noInterrupts();
 ssd1306_setpos(player,7);
 ssd1306_send_data_start();
 for (byte pw = 1; pw <platformWidth; pw++){ssd1306_send_byte(B00000011);}                
 ssd1306_send_data_stop();  
 interrupts(); 
}
void sendBlock(boolean fill){
  if (fill==1){
   ssd1306_send_byte(B00000000);
   ssd1306_send_byte(B01111110);
   ssd1306_send_byte(B01111110);
   ssd1306_send_byte(B01111110);
   ssd1306_send_byte(B01111110);
   ssd1306_send_byte(B01111110);
   ssd1306_send_byte(B01111110);
   ssd1306_send_byte(B00000000);
  }else {
   ssd1306_send_byte(B00000000);
   ssd1306_send_byte(B00000000);
   ssd1306_send_byte(B00000000);
   ssd1306_send_byte(B00000000);
   ssd1306_send_byte(B00000000);
   ssd1306_send_byte(B00000000);
   ssd1306_send_byte(B00000000);
   ssd1306_send_byte(B00000000);
  } 
}
void beep(int bCount,int bDelay){
  for (int i = 0; i<=bCount; i++){digitalWrite(1,HIGH);for(int i2=0; i2<bDelay; i2++){__asm__("nop\n\t");}digitalWrite(1,LOW);for(int i2=0; i2<bDelay; i2++){__asm__("nop\n\t");}}
}
#define DIGITAL_WRITE_HIGH(PORT) PORTB |= (1 << PORT)
#define DIGITAL_WRITE_LOW(PORT) PORTB &= ~(1 << PORT)

// Some code based on "IIC_wtihout_ACK" by http://www.14blog.com/archives/1358
#ifndef SSD1306XLED_H
#define SSD1306XLED_H
// ---------------------	// Vcc,	Pin 1 on SSD1306 Board
// ---------------------	// GND,	Pin 2 on SSD1306 Board
#ifndef SSD1306_SCL
#define SSD1306_SCL		PB3	// SCL,	Pin 3 on SSD1306 Board
#endif
#ifndef SSD1306_SDA
#define SSD1306_SDA		PB4	// SDA,	Pin 4 on SSD1306 Board
#endif
#ifndef SSD1306_SA
#define SSD1306_SA		0x78	// Slave address
#endif
// ----------------------------------------------------------------------------
void ssd1306_init(void);
void ssd1306_xfer_start(void);
void ssd1306_xfer_stop(void);
void ssd1306_send_byte(uint8_t byte);
void ssd1306_send_command(uint8_t command);
void ssd1306_send_data_start(void);
void ssd1306_send_data_stop(void);
void ssd1306_setpos(uint8_t x, uint8_t y);
void ssd1306_fillscreen(uint8_t fill_Data);
void ssd1306_char_f6x8(uint8_t x, uint8_t y, const char ch[]);
//void ssd1306_char_f8x16(uint8_t x, uint8_t y,const char ch[]);
//void ssd1306_char_f16x16(uint8_t x, uint8_t y, uint8_t N);
void ssd1306_draw_bmp(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t bitmap[]);
// ----------------------------------------------------------------------------
#endif
void ssd1306_init(void){
	DDRB |= (1 << SSD1306_SDA);	// Set port as output
	DDRB |= (1 << SSD1306_SCL);	// Set port as output

	ssd1306_send_command(0xAE); // display off
	ssd1306_send_command(0x00); // Set Memory Addressing Mode
	ssd1306_send_command(0x10); // 00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
	ssd1306_send_command(0x40); // Set Page Start Address for Page Addressing Mode,0-7
	ssd1306_send_command(0x81); // Set COM Output Scan Direction
	ssd1306_send_command(0xCF); // ---set low column address
	ssd1306_send_command(0xA1); // ---set high column address
	ssd1306_send_command(0xC8); // --set start line address
	ssd1306_send_command(0xA6); // --set contrast control register
	ssd1306_send_command(0xA8);
	ssd1306_send_command(0x3F); // --set segment re-map 0 to 127
	ssd1306_send_command(0xD3); // --set normal display
	ssd1306_send_command(0x00); // --set multiplex ratio(1 to 64)
	ssd1306_send_command(0xD5); // 
	ssd1306_send_command(0x80); // 0xa4,Output follows RAM content;0xa5,Output ignores RAM content
	ssd1306_send_command(0xD9); // -set display offset
	ssd1306_send_command(0xF1); // -not offset
	ssd1306_send_command(0xDA); // --set display clock divide ratio/oscillator frequency
	ssd1306_send_command(0x12); // --set divide ratio
	ssd1306_send_command(0xDB); // --set pre-charge period
	ssd1306_send_command(0x40); // 
	ssd1306_send_command(0x20); // --set com pins hardware configuration
	ssd1306_send_command(0x02);
	ssd1306_send_command(0x8D); // --set vcomh
	ssd1306_send_command(0x14); // 0x20,0.77xVcc
	ssd1306_send_command(0xA4); // --set DC-DC enable
	ssd1306_send_command(0xA6); // 
	ssd1306_send_command(0xAF); // --turn on oled panel 
}

void ssd1306_xfer_start(void){
	DIGITAL_WRITE_HIGH(SSD1306_SCL);	// Set to HIGH
	DIGITAL_WRITE_HIGH(SSD1306_SDA);	// Set to HIGH
	DIGITAL_WRITE_LOW(SSD1306_SDA);		// Set to LOW
	DIGITAL_WRITE_LOW(SSD1306_SCL);		// Set to LOW
}

void ssd1306_xfer_stop(void){
	DIGITAL_WRITE_LOW(SSD1306_SCL);		// Set to LOW
	DIGITAL_WRITE_LOW(SSD1306_SDA);		// Set to LOW
	DIGITAL_WRITE_HIGH(SSD1306_SCL);	// Set to HIGH
	DIGITAL_WRITE_HIGH(SSD1306_SDA);	// Set to HIGH
}

void ssd1306_send_byte(uint8_t byte){
	uint8_t i;
	for(i=0; i<8; i++)
	{
		if((byte << i) & 0x80) 			DIGITAL_WRITE_HIGH(SSD1306_SDA); 		else 			DIGITAL_WRITE_LOW(SSD1306_SDA); 		 		DIGITAL_WRITE_HIGH(SSD1306_SCL); 		DIGITAL_WRITE_LOW(SSD1306_SCL); 	} 	DIGITAL_WRITE_HIGH(SSD1306_SDA); 	DIGITAL_WRITE_HIGH(SSD1306_SCL); 	DIGITAL_WRITE_LOW(SSD1306_SCL); } void ssd1306_send_command(uint8_t command){ 	ssd1306_xfer_start(); 	ssd1306_send_byte(SSD1306_SA);  // Slave address, SA0=0 	ssd1306_send_byte(0x00);	// write command 	ssd1306_send_byte(command); 	ssd1306_xfer_stop(); } void ssd1306_send_data_start(void){ 	ssd1306_xfer_start(); 	ssd1306_send_byte(SSD1306_SA); 	ssd1306_send_byte(0x40);	//write data } void ssd1306_send_data_stop(void){ 	ssd1306_xfer_stop(); } void ssd1306_setpos(uint8_t x, uint8_t y) { 	ssd1306_xfer_start(); 	ssd1306_send_byte(SSD1306_SA);  //Slave address,SA0=0 	ssd1306_send_byte(0x00);	//write command 	ssd1306_send_byte(0xb0+y); 	ssd1306_send_byte(((x&0xf0)>>4)|0x10); // |0x10
	ssd1306_send_byte((x&0x0f)|0x01); // |0x01

	ssd1306_xfer_stop();
}

void ssd1306_fillscreen(uint8_t fill_Data){
	uint8_t m,n;
	for(m=0;m<8;m++)
	{
		ssd1306_send_command(0xb0+m);	//page0-page1
		ssd1306_send_command(0x00);		//low column start address
		ssd1306_send_command(0x10);		//high column start address
		ssd1306_send_data_start();
		for(n=0;n<128;n++) 		{ 			ssd1306_send_byte(fill_Data); 		} 		ssd1306_send_data_stop(); 	} } void ssd1306_char_f6x8(uint8_t x, uint8_t y, const char ch[]){ 	uint8_t c,i,j=0; 	while(ch[j] != '\0') 	{ 		c = ch[j] - 32; 		if(x>126)
		{
			x=0;
			y++;
		}
		ssd1306_setpos(x,y);
		ssd1306_send_data_start();
		for(i=0;i<6;i++)
		{
			ssd1306_send_byte(pgm_read_byte(&ssd1306xled_font6x8[c*6+i]));
		}
		ssd1306_send_data_stop();
		x += 6;
		j++;
	}
}

// Routines to set and clear bits (used in the sleep code)
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

void system_sleep() {
  ssd1306_fillscreen(0x00);
  ssd1306_send_command(0xAE);
  cbi(ADCSRA,ADEN);                    // switch Analog to Digitalconverter OFF
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
  sleep_enable();
  sleep_mode();                        // System actually sleeps here
  sleep_disable();                     // System continues execution here when watchdog timed out 
  sbi(ADCSRA,ADEN);                    // switch Analog to Digitalconverter ON  
  ssd1306_send_command(0xAF);
}

References:

Attiny85 Canon DSLR IR Remote

A nice use for the Attiny85 — a canon DSLR infrared remote.

Canon keychain IR remote

Parts required

  • Attiny85 microcontroller
  • NPN transistor
  • 3.3 or 5 V voltage regulator
  • Pushbutton
  • Infrared led
  • 12v car key/alarm battery
  • Some copper clad board

Design the PCB

pcb layout

 

Tweak the design for better looks in Illustrator (Download printable PDF)tweaked pcb design

Print and transfer the pcb design to the board (laser printer/laminator method)

PCB layout transferred to copper clad board

Trim the board with a dremel and etch with ferric chloride and some 3D printer assistance

Gcode for rocking the print bed

G21 ; set units to millimeters
M107
M92 X62.6054 ; calibrate X
M92 Y61.0152 ; calibrate Y
M92 Z2387.0719 ; calibrate Z
G90 ; use absolute coordinates
G1 Y100
G1 Y110
G1 Y100
G1 Y110
G1 Y100
G1 Y110
G1 Y100
G1 Y110
.... just copy-pase the last 2 lines to extend print time

Etched pcb

Wipe off the toner with acetone, tin and drill the holes using .8mm carbide drill

PCB with drilled holes

Solder the parts and add battery contacts made from a paperclip

Assembled remote

Program the attiny85 with the following code (16 15us pulses, pause for 7330us and do 16 15us pulses again)
I am ussing the assembler port calls to trigger the pins because digitalWrite() function is too slow

void setup() {
pinMode(0, OUTPUT);
}

void loop() {
int del = 15;
int del2 = 7330;
for (int i =0; i<16; i++){
PORTB |= _BV(PORTB0);
delayMicroseconds(del);
PORTB &= ~_BV(PORTB0);
delayMicroseconds(del);
}
delayMicroseconds(del2);
for (int i =0; i<16; i++){
PORTB |= _BV(PORTB0);
delayMicroseconds(del);
PORTB &= ~_BV(PORTB0);
delayMicroseconds(del);
}
delay(100);
}

Model and 3D print the keychain case (Download printable file from Thingiverse)

model

 

References

Programming the Attiny85 http://highlowtech.org/?p=1706

Timing for the trigger signal http://www.doc-diy.net/photo/rc-1_hacked/index.php