Laser Tag S09 – IR

Love Jesus, Love Code

This tutorial will go for about 1 hr.

In this tutorial, we will:

  1. Infra Red protocol
  2. Shot Packet
  3. Infrared Send signal
  4. Infrared Receive signal
  5. Scoring

IR Protocol

Milestag guns use the Sony infrared protocol with the only exception: The Sony signal usually repeats itself a minimum of 3 times, while the Milestag shot signal is only sent once per shot. This will be important to remember, as the IR library has the Sony protocol built in.

The protocol requires a 2400uS pulse header with 600uS gaps. 0 values are represented by 600uS pulses while 1 values are represented by a 1200uS pulse.

Diagram from the MilesTag CORE Operation Manual Firmware Version 5.6X

The binary signal does not mean much unless you can encode it and decode it using a key. The key is found in the MilesTag CORE Operations Manual as shown below.

Shot packets always have 0 as the first bit. The next 7 bits include the PlayerID, the next 2 bits are for the Team ID, the next 4 bits are for the damage, and the last bit is for the zone. This is a 15 bit signal. It is important to keep this

For example, If the Player ID was 5, the team ID was 1 and the damage was 50, and the zone was 1, we need the gun to automatically work out the binary for that.

The damage is a mapped value as shown below from the MilesTag 2 Protocol by Christopher Malton April 2011.

A value of 50 damage is represented by 0x0D in hexadecimal, which equals 1101 in binary. You can google “0x0D in binary” and you will receive the answer 0b1101. The 0b in front identifies the binary number, just like 0x identifies the number as being hexadecimal.

The player id of 5 is equal to 0000101 (needs to be 7 bits)

The team ID is equal to 01 (needs to be 2 bits)

The zone is equal to 1

The final binary code should be

000001010111011

Coding shot packet

We are going to use the << (left shift) binary command to move the different components of the shot packet into the correct position for the binary transmission.

  1. At the end of the MilesTagMapping.ino tab, add the following code:
void tagCode() { // calculates out what the player's tagger code. 
  shotCode = fieldID; // will be either 1 or 0 //zone is bit 0
  shotCode += myGunDamage << 1; // bits 1-4 are the damage dealt by the gun. Shuffle up 1 position 
  shotCode += teamID << 5; // bits 5-6 are the team ID. Shuffle up 5 positions 
  shotCode += playerID <<7; // bits 7-14 is the player ID. Shuffle up 7 positions 
  debugInfo += "shotCode = " + String(shotCode) + "in decimal, OR " + String(shotCode, BIN) + " in Binary";
}

The value of shotCode is stored in the computer as a decimal number. You will notice in the code above, you can easily change between hexadecimal, decimal and binary using the String() function. BIN = binary.

2. In the main program tab, at the end of the section // Current Data on Player, add the following integer declaration:

int shotCode = 0;

3. In the Game.ino tab, in the startGame() routine, add the following code:

tagCode();

Shooting the shot packet using IR

To make the shot using IR we will use the IRremoteESP8266 Library. The Library version 2.7.8 has historically worked better for Lasertag guns than the newer versions.

UNTESTED: This may be because after version 2.7.8 the following line in the code in the IRrecvDumpV3.ino file has been added. Making the value greater than 12 may help.

// NOTE: Set this value very high to effectively turn off UNKNOWN detection.
const uint16_t kMinUnknownSize = 12;

This may change over coming updates.

  1. Install IRremoteESP8266 using the library manager
  2. Set the pins for the IR receiver and IR transmitter. Go to the main program tab, under the // pin declaration section add the following code:
int IRtransmitPin = 13; // D7 - Primary fire mode IR transmitter pin
int IRreceivePin = 14; // D5 - The pin used for incoming IR signals 

Note: For the IRtransmitPin you can use pins 2,4,7,8,12 or 13 but not PWM pins. See http://j44industries.blogspot.com/2009/09/arduino-frequency-generation.html for more information.

  1. Add library files and allocate pins. Go to the main program tab, look for where the section where other libraries are being called and add the following code:
