OpenLedRace is a game that uses an addressable LED strip as a track where cars are represented as lights.

This is a simple 1D Led race game which is known as the open LED race. So, OpenLedRace is a game that uses an addressable LED strip as a track where cars are represented as lights.

It is created by Gerardo Barbarov Rostan and is an open-source project, so the code is constantly improving.

This time I present you a compact version of this game so that the track is linear and contains only 60 LEDs. The advantage of this design is that the device is portable and everything is put into operation in a few seconds.

The device is very simple and easy to make and consist of only a few components:

- Arduino Nano microcontroller

- WS2812 Led strip with 60 LEDs

- 2 buttons

- Buzzer

- Transistor

- and two resistors

In the basic game mode, where the speed of the car is proportional to the pulsations of the control button, and in the ascent, ramps must be pressed more quickly to compensate for the effect of simulated gravity. One car is presented in RED and the other in GREEN. In the beginning, each car is represented by two LEDs, and each complete turn is increased by one led. An interesting effect is an inertia. After we stop pressing the button, the car continues to move for some time. The speed of movement of the cars as a function of the frequency of pressing the button can be easily adjusted in the code. Other interesting effects are the addition of sound that simulates the engine and the simulation of the ascent on the road. Effects are activated by holding down both keys while the device is turned on. At the end of the race, the first few LEDs glow with the color of the winning car.

Finally, the assembly is installed in a suitable box made of PVC material and coated with a self-adhesive label.

Untitled Sketch_bb.jpg
        /*  
 * ____                     _      ______ _____    _____
  / __ \                   | |    |  ____|  __ \  |  __ \               
 | |  | |_ __   ___ _ __   | |    | |__  | |  | | | |__) |__ _  ___ ___ 
 | |  | | '_ \ / _ \ '_ \  | |    |  __| | |  | | |  _  // _` |/ __/ _ \
 | |__| | |_) |  __/ | | | | |____| |____| |__| | | | \ \ (_| | (_|  __/
  \____/| .__/ \___|_| |_| |______|______|_____/  |_|  \_\__,_|\___\___|
        | |                                                             
        |_|          
 Open LED Race
 An minimalist cars race for LED strip  
  
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 3 of the License, or
 (at your option) any later version.

 
 by [email protected]  for Arduino day Seville 2019 
 https://www.hackster.io/gbarbarov/open-led-race-a0331a
 https://twitter.com/openledrace
 
 
 https://gitlab.com/open-led-race
 https://openledrace.net/open-software/
*/


//version Basic for PCB Rome Edition 
// 2 Player , without Boxes Track

char const softwareId[] = "A2P0";  // A2P0: "A"=OpenLEDRace Team, "2P0"=Game ID (2P=2 Players, 0=Type 0 w slope w/ box)
char const version[] = "1.0.0";

                                                            
#include <Adafruit_NeoPixel.h>
#define MAXLED         60 // MAX LEDs actives on strip

#define PIN_LED        2  // R 500 ohms to DI pin for WS2812 and WS2813, for WS2813 BI pin of first LED to GND  ,  CAP 1000 uF to VCC 5v/GND,power supplie 5V 2A  
#define PIN_P1         3   // switch player 1 to PIN and GND
#define PIN_P2         4   // switch player 2 to PIN and GND 
#define PIN_AUDIO      9   // through CAP 2uf to speaker 8 ohms


#define INI_RAMP 80
#define MED_RAMP 90
#define END_RAMP 100
#define HIGH_RAMP 16
bool ENABLE_RAMP=0;
bool VIEW_RAMP=0;

int NPIXELS=MAXLED; // leds on track
int cont_print=0;

#define COLOR1   Color(255,0,0)
#define COLOR2   Color(0,255,0)

#define COLOR1_tail   Color(i*3,0,0)
#define COLOR2_tail   Color(0,i*3,0)

// Serial Communications
#define EOL                 '\n' // End of Command char used in Protocol

#define REC_COMMAND_BUFLEN  32
char cmd[REC_COMMAND_BUFLEN];    // Stores command received by ReadSerialComand()

#define TX_COMMAND_BUFLEN  64
char txbuff[TX_COMMAND_BUFLEN];  // to prepare command strings to send 



int win_music[] = {
  2637, 2637, 0, 2637, 
  0, 2093, 2637, 0,
  3136    
};
      
byte  gravity_map[MAXLED];     

int TBEEP=0;
int FBEEP=0; 
byte SMOTOR=0;
float speed1=0;
float speed2=0;
float dist1=0;
float dist2=0;

byte loop1=0;
byte loop2=0;

byte leader=0;
byte loop_max=5; //total laps race


float ACEL=0.02;
float kf=0.015; //friction constant
float kg=0.003; //gravity constant

byte flag_sw1=0;
byte flag_sw2=0;
byte draworder=0;
 