//Code for IRremoteESP8266
#include <Arduino.h>
#include <IRremoteESP8266.h>  // use library 2.7.8 (the latest version has a lot more interference.
#include <IRrecv.h>
#include <IRutils.h>
IRrecv irrecv(IRreceivePin);
decode_results results;
#include <IRsend.h>
IRsend irsend(IRtransmitPin); 

//Incoming signal Details
int receivedBit[400]; // IR data received, fill array that can be filled
int receivedByte[50]; // Used to store the data stream in bytes for easy HEX codes in MilesTagMessages 
int receivedLength = 0; // stores the received signal length
int check = 0; // Variable used in parity checking 

// Received data
const int maxHits = 512; // Number of signals to be recorded: Allows for the game data to be reviewed after the game
int hitNo = 0; // Hit number
int hit[maxHits][3]; // Multi dimensional array, [*][0] Player, [*][1] team, [*][2] damage, * = hit number

You may have noticed the code int hit[maxHits][3]; This is what is known as a multi-dimensional array. Visually it can be represented like a spreadsheet where the first number is the hit number, and the second number identifies if it is the player, team or damage being referred to. The numbers in the brackets [][] identify the coordinates in the spreadsheet – if you were to think about it visually.

  1. In the main program tab, go to the setup() routine, and add the following code to the //setup pins section.
  digitalWrite(IRtransmitPin, LOW); //start on Low

This is done to ensure the LED light does not turn on for an extended period of time. Ideally, we want the IR transmitter to run at a higher current using pulses that could potentially damage the LED if ran constantly. This will be more important if a booster circuit is added to the IR transmitter.

  1. In the main program tab, go to the setup() routine, and add the following code at the end of the routine, but still in the routine.
//Start the IR Receiver
irrecv.enableIRIn();  

// Start the IR Sender
irsend.begin();
  1. Send the shot code over IR. Go to the Shoot.ino tab, replace the shoot() routine with:
void shoot(){
  //flash LED strip on
    strip.begin();
    setAll(0xFF,0xFF,0xFF);

  //send out shot pulse over IR
  #if SEND_GLOBALCACHE
    irsend.sendSony(shotCode, 15, 0); // sends a 15 bit pulse, the shotCode is in decimal, the last digit creats a single shot
    debugInfo += "shotCode: " + String(shotCode, BIN) + " sent <br>";
  #else   // SEND_GLOBALCACHE
    debugInfo += "shotCode: " + String(shotCode, BIN) + " not sent because SEND_GLOBALCACHE not avaliable <br>"; 
  #endif  

  // return LED strip back to 
  strip.show(); // Initialize all pixels to 'off'
  teamColour(); // change back to team colour

  // play shoot sound
  switch (soundSet){ 
    case 0: mp3.playFileByIndexNumber(1); break; // military shot
    case 1: mp3.playFileByIndexNumber(21); break; // si-fi shot
  }  

  shotsLeft = shotsLeft - 1;
  shotDelayTime = millis();
}  

Note: we just changed the section under the heading //send out shot pulse over IR. You wil notice we use irsend.sendSony(shotCode, 15, 0); since we are using the Sony protocol. The only difference is that we do not want to repeat the signal. To stop any repeats, The last digit in the command needs to be 0. We also specify that the code is15 bits long and the shotCode which holds the data we are sending.

7. Compile and check your debug web page.

Note: you should now see your shot code being displayed. It will not print the leading 0’s.

Receiving the IR shot packet

This section will first get the shot-packet, and then we will need to encode it, and then apply any damage to the gun (player).

  1. Create a new tab called IR. This tab will contain code that responds to any IR signal received, and put in the following subroutines recieveIR(), toBinaryArray(), Interpret Recieved(), shotPackage():
void receiveIR() { //  checks for an incoming signal and decodes it if it sees one.
// use the library
 if (irrecv.decode(&results)) {
    
    if (results.decode_type == SONY){
      debugInfo += "SONY Code Detected = " + String(results.value, BIN) + " Bits = "+ String(results.bits)+ "<br>";
      toBinaryArray(results.value, results.bits); //fill array
      interpretReceived(results.value, results.bits);
    }
    else{
      debugInfo += "UNKNOWN Code Detected = " + String(results.value, BIN) + " Bits = "+ String(results.bits)+ "<br>";
      irrecv.enableIRIn(); //reset IR
    }
  irrecv.resume();
  }
}

recieveIR() relies on the remoteIR library to decode the received signal. The lasertag signals are in the SONY format so we can ignore other signals. It is important that once the signal is received that the IR sensor is resumed so it can receive further data by using the irrecv.resume(); command. Sometimes the library may glitch, so when UNKNOWN code is detected we also reset the IR receiver.

void toBinaryArray(int receivedData, int recievedLength){  //populate receivedBit array
  std::fill(std::begin(receivedBit), std::end(receivedBit), 0); //fills array with 0 values
  for (int i = recievedLength-1;  i >= 0; i--){
    if ( receivedData & (1 << i)){
      receivedBit[i] = 1;
      debugInfo += "1";
    }
    else{
      receivedBit[i] = 0;
      debugInfo += "0";
    }
  }
  debugInfo += " stored in receivedBit array <br>";
}

toBinaryArray(int, int) converts the signal received by the gun to a binary array. This allows us to easily manipulate the data received by the gun. It is important that any leading 0’s are included in the array, and this is why the receivedLength variable is needed. The “Binary Shift Left” command is used to help fill the array. See https://www.tutorialspoint.com/arduino/arduino_bitwise_operators.htm for more information.

void interpretReceived(int receivedData, int recievedLength){
  // check bit in leftmost position to see if a shot packet (0) 
  if (!(receivedData & receivedLength==15 & (1 << (14)))){ //shot packet received  
    debugInfo += "shot packet received: ";
    shotPackage();
  }
}

void interpretReceived(int, int) determines the type of package received. In MilesTag there are a number of different data packets that can be received, such as shot packets, clone signals, and signals used from resporn points and other game objects. The if statement determines if it is a valid shot packet before processing it as a shot package. In this section we will eventually include if statements for the clone signal and game objects.

void shotPackage(){
if(hitNo == maxHits){hitNo = 0;} // hitNo sorts out where the data should be stored if statement means old data gets overwritten if too much is received
hitNo++;
hit[hitNo][0] = 0; hit[hitNo][1] = 0; hit[hitNo][2] = 0; // set hit data to 0. 0 = player, 1 = team, 2 = damage

hit[hitNo][0] += receivedBit[13] << 6;
hit[hitNo][0] += receivedBit[12] << 5;
hit[hitNo][0] += receivedBit[11] << 4;
hit[hitNo][0] += receivedBit[10] << 3;
hit[hitNo][0] += receivedBit[9] << 2;
hit[hitNo][0] += receivedBit[8] << 1;
hit[hitNo][0] += receivedBit[7] << 0;

hit[hitNo][1] += receivedBit[6] << 1;
hit[hitNo][1] += receivedBit[5] << 0;

hit[hitNo][2] += receivedBit[4] << 3;
hit[hitNo][2] += receivedBit[3] << 2;
hit[hitNo][2] += receivedBit[2] << 1;
hit[hitNo][2] += receivedBit[1] << 0;

debugInfo += "HIT by " + playerName(hit[hitNo][0]) + "| TEAM: " + teamName(hit[hitNo][1]) + "| DAMAGE:" + String(damageMapped(hit[hitNo][2])) + "<br>";
}

void shotPackage() processes the shot package and adds it into a multi-dimensional array which will store all shot data. More information about manipulating binary numbers using c++ can be found at https://hackernoon.com/bit-manipulation-in-c-and-c-1cs2bux

  1. Put receiveIR(); in the main program loop. This is found in your main program tab. You will already have some subroutines called in the loop() already.