unsigned long timestamp=0;

Adafruit_NeoPixel track = Adafruit_NeoPixel(MAXLED, PIN_LED, NEO_GRB + NEO_KHZ800);

int tdelay = 5; 

void set_ramp(byte H,byte a,byte b,byte c)
{for(int i=0;i<(b-a);i++){gravity_map[a+i]=127-i*((float)H/(b-a));};
 gravity_map[b]=127; 
 for(int i=0;i<(c-b);i++){gravity_map[b+i+1]=127+H-i*((float)H/(c-b));};
}

void set_loop(byte H,byte a,byte b,byte c)
{for(int i=0;i<(b-a);i++){gravity_map[a+i]=127-i*((float)H/(b-a));};
 gravity_map[b]=255; 
 for(int i=0;i<(c-b);i++){gravity_map[b+i+1]=127+H-i*((float)H/(c-b));};
}


void setup() {
  Serial.begin(115200);
  for(int i=0;i<NPIXELS;i++){gravity_map[i]=127;};
  track.begin(); 
  pinMode(PIN_P1,INPUT_PULLUP); 
  pinMode(PIN_P2,INPUT_PULLUP);  

  if ((digitalRead(PIN_P1)==0)) //push switch 1 on reset for activate physics
  {ENABLE_RAMP=1;
   set_ramp(HIGH_RAMP,INI_RAMP,MED_RAMP,END_RAMP); 
   for(int i=0;i<(MED_RAMP-INI_RAMP);i++){track.setPixelColor(INI_RAMP+i, track.Color(24+i*4,0,24+i*4) );};  
   for(int i=0;i<(END_RAMP-MED_RAMP);i++){track.setPixelColor(END_RAMP-i, track.Color(24+i*4,0,24+i*4) );};                                           
   track.show();
   delay(1000);
   tone(PIN_AUDIO,500);delay(500);noTone(PIN_AUDIO);delay(500);
   if ((digitalRead(PIN_P1)==0)) {VIEW_RAMP=1;} // if retain push switch 1 set view ramp
   else {for(int i=0;i<NPIXELS;i++){track.setPixelColor(i, track.Color(0,0,0));};
         track.show();
         VIEW_RAMP=0;  
        }; 
   
  };

  if ((digitalRead(PIN_P2)==0)) {delay(1000); tone(PIN_AUDIO,1000);delay(500);noTone(PIN_AUDIO);delay(500);if ((digitalRead(PIN_P2)==1)) SMOTOR=1;} //push switch 2 until a tone beep on reset for activate magic FX  ;-)
  
  start_race();    
}

void start_race(){send_race_phase(4); // Race phase 4: Countdown 
                  for(int i=0;i<NPIXELS;i++){track.setPixelColor(i, track.Color(0,0,0));};
                  track.show();
                  delay(5000);
                  track.setPixelColor(12, track.Color(0,255,0));
                  track.setPixelColor(11, track.Color(0,255,0));
                  track.show();
                  tone(PIN_AUDIO,400);
                  delay(2000);
                  noTone(PIN_AUDIO);                  
                  track.setPixelColor(12, track.Color(0,0,0));
                  track.setPixelColor(11, track.Color(0,0,0));
                  track.setPixelColor(10, track.Color(255,255,0));
                  track.setPixelColor(9, track.Color(255,255,0));
                  track.show();
                  tone(PIN_AUDIO,600);
                  delay(2000);
                  noTone(PIN_AUDIO);                  
                  track.setPixelColor(9, track.Color(0,0,0));
                  track.setPixelColor(10, track.Color(0,0,0));
                  track.setPixelColor(8, track.Color(255,0,0));
                  track.setPixelColor(7, track.Color(255,0,0));
                  track.show();
                  tone(PIN_AUDIO,1200);
                  delay(2000);
                  noTone(PIN_AUDIO);                               
                  timestamp=0; 
                  send_race_phase(5); // Race phase 4: Race Started 
                 };

void winner_fx(byte w) {
               int msize = sizeof(win_music) / sizeof(int);
               for (int note = 0; note < msize; note++) {
               if (SMOTOR==1) {tone(PIN_AUDIO, win_music[note]/(3-w),200);} else {tone(PIN_AUDIO, win_music[note],200);};
               delay(230);
               noTone(PIN_AUDIO);}                                             
              };


int get_relative_position1( void ) {
    enum{
      MIN_RPOS = 0,
      MAX_RPOS = 99,
    };    
    int trackdist = 0;
    int pos = 0;  
    trackdist = (int)dist1 % NPIXELS;
    pos = map(trackdist, 0, NPIXELS -1, MIN_RPOS, MAX_RPOS);    
    return pos;
}

int get_relative_position2( void ) {
    enum{
      MIN_RPOS = 0,
      MAX_RPOS = 99,
    };    
    int trackdist = 0;
    int pos = 0;  
    trackdist = (int)dist2 % NPIXELS;
    pos = map(trackdist, 0, NPIXELS -1, MIN_RPOS, MAX_RPOS);    
    return pos;
}

void print_cars_position( void ) {
 int  rpos = get_relative_position1();
 sprintf( txbuff, "p%d%d%d,%d%c", 1, 1, loop1, rpos, EOL );
  sendSerialCommand(txbuff);
 
 rpos = get_relative_position2();
 sprintf( txbuff, "p%d%d%d,%d%c", 2, 1, loop2, rpos, EOL );
 sendSerialCommand(txbuff);
 
}


void burning1(){
//to do
 }

void burning2(){
//to do
 }

void track_rain_fx(){
//to do
 }

void track_oil_fx(){
//to do
 }

void track_snow_fx(){
//to do
 }


void fuel_empty(){
//to do
 }

void fill_fuel_fx(){
//to do
 }

void in_track_boxs_fx(){
//to do
 }

void pause_track_boxs_fx(){
//to do
 }
 
void flag_boxs_stop(){
//to do
 }

void flag_boxs_ready(){
//to do
 }

void draw_safety_car(){
//to do
 }

void telemetry_rx(){
  //to do
 }
 
void telemetry_tx(){
  //to do
 }

void telemetry_lap_time_car1(){
//to do
 }

void telemetry_lap_time_car2(){
//to do
 }

void telemetry_record_lap(){
//to do
 }

void telemetry_total_time(){
//to do
 }

int read_sensor(byte player){
  return(0); //to do
}

int calibration_sensor(byte player){
  return(0); //to do
}

int display_lcd_laps(){
    return(0); //to do  
}

int display_lcd_time(){
    return(0); //to do  
}

void draw_car1(void){for(int i=0;i<=loop1;i++){track.setPixelColor(((word)dist1 % NPIXELS)+i, track.COLOR1);};                   
 }

void draw_car2(void){for(int i=0;i<=loop2;i++){track.setPixelColor(((word)dist2 % NPIXELS)+i, track.COLOR2);};            
 }
  
void loop() {   

    // look for commands received on serial 
    checkSerialCommand();

  
    for(int i=0;i<NPIXELS;i++){track.setPixelColor(i, track.Color(0,0,0));};
    if ((ENABLE_RAMP==1) && ( VIEW_RAMP==1)) {for(int i=0;i<(MED_RAMP-INI_RAMP);i++){track.setPixelColor(INI_RAMP+i, track.Color(24+i*4,0,24+i*4) );};  
                                              for(int i=0;i<(END_RAMP-MED_RAMP);i++){track.setPixelColor(END_RAMP-i, track.Color(24+i*4,0,24+i*4) );};                                           
                                             };    
    
    if ( (flag_sw1==1) && (digitalRead(PIN_P1)==0) ) {flag_sw1=0;speed1+=ACEL;};
    if ( (flag_sw1==0) && (digitalRead(PIN_P1)==1) ) {flag_sw1=1;};

    if ((gravity_map[(word)dist1 % NPIXELS])<127) speed1-=kg*(127-(gravity_map[(word)dist1 % NPIXELS]));
    if ((gravity_map[(word)dist1 % NPIXELS])>127) speed1+=kg*((gravity_map[(word)dist1 % NPIXELS])-127);
        
    speed1-=speed1*kf; 
    
    if ( (flag_sw2==1) && (digitalRead(PIN_P2)==0) ) {flag_sw2=0;speed2+=ACEL;};
    if ( (flag_sw2==0) && (digitalRead(PIN_P2)==1) ) {flag_sw2=1;};

    if ((gravity_map[(word)dist2 % NPIXELS])<127) speed2-=kg*(127-(gravity_map[(word)dist2 % NPIXELS]));
    if ((gravity_map[(word)dist2 % NPIXELS])>127) speed2+=kg*((gravity_map[(word)dist2 % NPIXELS])-127);
        
    speed2-=speed2*kf; 
        
    dist1+=speed1;
    dist2+=speed2;

    if (dist1>dist2) {if (leader==2) {FBEEP=440;TBEEP=10;}
                      leader=1;} 
    if (dist2>dist1) {if (leader==1) {FBEEP=440*2;TBEEP=10;}
                      leader=2;};

    
      
    if (dist1>NPIXELS*loop1) {loop1++;TBEEP=10;FBEEP=440;};
    if (dist2>NPIXELS*loop2) {loop2++;TBEEP=10;FBEEP=440*2;};

    if (loop1>loop_max) {sprintf( txbuff, "w1%c", EOL );
                         sendSerialCommand(txbuff); // Send Winner=1 command
                         for(int i=0;i<NPIXELS/10;i++){track.setPixelColor(i,track.COLOR1_tail);}; track.show();
                         winner_fx(1);loop1=0;loop2=0;dist1=0;dist2=0;speed1=0;speed2=0;timestamp=0;
                         start_race();
                        }
    if (loop2>loop_max) {sprintf( txbuff, "w2%c", EOL );
                         sendSerialCommand(txbuff); // Send Winner=2 command
                         for(int i=0;i<NPIXELS/10;i++){track.setPixelColor(i, track.COLOR2_tail);}; track.show();
                         winner_fx(2);loop1=0;loop2=0;dist1=0;dist2=0;speed1=0;speed2=0;timestamp=0;
                         start_race();
                        }
   
     if ((millis() & 512)==(512*draworder)) {if (draworder==0) {draworder=1;}
                          else {draworder=0;}
                         };
    if (abs(round(speed1*100))>abs(round(speed2*100))) {draworder=1;};
    if (abs(round(speed2*100))>abs(round(speed1*100))) {draworder=0;};
       
    if ( draworder==0 ) {
      draw_car1();
      draw_car2();
    }
    else {
      draw_car2();
      draw_car1();
    } 
                 
    track.show(); 
    if (SMOTOR==1) tone(PIN_AUDIO,FBEEP+int(speed1*440*2)+int(speed2*440*3));
    delay(tdelay);
    if (TBEEP>0) {TBEEP--;} else {FBEEP=0;};
    cont_print++; 
    if (cont_print>100) {print_cars_position();cont_print=0;}
         
}