void loop() {
   AsyncElegantOTA.loop();
   gameLoop();
   receiveIR();
}
  1. Modify IRrecv.h so that kRawBuf = 1000;
    • Go to your directory where your Arduino library files are stored. This is most likely in the directory: \Documents\Arduino\libraries\IRremoteESP8266\src
    • Open IRrecv.h in a text editor such as notepad++
    • Search for kRawBuf, it should be around line 25 and change the value from 100 to 1000, and save the file.
const uint16_t kRawBuf = 1000;  // Default length of raw capture buffer

Responding to being hit

When you are hit, we need the lights on the gun to flash, Sound on the gun to play damage sound and the health to be reduced. We will use a subroutine called hit() to make these changes.

  1. In the IR.ino tab, at the end of the shotPackage() subroutine, add the following call:
if (gameState == 1){hitReceived();}  // only process hits if player in game and alive
  1. After the shotPackage() subroutine, add the following code:
void hitReceived() { // Make some noise and flash some lights when you get shot
hitFlash();
health = health - damageMapped(hit[hitNo][2]);
mp3.playFileByIndexNumber(6); // damage sound
if(health <= 0){canFire = false; dead();}
hitFlash();
}

You will notice that if health is less than or equal to zero, then you are dead, and we run the dead() subroutine.

  1. Add in the dead sound when dead() is called by going to Game.ino, and make the void dead() subroutine look like:
void dead(){ 
  displayUpdate();
  mp3.playFileByIndexNumber(7); // Dead sound
  gameState = 3;
  deadTime = millis(); //start the dead time
  Serial.println ("game state = 3");
  debugInfo += "Game State = 3 (Dead) <br>"; 
}
  1. Test your code.

Displaying who shot you

Once a hit is made the person who shot you will be displayed to your screen, with the amount of damage and the team name.

  1. Got o the Display.ino tab.
  2. Update the displayUpdate() subroutine with:
void displayUpdate(){
  // Clear the buffer.
  display.setRotation(0);
  display.clearDisplay();
  //display data to screen
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.setTextSize(2);
  display.print(String(health));
  display.write(3);
  display.print(String(magazines));
  display.write(15);
  display.print(String(shotsLeft));
  display.setCursor(0,16);
  display.setTextSize(1);
  if (hitNo>0){
    display.setTextSize(2);
    display.write(1);
    display.println(" " + playerName(hit[hitNo][0]));
    display.write(3);
    display.print(" -" + String(damageMapped(hit[hitNo][2])) + " " + teamName(hit[hitNo][1]).substring(0,3));  
  }
  else{
    display.setTextSize(1);
    display.println(WiFi.localIP());
  }
  //Code to out zero in for seconds less than ten on display
  display.setTextSize(2);
  display.setCursor(12,48);
  if ((timeLeft % 60) < 10) display.println(""+ String(timeLeft / 60) +":0"+ timeLeft % 60);
  else display.println(""+ String(timeLeft / 60) +":"+ timeLeft % 60);
    
  display.drawTriangle(0, 48, 10, 48, 5, 55, WHITE);
  display.drawTriangle(0, 62, 10, 62, 5, 55, WHITE);
  display.setTextSize(1);
  display.fillRect(114,48,128,56,BLACK);
  display.setCursor(80,48);
  display.println(weaponName(fireSelect));
  display.setCursor(80,56);
  display.println(damageMapped(myGunDamage));
  display.setCursor(16,0);
  display.setRotation(1);
  display.println(playerName(playerID));
  display.display();
}

The code we added was the if/else statement. If there are more than 0 hits, it will remove the IP address and display who hit you, the damage done and what team they are from.

substring(0,3) returns the first 3 characters of the string. This is used on the display otherwise colours like green and yellow end up on the following line.

  1. Test your code and upload to gun to see if it is working.

Displaying score at the end

  1. First we need to populate the variables that will record an ongoing tally of all the hits and kills a particular player does. We could use another multi-dimensioanl array, but in this case we will just use standard single dimenensional arrays. Add the following subroutine to the end of the IR.ino tab:
void scoreUpdate(){
  damageID [hit[hitNo][0]] += damageMapped(hit[hitNo][2]);
  if (health <=0){ killID [hit[hitNo][0]] += 1; deathCount +=1;}
  enemyTeamID [hit[hitNo][0]] = hit[hitNo][1];
}
  1. Go to the IR.ino subroutine and the void hitReceived() subroutine. After the health is updated place the following line of code to call the scoreUpdate routine:
scoreUpdate();
  1. Add the following code to the end of the Display.ino tab. It should replace the existing void displayGameOver() subroutine.
void displayGameOver(){
  int topKiller[6] = {100,100,100,100,100,100}; // based on damage dealt - allow for overflow when shuffling. PlayerID set to 100
    
  display.setRotation(0);
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(1);
  display.println(playerName(playerID) + " killed " + String(deathCount) + " times");
  display.setTextSize(1);
  display.println("DAMAGE REPORT:");
  
  //Find top 5 killers based on damage
  for (int i=0; i<99; i++){ // shuffle through all player IDs i= playerID
      for (int j=0; j<5; j++) {
        int pos = 0;
        if (damageID [i] > damageID[topKiller[j]]){ // check if the damage is greater than someone else in the list
            pos = j;
          while (j<5){ //shuffle all killers down list by 1 from where they are greater
            topKiller[5-j] = topKiller[4-j];
            j++;
          }
          topKiller[pos] = i; // put the killer into the list at the correct position.
        }
        exit; // exit out of for loop once player inserted into list
      }  
  }
    
   for (int i=0; i<5; i++){
    if (topKiller[i] != 100){ // only display players in the game. PlayerID 100 means not used 
      display.println (playerName(topKiller[i]) + " " + String(teamName(enemyTeamID [topKiller[i]])) + " " + String(damageID[topKiller[i]]) + "D " + String(killID[topKiller[i]]) + "K");
    }
  } 
  display.display();
}
  1. Test your code.

Stop gun shooting itself

You will notice that when you fire the gun, it should now inflict damage on itself. This is because the IR sensor easily picks up the IR senders signal. The good thing is that this shows that the Gun is able to multi-task, it can recieve a signal and send a signal at the same time. The only issue is when it receives 2 signals at the same time. So it is good if you can make your gun in a way that it does not recieve it’s own shoot signal or interference will occur from incoming shoot signals from other guns.

In the IR.ino tab, at the end of the shotPackage() subroutine, change the following line:

if (gameState == 1){hitReceived();}  // only process hits if player in game and alive

to:

  if (gameState == 1 && hit[hitNo][0] != playerID){hitReceived();}  // only process hits if player in game and alive & not shooting self
  if (hit[hitNo][0] == playerID) {
    debugInfo += "Self-shot detected <br>";
    if (hitNo>0) {hitNo--;} //take count back and it will be rewritten next shot
  }

Christian Content

In Lasertag, we want to record every hit against you. We want to record how many times someone hit you, and how much damage was applied with each hit, and we want to record who did it.

It would be a pretty sad world if we were to be this petty in real life. For the game it makes sense, but in real life, if you recorded every bad thing people did to you, then the chances are, you would be a pretty grumpy person.

The Bible tells us in:

1 Corinthians 13:4–5 (HCSB) 4 Love is patient, love is kind. Love does not envy, is not boastful, is not conceited, 5 does not act improperly, is not selfish, is not provoked, and does not keep a record of wrongs.

If we love someone, we don’t keep a record of their wrongs, we forgive them.

When it comes to God, there is joy in knowing that he loves us and that our sins are forgiven.

Psalm 32:1–2 (HCSB) 1 How joyful is the one whose transgression is forgiven, whose sin is covered! 2 How joyful is the man the Lord does not charge with sin and in whose spirit is no deceit!

This love from God does not come automatically, we need to accept his gift of forgiveness by believing that Jesus died on the cross for our sins, and start obeying Jesus as our Lord.

Leave a Reply