/*
 * 
 */

void checkSerialCommand(){
  
  int clen = checkSerial(cmd);
  if(clen == 0) return ;       // No commands received
  if(clen  < 0) {              // Error receiving command  
    sprintf( txbuff, "!1Error reading serial command:[%d]",clen); // Send a warning to host
    sendSerialCommand(txbuff);
    return;
  }
  // clen > 0 ---> Command with length=clen ready in  cmd[]

  switch (cmd[0]) {
    case '#':                         // Handshake -> send back 
      {
        sprintf( txbuff, "#%c", EOL );
        sendSerialCommand(txbuff);    
      }
      return;

  case  '@' :                         // Enter "Configuration Mode"
    {
      // send back @OK 
      // No real cfg mode here, but send @OK so the Desktop app (Upload, configure)
      // can send a GET SOFTWARE Type/Ver command and identify this software
        sprintf( txbuff, "@OK%c", EOL );
        sendSerialCommand(txbuff);          
    }
    return;
    
  case '?' :                          // Get Software Id
    {
      sprintf( txbuff, "%s%s%c", "?", softwareId, EOL );
      sendSerialCommand(txbuff);    
    }
    return;
  
  case '%' :                          // Get Software Version
    {
      sprintf( txbuff, "%s%s%c", "%", version, EOL );
      sendSerialCommand(txbuff);    
    }
    return;    
  } 

  // if we get here, the command it's not managed by this software -> Answer <CommandId>NOK
  sprintf(txbuff, "%cNOK%c", cmd[0], EOL );  
  sendSerialCommand(txbuff);
  
  return; 
}


/*
 * 
 */
void send_race_phase( int phase ) {
  sprintf(txbuff, "R%d%c",phase,EOL);
  sendSerialCommand(txbuff);
}


// Command Send/Receive base functions 
///////////////////////////////////////

// vars used only in these functions
Stream*  _stream = &Serial;
int      _bufIdx;

/*  Non blocking: call these function in main loop()
 *   
 *  If there are bytes available in Serial, READ JUST ONE char 
 *  and ADD it to the 'internal' cmd buffer 
 *    The char just read is an END OF COMMAND char ?
 *      N: Return 0 (no complete command available yet) 
 *      Y: Return buffer length (the caller will find the command in [buf]
 */
int checkSerial(char *   buf) { 
  while (_stream->available()) {
    if(_bufIdx < REC_COMMAND_BUFLEN - 2) {
      char data = _stream->read();
      if(data == EOL) {
        int cmsSize=_bufIdx;
        buf[_bufIdx++] = '\0';
        _bufIdx=0;
        return(cmsSize);
      } else {
        buf[_bufIdx++] = data;
      }
    } else {
      // buffer full 
      // reset and retunn error
      buf[_bufIdx++] = '\0';
      _bufIdx=0;
      return(-2); 
    }
  }
  return(0);
}

/*
 *
 */
void sendSerialCommand(char* str) {
  // get command length
  int dlen=0;
  for(; dlen<TX_COMMAND_BUFLEN; dlen++ ) { 
    if(*(str+dlen) == EOL ) {
      dlen++; // send EOC 
      break;
    }
  }
  _stream->write(str, dlen);
  return;
}
    
Mirko Pavleski
Electronics , Arduino , Physics

Categories