Instructables
Duino tagger- General introduction

Duino tag is a laser tag system based around the arduino.

Finally a laser tag system that can be tweaked modded and hacked until you have the perfect laser tag system for office ordnance, woodland wars and suburban skirmishes.

Laser tag is combat game like paintball or airsoft without the pain, it uses infrared light (IR) to simulate the tagging / shooting of other players or targets.

I have been working on this project for a while, but don't see it as over, I just though it was time to get more people involved. Hopefully this instructable will be near enough finished in time for me to enter it in the arduino competition, although I expect the instructable will need editing and tweaking for some time to come. 


This instructable aims to provide you with the information you will need to go out and build your own duino tagger.
This instructable focuses on how to build a duino tagger by modifying a light gun but with a bit of extra work you could build you own gun from scratch.

This instructable does not look in too much detail at the software / code side of the project, although a working code based on the miles tag protocol is provided.

For those wishing to learn about duino tagger programming I suggest you start at the excellent tutorials found at A Terrible Idea.

Thoes experience arduino users will probably find the overview page (Step 1) and code pages (Step 8) the most useful, newer arduino users may need to take a closer look at the instructable and the links provided.

I hope many of you will find this instructable useful and will go on to build your own duino taggers. There is much scope for improving and upgrading this system outlined here. If you do go on to improve on this duinotagger please share your work and hopefully in time the system will evolve into a much richer gaming experience.  


Youtube videos of my duino taggers:


This video shows me using the second duino tagger I made to shoot at a talcapult target I have been working on. I hope to make an instructable about the talcapult soon.





 
Remove these adsRemove these ads by Signing Up
1-40 of 80Next »
Quin57 months ago

I'm looking to purchase a lazertag system to start a lazertag business,
would you be able to produce or lead me to the guns and equipment I need
that would be safe and certifiable, that would be a cheaper option than
the $600/gun price range that I've seen advertised by the lazertag
manufacturers? email if so at ulrichinvesting@gmail.com

j44 (author)  Quin55 months ago

Sorry im not really familiar with the commercial systems, and dont know of any cheaper alternatives.

morteeamore2 years ago
I uploaded the project in the Arduino 2560.
first problem:
ERROR
sensor: 1 ... 2 2 2 2 0 2 2 0 2 2 0 2 2 2 2 2 2

as Keanan,

I followed the advice:

Try removing timeOut paramter in le seguenti line of the code:

received [i] = pulseIn (IRreceivePin, LOW, timeOut);

and did not work,

I changed the output pin of the IR, I put A2, and began to see the signal,
but sees alternately:
Ready ....
sensor: 0 ... 0 0 2 0 0 1 0 1 0 0 1 0 0 0 0 1 1
ERROR
sensor: 0 ... 0 0 1 0 0 1 0 1 0 0 1 0 0 0 0 1 1
Valid Signal
Hit No: 0 Player: 5 Team: 1 Weapon: 1 HP: 1 Parity: 1
Life: 2
sensor: 0 ... 0 0 2 0 0 2 0 2 0 0 2 0 0 0 0 1 1
ERROR
sensor: 0 ... 0 0 2 0 0 2 0 2 0 0 1 0 0 0 0 1 1
ERROR
sensor: 0 ... 0 0 1 0 0 1 0 1 0 0 1 0 0 0 0 1 1
Valid Signal
Hit No: 1 Player: 5 Team: 1 Weapon: 1 HP: 1 Parity: 1
Life: 1
sensor: 0 ... 0 0 1 0 0 1 0 1 0 0 1 0 0 0 0 1 1
Valid Signal
Hit No: 2 Player: 5 Team: 1 Weapon: 1 HP: 1 Parity: 1
Life: 0
DEAD

What solution do you recommend?
j44 (author)  morteeamore2 years ago
I wrote this code for arduino 14 and quite a few of the functions seem to have changed since then.
I don't really have the time now to sit down and rewrite it but if you look through some of the comments below I have made a few suggestions as to what the problems might be.
funkytaco j441 year ago
Can I use Arduino Uno for this?
GR0B1 year ago
How compatible is this code with the Miles Tag Protocol?
I have loaded it onto an Arduino but when I shoot it with a device that is using the Miles Tag Protocol I just get "ERROR".
If i use this code to shoot a Miles Tag Protocol based gun it does not register.

Comparing the IR signals my shooting a unit running http://www.righto.com/2009/08/multi-protocol-infrared-remote-library.html
the code above gives a unknown signal type while the Miles Tag gun gives a Sony:17bit
funkytaco1 year ago
How much will this project cost? I already have two Nerf guns I could gut.
nairdajun1 year ago
Hi guys,

I wanted to start on this project with an arduino nano and so i bought a light gun. Except this gun is rather complicated and has a recoil blowback feature. It looks extremely different to the one you have, and i dont know where to start =\. Im a total arduino noob and ive never done an arduino project before, but i was wondering whether it was possible to use the same IR firing internals without replacing the LED (because as you can see its the little silver box) as well as powering the recoil feature. From the picture you can see the wiring to the bottom, that is an actual laser pointer.
2013-03-01 11.28.38.jpg
hargard2 years ago
Hi.
And a big thank YOU for this instruction. Real good.
Gota try and make this myself and mabe add things.. Have done some Arduino codeing in past.
Thanks Again.
Hargard
Keanan3 years ago
I have tried these recommendations and the arduino receiving the IR code still can't see all of the IR code or not at all. Is there any other code that could work for simple two person tag that uses the same components?
j44 (author)  Keanan3 years ago
//The following should respond to any IR signal

// Start of code (copy and paste into arduino sketch)
//
// Duino Tag release V1.01
// Laser Tag for the arduino based on the Miles Tag Protocol.
// By J44industries: www.J44industries.blogspot.com
// For information on building your own Duino Tagger go to: http://www.instructables.com/member/j44/
//
// Much credit deserves to go to Duane O'Brien if it had not been for the excellent Duino Tag tutorials he wrote I would have never been able to write this code.
// Duane's tutorials are highly recommended reading in order to gain a better understanding of the arduino and IR communication. See his site http://aterribleidea.com/duino-tag-resources/
//
// This code sets out the basics for arduino based laser tag system and tries to stick to the miles tag protocol where possible.
// Miles Tag details: http://www.lasertagparts.com/mtdesign.htm
// There is much scope for expanding the capabilities of this system, and hopefully the game will continue to evolve for some time to come.
// Licence: Attribution Share Alike: Give credit where credit is due, but you can do what you like with the code.
// If you have code improvements or additions please go to http://duinotag.blogspot.com
//

// Digital IO's
int triggerPin = 3; // Push button for primary fire. Low = pressed
int trigger2Pin = 13; // Push button for secondary fire. Low = pressed
int speakerPin = 4; // Direct output to piezo sounder/speaker
int audioPin = 9; // Audio Trigger. Can be used to set off sounds recorded in the kind of electronics you can get in greetings card that play a custom message.
int lifePin = 6; // An analogue output (PWM) level corresponds to remaining life. Use PWM pin: 3,5,6,9,10 or 11. Can be used to drive LED bar graphs. eg LM3914N
int ammoPin = 5; // An analogue output (PWM) level corresponds to remaining ammunition. Use PWM pin: 3,5,6,9,10 or 11.
int hitPin = 7; // LED output pin used to indicate when the player has been hit.
int IRtransmitPin = 2; // Primary fire mode IR transmitter pin: Use pins 2,4,7,8,12 or 13. DO NOT USE PWM pins!! More info: http://j44industries.blogspot.com/2009/09/arduino-frequency-generation.html#more
int IRtransmit2Pin = 8; // Secondary fire mode IR transmitter pin: Use pins 2,4,7,8,12 or 13. DO NOT USE PWM pins!!
int IRreceivePin = 12; // The pin that incoming IR signals are read from
int IRreceive2Pin = 11; // Allows for checking external sensors are attached as well as distinguishing between sensor locations (eg spotting head shots)
// Minimum gun requirements: trigger, receiver, IR led, hit LED.

// Player and Game details
int myTeamID = 1; // 1-7 (0 = system message)
int myPlayerID = 5; // Player ID
int myGameID = 0; // Interprited by configureGane subroutine; allows for quick change of game types.
int myWeaponID = 0; // Deffined by gameType and configureGame subroutine.
int myWeaponHP = 0; // Deffined by gameType and configureGame subroutine.
int maxAmmo = 0; // Deffined by gameType and configureGame subroutine.
int maxLife = 0; // Deffined by gameType and configureGame subroutine.
int automatic = 0; // Deffined by gameType and configureGame subroutine. Automatic fire 0 = Semi Auto, 1 = Fully Auto.
int automatic2 = 0; // Deffined by gameType and configureGame subroutine. Secondary fire auto?

//Incoming signal Details
int received[18]; // Received data: received[0] = which sensor, received[1] - [17] byte1 byte2 parity (Miles Tag structure)
int check = 0; // Variable used in parity checking

// Stats
int ammo = 0; // Current ammunition
int life = 0; // Current life

// Code Variables
int timeOut = 0; // Deffined in frequencyCalculations (IRpulse + 50)
int FIRE = 0; // 0 = don't fire, 1 = Primary Fire, 2 = Secondary Fire
int TR = 0; // Trigger Reading
int LTR = 0; // Last Trigger Reading
int T2R = 0; // Trigger 2 Reading (For secondary fire)
int LT2R = 0; // Last Trigger 2 Reading (For secondary fire)

// Signal Properties
int IRpulse = 600; // Basic pulse duration of 600uS MilesTag standard 4*IRpulse for header bit, 2*IRpulse for 1, 1*IRpulse for 0.
int IRfrequency = 38; // Frequency in kHz Standard values are: 38kHz, 40kHz. Choose dependant on your receiver characteristics
int IRt = 0; // LED on time to give correct transmission frequency, calculated in setup.
int IRpulses = 0; // Number of oscillations needed to make a full IRpulse, calculated in setup.
int header = 4; // Header lenght in pulses. 4 = Miles tag standard
int maxSPS = 10; // Maximum Shots Per Seconds. Not yet used.
int TBS = 0; // Time between shots. Not yet used.

// Transmission data
int byte1[8]; // String for storing byte1 of the data which gets transmitted when the player fires.
int byte2[8]; // String for storing byte1 of the data which gets transmitted when the player fires.
int myParity = 0; // String for storing parity of the data which gets transmitted when the player fires.

// Received data
int memory = 10; // Number of signals to be recorded: Allows for the game data to be reviewed after the game, no provision for transmitting / accessing it yet though.
int hitNo = 0; // Hit number
// Byte1
int player[10]; // Array must be as large as memory
int team[10]; // Array must be as large as memory
// Byte2
int weapon[10]; // Array must be as large as memory
int hp[10]; // Array must be as large as memory
int parity[10]; // Array must be as large as memory


void setup() {
// Serial coms set up to help with debugging.
Serial.begin(9600);
Serial.println("Startup...");
// Pin declarations
pinMode(triggerPin, INPUT);
pinMode(trigger2Pin, INPUT);
pinMode(speakerPin, OUTPUT);
pinMode(audioPin, OUTPUT);
pinMode(lifePin, OUTPUT);
pinMode(ammoPin, OUTPUT);
pinMode(hitPin, OUTPUT);
pinMode(IRtransmitPin, OUTPUT);
pinMode(IRtransmit2Pin, OUTPUT);
pinMode(IRreceivePin, INPUT);
pinMode(IRreceive2Pin, INPUT);

frequencyCalculations(); // Calculates pulse lengths etc for desired frequency
configureGame(); // Look up and configure game details
tagCode(); // Based on game details etc works out the data that will be transmitted when a shot is fired


digitalWrite(triggerPin, HIGH); // Not really needed if your circuit has the correct pull up resistors already but doesn't harm
digitalWrite(trigger2Pin, HIGH); // Not really needed if your circuit has the correct pull up resistors already but doesn't harm

for (int i = 1;i < 254;i++) { // Loop plays start up noise
analogWrite(ammoPin, i);
playTone((3000-9*i), 2);
}

// Next 4 lines initialise the display LEDs
analogWrite(ammoPin, ((int) ammo));
analogWrite(lifePin, ((int) life));
lifeDisplay();
ammoDisplay();

Serial.println("Ready....");
}


// Main loop most of the code is in the sub routines
void loop(){
receiveIR();
if(FIRE != 0){
shoot();
ammoDisplay();
}
triggers();
}


// SUB ROUTINES


void ammoDisplay() { // Updates Ammo LED output
float ammoF;
ammoF = (260/maxAmmo) * ammo;
if(ammoF <= 0){ammoF = 0;}
if(ammoF > 255){ammoF = 255;}
analogWrite(ammoPin, ((int) ammoF));
}


void lifeDisplay() { // Updates Ammo LED output
float lifeF;
lifeF = (260/maxLife) * life;
if(lifeF <= 0){lifeF = 0;}
if(lifeF > 255){lifeF = 255;}
analogWrite(lifePin, ((int) lifeF));
}


void receiveIR() { // Void checks for an incoming signal and decodes it if it sees one.

if(digitalRead(IRreceivePin) == LOW){ // If the receive pin is low a signal is being received.
digitalWrite(hitPin,HIGH);
hit();
}
}

void shoot() {
if(FIRE == 1){ // Has the trigger been pressed?
Serial.println("FIRE 1");
sendPulse(IRtransmitPin, 4); // Transmit Header pulse, send pulse subroutine deals with the details
delayMicroseconds(IRpulse);

for(int i = 0; i < 8; i++) { // Transmit Byte1
if(byte1[i] == 1){
sendPulse(IRtransmitPin, 1);
//Serial.print("1 ");
}
//else{Serial.print("0 ");}
sendPulse(IRtransmitPin, 1);
delayMicroseconds(IRpulse);
}

for(int i = 0; i < 8; i++) { // Transmit Byte2
if(byte2[i] == 1){
sendPulse(IRtransmitPin, 1);
// Serial.print("1 ");
}
//else{Serial.print("0 ");}
sendPulse(IRtransmitPin, 1);
delayMicroseconds(IRpulse);
}

if(myParity == 1){ // Parity
sendPulse(IRtransmitPin, 1);
}
sendPulse(IRtransmitPin, 1);
delayMicroseconds(IRpulse);
Serial.println("");
Serial.println("DONE 1");
}


if(FIRE == 2){ // Where a secondary fire mode would be added
Serial.println("FIRE 2");
sendPulse(IRtransmitPin, 4); // Header
Serial.println("DONE 2");
}
FIRE = 0;
ammo = ammo - 1;
}


void sendPulse(int pin, int length){ // importing variables like this allows for secondary fire modes etc.
// This void genertates the carrier frequency for the information to be transmitted
int i = 0;
int o = 0;
while( i < length ){
i++;
while( o < IRpulses ){
o++;
digitalWrite(pin, HIGH);
delayMicroseconds(IRt);
digitalWrite(pin, LOW);
delayMicroseconds(IRt);
}
}
}


void triggers() { // Checks to see if the triggers have been presses
LTR = TR; // Records previous state. Primary fire
LT2R = T2R; // Records previous state. Secondary fire
TR = digitalRead(triggerPin); // Looks up current trigger button state
T2R = digitalRead(trigger2Pin); // Looks up current trigger button state
// Code looks for changes in trigger state to give it a semi automatic shooting behaviour
if(TR != LTR && TR == LOW){
FIRE = 1;
}
if(T2R != LT2R && T2R == LOW){
FIRE = 2;
}
if(TR == LOW && automatic == 1){
FIRE = 1;
}
if(T2R == LOW && automatic2 == 1){
FIRE = 2;
}
if(FIRE == 1 || FIRE == 2){
if(ammo < 1){FIRE = 0; noAmmo();}
if(life < 1){FIRE = 0; dead();}
// Fire rate code to be added here
}

}


void configureGame() { // Where the game characteristics are stored, allows several game types to be recorded and you only have to change one variable (myGameID) to pick the game.
if(myGameID == 0){
myWeaponID = 1;
maxAmmo = 30;
ammo = 30;
maxLife = 3;
life = 3;
myWeaponHP = 1;
}
if(myGameID == 1){
myWeaponID = 1;
maxAmmo = 100;
ammo = 100;
maxLife = 10;
life = 10;
myWeaponHP = 2;
}
}


void frequencyCalculations() { // Works out all the timings needed to give the correct carrier frequency for the IR signal
IRt = (int) (500/IRfrequency);
IRpulses = (int) (IRpulse / (2*IRt));
IRt = IRt - 4;
// Why -4 I hear you cry. It allows for the time taken for commands to be executed.
// More info: http://j44industries.blogspot.com/2009/09/arduino-frequency-generation.html#more

Serial.print("Oscilation time period /2: ");
Serial.println(IRt);
Serial.print("Pulses: ");
Serial.println(IRpulses);
timeOut = IRpulse + 50; // Adding 50 to the expected pulse time gives a little margin for error on the pulse read time out value
}


void tagCode() { // Works out what the players tagger code (the code that is transmitted when they shoot) is
byte1[0] = myTeamID >> 2 & B1;
byte1[1] = myTeamID >> 1 & B1;
byte1[2] = myTeamID >> 0 & B1;

byte1[3] = myPlayerID >> 4 & B1;
byte1[4] = myPlayerID >> 3 & B1;
byte1[5] = myPlayerID >> 2 & B1;
byte1[6] = myPlayerID >> 1 & B1;
byte1[7] = myPlayerID >> 0 & B1;


byte2[0] = myWeaponID >> 2 & B1;
byte2[1] = myWeaponID >> 1 & B1;
byte2[2] = myWeaponID >> 0 & B1;

byte2[3] = myWeaponHP >> 4 & B1;
byte2[4] = myWeaponHP >> 3 & B1;
byte2[5] = myWeaponHP >> 2 & B1;
byte2[6] = myWeaponHP >> 1 & B1;
byte2[7] = myWeaponHP >> 0 & B1;

myParity = 0;
for (int i=0; i<8; i++) {
if(byte1[i] == 1){myParity = myParity + 1;}
if(byte2[i] == 1){myParity = myParity + 1;}
myParity = myParity >> 0 & B1;
}

// Next few lines print the full tagger code.
Serial.print("Byte1: ");
Serial.print(byte1[0]);
Serial.print(byte1[1]);
Serial.print(byte1[2]);
Serial.print(byte1[3]);
Serial.print(byte1[4]);
Serial.print(byte1[5]);
Serial.print(byte1[6]);
Serial.print(byte1[7]);
Serial.println();

Serial.print("Byte2: ");
Serial.print(byte2[0]);
Serial.print(byte2[1]);
Serial.print(byte2[2]);
Serial.print(byte2[3]);
Serial.print(byte2[4]);
Serial.print(byte2[5]);
Serial.print(byte2[6]);
Serial.print(byte2[7]);
Serial.println();

Serial.print("Parity: ");
Serial.print(myParity);
Serial.println();
}


void playTone(int atone, int duration) { // A sub routine for playing tones like the standard arduino melody example
for (long i = 0; i < duration * 1000L; i += atone * 2) {
digitalWrite(speakerPin, HIGH);
delayMicroseconds(atone);
digitalWrite(speakerPin, LOW);
delayMicroseconds(atone);
}
}


void dead() { // void determines what the tagger does when it is out of lives
// Makes a few noises and flashes some lights
for (int i = 1;i < 254;i++) {
analogWrite(ammoPin, i);
playTone((1000+9*i), 2);
}
analogWrite(ammoPin, ((int) ammo));
analogWrite(lifePin, ((int) life));
Serial.println("DEAD");

for (int i=0; i<10; i++) {
analogWrite(ammoPin, 255);
digitalWrite(hitPin,HIGH);
delay (500);
analogWrite(ammoPin, 0);
digitalWrite(hitPin,LOW);
delay (500);
}
}


void noAmmo() { // Make some noise and flash some lights when out of ammo
digitalWrite(hitPin,HIGH);
playTone(500, 100);
playTone(1000, 100);
digitalWrite(hitPin,LOW);
}


void hit() { // Make some noise and flash some lights when you get shot
digitalWrite(hitPin,HIGH);
life = life - 1;
Serial.print("Life: ");
Serial.println(life);
playTone(500, 500);
if(life <= 0){dead();}
digitalWrite(hitPin,LOW);
lifeDisplay();
}
This code is work for me, other not.
j44 (author)  Keanan3 years ago
//The following code has a slight modification to the tone function which may have solved a potential problem, so might be worth a try, I will also post some simpler code in a minute

// Start of code (copy and paste into arduino sketch)
//
// Duino Tag release V1.01
// Laser Tag for the arduino based on the Miles Tag Protocol.
// By J44industries: www.J44industries.blogspot.com
// For information on building your own Duino Tagger go to: http://www.instructables.com/member/j44/
//
// Much credit deserves to go to Duane O'Brien if it had not been for the excellent Duino Tag tutorials he wrote I would have never been able to write this code.
// Duane's tutorials are highly recommended reading in order to gain a better understanding of the arduino and IR communication. See his site http://aterribleidea.com/duino-tag-resources/
//
// This code sets out the basics for arduino based laser tag system and tries to stick to the miles tag protocol where possible.
// Miles Tag details: http://www.lasertagparts.com/mtdesign.htm
// There is much scope for expanding the capabilities of this system, and hopefully the game will continue to evolve for some time to come.
// Licence: Attribution Share Alike: Give credit where credit is due, but you can do what you like with the code.
// If you have code improvements or additions please go to http://duinotag.blogspot.com
//

// Digital IO's
int triggerPin = 3; // Push button for primary fire. Low = pressed
int trigger2Pin = 13; // Push button for secondary fire. Low = pressed
int speakerPin = 4; // Direct output to piezo sounder/speaker
int audioPin = 9; // Audio Trigger. Can be used to set off sounds recorded in the kind of electronics you can get in greetings card that play a custom message.
int lifePin = 6; // An analogue output (PWM) level corresponds to remaining life. Use PWM pin: 3,5,6,9,10 or 11. Can be used to drive LED bar graphs. eg LM3914N
int ammoPin = 5; // An analogue output (PWM) level corresponds to remaining ammunition. Use PWM pin: 3,5,6,9,10 or 11.
int hitPin = 7; // LED output pin used to indicate when the player has been hit.
int IRtransmitPin = 2; // Primary fire mode IR transmitter pin: Use pins 2,4,7,8,12 or 13. DO NOT USE PWM pins!! More info: http://j44industries.blogspot.com/2009/09/arduino-frequency-generation.html#more
int IRtransmit2Pin = 8; // Secondary fire mode IR transmitter pin: Use pins 2,4,7,8,12 or 13. DO NOT USE PWM pins!!
int IRreceivePin = 12; // The pin that incoming IR signals are read from
int IRreceive2Pin = 11; // Allows for checking external sensors are attached as well as distinguishing between sensor locations (eg spotting head shots)
// Minimum gun requirements: trigger, receiver, IR led, hit LED.

// Player and Game details
int myTeamID = 1; // 1-7 (0 = system message)
int myPlayerID = 5; // Player ID
int myGameID = 0; // Interprited by configureGane subroutine; allows for quick change of game types.
int myWeaponID = 0; // Deffined by gameType and configureGame subroutine.
int myWeaponHP = 0; // Deffined by gameType and configureGame subroutine.
int maxAmmo = 0; // Deffined by gameType and configureGame subroutine.
int maxLife = 0; // Deffined by gameType and configureGame subroutine.
int automatic = 0; // Deffined by gameType and configureGame subroutine. Automatic fire 0 = Semi Auto, 1 = Fully Auto.
int automatic2 = 0; // Deffined by gameType and configureGame subroutine. Secondary fire auto?

//Incoming signal Details
int received[18]; // Received data: received[0] = which sensor, received[1] - [17] byte1 byte2 parity (Miles Tag structure)
int check = 0; // Variable used in parity checking

// Stats
int ammo = 0; // Current ammunition
int life = 0; // Current life

// Code Variables
int timeOut = 0; // Deffined in frequencyCalculations (IRpulse + 50)
int FIRE = 0; // 0 = don't fire, 1 = Primary Fire, 2 = Secondary Fire
int TR = 0; // Trigger Reading
int LTR = 0; // Last Trigger Reading
int T2R = 0; // Trigger 2 Reading (For secondary fire)
int LT2R = 0; // Last Trigger 2 Reading (For secondary fire)

// Signal Properties
int IRpulse = 600; // Basic pulse duration of 600uS MilesTag standard 4*IRpulse for header bit, 2*IRpulse for 1, 1*IRpulse for 0.
int IRfrequency = 56; // Frequency in kHz Standard values are: 38kHz, 40kHz. Choose dependant on your receiver characteristics
int IRt = 0; // LED on time to give correct transmission frequency, calculated in setup.
int IRpulses = 0; // Number of oscillations needed to make a full IRpulse, calculated in setup.
int header = 4; // Header lenght in pulses. 4 = Miles tag standard
int maxSPS = 10; // Maximum Shots Per Seconds. Not yet used.
int TBS = 0; // Time between shots. Not yet used.

// Transmission data
int byte1[8]; // String for storing byte1 of the data which gets transmitted when the player fires.
int byte2[8]; // String for storing byte1 of the data which gets transmitted when the player fires.
int myParity = 0; // String for storing parity of the data which gets transmitted when the player fires.

// Received data
int memory = 10; // Number of signals to be recorded: Allows for the game data to be reviewed after the game, no provision for transmitting / accessing it yet though.
int hitNo = 0; // Hit number
// Byte1
int player[10]; // Array must be as large as memory
int team[10]; // Array must be as large as memory
// Byte2
int weapon[10]; // Array must be as large as memory
int hp[10]; // Array must be as large as memory
int parity[10]; // Array must be as large as memory


void setup() {
// Serial coms set up to help with debugging.
Serial.begin(9600);
Serial.println("Startup...");
// Pin declarations
pinMode(triggerPin, INPUT);
pinMode(trigger2Pin, INPUT);
pinMode(speakerPin, OUTPUT);
pinMode(audioPin, OUTPUT);
pinMode(lifePin, OUTPUT);
pinMode(ammoPin, OUTPUT);
pinMode(hitPin, OUTPUT);
pinMode(IRtransmitPin, OUTPUT);
pinMode(IRtransmit2Pin, OUTPUT);
pinMode(IRreceivePin, INPUT);
pinMode(IRreceive2Pin, INPUT);

frequencyCalculations(); // Calculates pulse lengths etc for desired frequency
configureGame(); // Look up and configure game details
tagCode(); // Based on game details etc works out the data that will be transmitted when a shot is fired


digitalWrite(triggerPin, HIGH); // Not really needed if your circuit has the correct pull up resistors already but doesn't harm
digitalWrite(trigger2Pin, HIGH); // Not really needed if your circuit has the correct pull up resistors already but doesn't harm

for (int i = 1;i < 254;i++) { // Loop plays start up noise
analogWrite(ammoPin, i);
playTone((3000-9*i), 2);
}

// Next 4 lines initialise the display LEDs
analogWrite(ammoPin, ((int) ammo));
analogWrite(lifePin, ((int) life));
lifeDisplay();
ammoDisplay();

Serial.println("Ready....");
}


// Main loop most of the code is in the sub routines
void loop(){
receiveIR();
if(FIRE != 0){
shoot();
ammoDisplay();
}
triggers();
}


// SUB ROUTINES


void ammoDisplay() { // Updates Ammo LED output
float ammoF;
ammoF = (260/maxAmmo) * ammo;
if(ammoF <= 0){ammoF = 0;}
if(ammoF > 255){ammoF = 255;}
analogWrite(ammoPin, ((int) ammoF));
}


void lifeDisplay() { // Updates Ammo LED output
float lifeF;
lifeF = (260/maxLife) * life;
if(lifeF <= 0){lifeF = 0;}
if(lifeF > 255){lifeF = 255;}
analogWrite(lifePin, ((int) lifeF));
}


void receiveIR() { // Void checks for an incoming signal and decodes it if it sees one.
int error = 0;

if(digitalRead(IRreceivePin) == LOW){ // If the receive pin is low a signal is being received.
digitalWrite(hitPin,HIGH);
if(digitalRead(IRreceive2Pin) == LOW){ // Is the incoming signal being received by the head sensors?
received[0] = 1;
}
else{
received[0] = 0;
}

while(digitalRead(IRreceivePin) == LOW){
}
for(int i = 1; i <= 17; i++) { // Repeats several times to make sure the whole signal has been received
received[i] = pulseIn(IRreceivePin, LOW, timeOut); // pulseIn command waits for a pulse and then records its duration in microseconds.
}

Serial.print("sensor: "); // Prints if it was a head shot or not.
Serial.print(received[0]);
Serial.print("...");

for(int i = 1; i <= 17; i++) { // Looks at each one of the received pulses
int receivedTemp[18];
receivedTemp[i] = 2;
if(received[i] > (IRpulse - 200) && received[i] < (IRpulse + 200)) {receivedTemp[i] = 0;} // Works out from the pulse length if it was a data 1 or 0 that was received writes result to receivedTemp string
if(received[i] > (IRpulse + IRpulse - 200) && received[i] < (IRpulse + IRpulse + 200)) {receivedTemp[i] = 1;} // Works out from the pulse length if it was a data 1 or 0 that was received
received[i] = 3; // Wipes raw received data
received[i] = receivedTemp[i]; // Inputs interpreted data

Serial.print(" ");
Serial.print(received[i]); // Print interpreted data results
}
Serial.println(""); // New line to tidy up printed results

// Parity Check. Was the data received a valid signal?
check = 0;
for(int i = 1; i <= 16; i++) {
if(received[i] == 1){check = check + 1;}
if(received[i] == 2){error = 1;}
}
// Serial.println(check);
check = check >> 0 & B1;
// Serial.println(check);
if(check != received[17]){error = 1;}
if(error == 0){Serial.println("Valid Signal");}
else{Serial.println("ERROR");}
if(error == 0){interpritReceived();}
digitalWrite(hitPin,LOW);
}
}


void interpritReceived(){ // After a message has been received by the ReceiveIR subroutine this subroutine decidedes how it should react to the data
if(hitNo == memory){hitNo = 0;} // hitNo sorts out where the data should be stored if statement means old data gets overwritten if too much is received
team[hitNo] = 0;
player[hitNo] = 0;
weapon[hitNo] = 0;
hp[hitNo] = 0;
// Next few lines Effectivly converts the binary data into decimal
// Im sure there must be a much more efficient way of doing this
if(received[1] == 1){team[hitNo] = team[hitNo] + 4;}
if(received[2] == 1){team[hitNo] = team[hitNo] + 2;}
if(received[3] == 1){team[hitNo] = team[hitNo] + 1;}

if(received[4] == 1){player[hitNo] = player[hitNo] + 16;}
if(received[5] == 1){player[hitNo] = player[hitNo] + 8;}
if(received[6] == 1){player[hitNo] = player[hitNo] + 4;}
if(received[7] == 1){player[hitNo] = player[hitNo] + 2;}
if(received[8] == 1){player[hitNo] = player[hitNo] + 1;}

if(received[9] == 1){weapon[hitNo] = weapon[hitNo] + 4;}
if(received[10] == 1){weapon[hitNo] = weapon[hitNo] + 2;}
if(received[11] == 1){weapon[hitNo] = weapon[hitNo] + 1;}

if(received[12] == 1){hp[hitNo] = hp[hitNo] + 16;}
if(received[13] == 1){hp[hitNo] = hp[hitNo] + 8;}
if(received[14] == 1){hp[hitNo] = hp[hitNo] + 4;}
if(received[15] == 1){hp[hitNo] = hp[hitNo] + 2;}
if(received[16] == 1){hp[hitNo] = hp[hitNo] + 1;}

parity[hitNo] = received[17];

Serial.print("Hit No: ");
Serial.print(hitNo);
Serial.print(" Player: ");
Serial.print(player[hitNo]);
Serial.print(" Team: ");
Serial.print(team[hitNo]);
Serial.print(" Weapon: ");
Serial.print(weapon[hitNo]);
Serial.print(" HP: ");
Serial.print(hp[hitNo]);
Serial.print(" Parity: ");
Serial.println(parity[hitNo]);


//This is probably where more code should be added to expand the game capabilities at the moment the code just checks that the received data was not a system message and deducts a life if it wasn't.
if (player[hitNo] != 0){hit();}
hitNo++ ;
}


void shoot() {
if(FIRE == 1){ // Has the trigger been pressed?
Serial.println("FIRE 1");
sendPulse(IRtransmitPin, 4); // Transmit Header pulse, send pulse subroutine deals with the details
delayMicroseconds(IRpulse);

for(int i = 0; i < 8; i++) { // Transmit Byte1
if(byte1[i] == 1){
sendPulse(IRtransmitPin, 1);
//Serial.print("1 ");
}
//else{Serial.print("0 ");}
sendPulse(IRtransmitPin, 1);
delayMicroseconds(IRpulse);
}

for(int i = 0; i < 8; i++) { // Transmit Byte2
if(byte2[i] == 1){
sendPulse(IRtransmitPin, 1);
// Serial.print("1 ");
}
//else{Serial.print("0 ");}
sendPulse(IRtransmitPin, 1);
delayMicroseconds(IRpulse);
}

if(myParity == 1){ // Parity
sendPulse(IRtransmitPin, 1);
}
sendPulse(IRtransmitPin, 1);
delayMicroseconds(IRpulse);
Serial.println("");
Serial.println("DONE 1");
}


if(FIRE == 2){ // Where a secondary fire mode would be added
Serial.println("FIRE 2");
sendPulse(IRtransmitPin, 4); // Header
Serial.println("DONE 2");
}
FIRE = 0;
ammo = ammo - 1;
}


void sendPulse(int pin, int length){ // importing variables like this allows for secondary fire modes etc.
// This void genertates the carrier frequency for the information to be transmitted
int i = 0;
int o = 0;
while( i < length ){
i++;
while( o < IRpulses ){
o++;
digitalWrite(pin, HIGH);
delayMicroseconds(IRt);
digitalWrite(pin, LOW);
delayMicroseconds(IRt);
}
}
}


void triggers() { // Checks to see if the triggers have been presses
LTR = TR; // Records previous state. Primary fire
LT2R = T2R; // Records previous state. Secondary fire
TR = digitalRead(triggerPin); // Looks up current trigger button state
T2R = digitalRead(trigger2Pin); // Looks up current trigger button state
// Code looks for changes in trigger state to give it a semi automatic shooting behaviour
if(TR != LTR && TR == LOW){
FIRE = 1;
}
if(T2R != LT2R && T2R == LOW){
FIRE = 2;
}
if(TR == LOW && automatic == 1){
FIRE = 1;
}
if(T2R == LOW && automatic2 == 1){
FIRE = 2;
}
if(FIRE == 1 || FIRE == 2){
if(ammo < 1){FIRE = 0; noAmmo();}
if(life < 1){FIRE = 0; dead();}
// Fire rate code to be added here
}

}


void configureGame() { // Where the game characteristics are stored, allows several game types to be recorded and you only have to change one variable (myGameID) to pick the game.
if(myGameID == 0){
myWeaponID = 1;
maxAmmo = 30;
ammo = 30;
maxLife = 3;
life = 3;
myWeaponHP = 1;
}
if(myGameID == 1){
myWeaponID = 1;
maxAmmo = 100;
ammo = 100;
maxLife = 10;
life = 10;
myWeaponHP = 2;
}
}


void frequencyCalculations() { // Works out all the timings needed to give the correct carrier frequency for the IR signal
IRt = (int) (500/IRfrequency);
IRpulses = (int) (IRpulse / (2*IRt));
IRt = IRt - 4;
// Why -4 I hear you cry. It allows for the time taken for commands to be executed.
// More info: http://j44industries.blogspot.com/2009/09/arduino-frequency-generation.html#more

Serial.print("Oscilation time period /2: ");
Serial.println(IRt);
Serial.print("Pulses: ");
Serial.println(IRpulses);
timeOut = IRpulse + 50; // Adding 50 to the expected pulse time gives a little margin for error on the pulse read time out value
}


void tagCode() { // Works out what the players tagger code (the code that is transmitted when they shoot) is
byte1[0] = myTeamID >> 2 & B1;
byte1[1] = myTeamID >> 1 & B1;
byte1[2] = myTeamID >> 0 & B1;

byte1[3] = myPlayerID >> 4 & B1;
byte1[4] = myPlayerID >> 3 & B1;
byte1[5] = myPlayerID >> 2 & B1;
byte1[6] = myPlayerID >> 1 & B1;
byte1[7] = myPlayerID >> 0 & B1;


byte2[0] = myWeaponID >> 2 & B1;
byte2[1] = myWeaponID >> 1 & B1;
byte2[2] = myWeaponID >> 0 & B1;

byte2[3] = myWeaponHP >> 4 & B1;
byte2[4] = myWeaponHP >> 3 & B1;
byte2[5] = myWeaponHP >> 2 & B1;
byte2[6] = myWeaponHP >> 1 & B1;
byte2[7] = myWeaponHP >> 0 & B1;

myParity = 0;
for (int i=0; i<8; i++) {
if(byte1[i] == 1){myParity = myParity + 1;}
if(byte2[i] == 1){myParity = myParity + 1;}
myParity = myParity >> 0 & B1;
}

// Next few lines print the full tagger code.
Serial.print("Byte1: ");
Serial.print(byte1[0]);
Serial.print(byte1[1]);
Serial.print(byte1[2]);
Serial.print(byte1[3]);
Serial.print(byte1[4]);
Serial.print(byte1[5]);
Serial.print(byte1[6]);
Serial.print(byte1[7]);
Serial.println();

Serial.print("Byte2: ");
Serial.print(byte2[0]);
Serial.print(byte2[1]);
Serial.print(byte2[2]);
Serial.print(byte2[3]);
Serial.print(byte2[4]);
Serial.print(byte2[5]);
Serial.print(byte2[6]);
Serial.print(byte2[7]);
Serial.println();

Serial.print("Parity: ");
Serial.print(myParity);
Serial.println();
}


void playTone(int atone, int duration) { // A sub routine for playing tones like the standard arduino melody example
for (long i = 0; i < duration * 1000L; i += atone * 2) {
digitalWrite(speakerPin, HIGH);
delayMicroseconds(atone);
digitalWrite(speakerPin, LOW);
delayMicroseconds(atone);
}
}


void dead() { // void determines what the tagger does when it is out of lives
// Makes a few noises and flashes some lights
for (int i = 1;i < 254;i++) {
analogWrite(ammoPin, i);
playTone((1000+9*i), 2);
}
analogWrite(ammoPin, ((int) ammo));
analogWrite(lifePin, ((int) life));
Serial.println("DEAD");

for (int i=0; i<10; i++) {
analogWrite(ammoPin, 255);
digitalWrite(hitPin,HIGH);
delay (500);
analogWrite(ammoPin, 0);
digitalWrite(hitPin,LOW);
delay (500);
}
}


void noAmmo() { // Make some noise and flash some lights when out of ammo
digitalWrite(hitPin,HIGH);
playTone(500, 100);
playTone(1000, 100);
digitalWrite(hitPin,LOW);
}


void hit() { // Make some noise and flash some lights when you get shot
digitalWrite(hitPin,HIGH);
life = life - hp[hitNo];
Serial.print("Life: ");
Serial.println(life);
playTone(500, 500);
if(life <= 0){dead();}
digitalWrite(hitPin,LOW);
lifeDisplay();
}
KT Gadget5 years ago
This is awesome! I am just about to use the Arduinos for a competition my robotics club is going into. I really want to build a full on laser tag system for my group of friends to mess around with.

One question though, wouldnt a laser work the same way (i know the hazard might be the problem) but isnt that what is used for most laser tag systems?

As a suggestion, make a simple voltage regulator for the arduino and add a bigger battery to be able to use the motor, but I am not sure how to signal the motor to move (Im thinking using a PWM system?).
j44 (author)  KT Gadget5 years ago
Thanks!

All laser tag systems that I know of use IR diodes, although some systems have a low power laser as well just to show where you have shot. If you were to just use a laser then you would find the bean would be far too narrow; you would either have to coat the players head to toe in sensors or aim very very accurately at the sensors, I think you would struggle to get a realistic game feel.

I have got force feedback motors working with the arduino, but I wanted to keep these guns small and relativly simple so didn't want to bother with a bigger battery pack so disabled the feature.

Let me know how you get on.

Maybe you can try with an r/c modell accu pack, like this one.
http://www.hobbyking.com/hobbyking/store/__11938__Turnigy_nano_tech_6000mah_2S_25_50C_Lipo_Pack.html

Cute, little, and strong.
mdshann j443 years ago
Since you already have sensors mounted on the player wouldn't it be possible to use the same line and put a battery pack say on a persons back? Having a high battery capacity would allow you to use the rumble feature in the gun, and maybe even add more rumble motors. It would be cool say if you get hit on the head sensor to have a small cell phone sized rumble go off on that sensor, and maybe a larger one for the chest sensor and a third on the back sensor. The rumble in the gun could be use as feedback for when you shoot, and then when you are out of ammo it doesn't give feedback, like a gun not firing when out of ammo. Another feature that could be cool is a game start timer, say everyone starts in the middle of the playing field and hits the power on the system at the same time and it gives you like 30-45 seconds to run to your base / starting point and then all the sensors go off to indicate the game has started.
sparktech j444 years ago
hey i was wondering i dont know if you can do this but heres my idea could you tell me how i could do this? ok so a small screen to tell you where the other people are (10 ft acurate) a umpc built into the gun so at the beginning of the game you choose a gun (machine gun, sniper rifle, etc. each gun has dif. ammo quantities, damage, and "range") each person gets a pistol with unlimited ammo. get 3 grenades,1 nuke (each has dif radius of how much it kills) 100 health to start with and all the sensors are on the gun (maybe 6?) i was wondering if you could get and program all these things into the gun i dont care about costs so could you just tell me how or if you think it is possible? i am learning how to program this stuff but i have about 6 people who could help me program the arduino Thanks
j44 (author)  sparktech4 years ago
Different types of guns etc would be quite easy to implement, I started to put support for that in the code. Knowing where other players are would probably need GPS, wireless links and screens etc, to start from scratch would be tricky and expensive. If I were to undertake a build like that I would think about using the arduino with an android phone. The android phone would provide the GPS, screen and wireless communication, in a small convenient easy to program device. For how to use an arduino with an android phone try looking at the amarino tool kit: http://www.amarino-toolkit.net/ Let me know if you build anything, would be interested to see any pics or vids.
sparktech j444 years ago
for gps couldnt you use an xbee module? they make screens for arduino
j44 (author)  sparktech4 years ago
Its taken me longer than I am proud of to notice what seems to be going on here.

Its almost you have a vested interest in advertising SparkFun products :-)

So I will say the following:

I have recently used the MP3 Trigger V2 to add sound to a project and it was great (easy to use and worked very well).
http://www.sparkfun.com/commerce/product_info.php?products_id=9715
Would be a great way of improving the sound effect on a duino tagger.

I have also had very good experiences with several arduino pro minis recently.
http://www.sparkfun.com/commerce/product_info.php?products_id=9218
Great for any compact duino tag projects.

So credit where credit is due I have had good experience with SparkFun products.

Still think it would be cool to have a duino tagger android app ;-P
sparktech j444 years ago
http://www.sparkfun.com/commerce/product_info.php?products_id=8977 this is even easier than the other touchscreen this would not be a problem to put on the arduino
sparktech j444 years ago
here is a screen and a trackball to use as a button so you could select stuff and scroll http://www.sparkfun.com/commerce/product_info.php?products_id=8537 http://www.sparkfun.com/commerce/product_info.php?products_id=9320 then if price is not an option you could use this and substitute it for both or keep the trackball (i love the blackberry trackballs) http://www.sparkfun.com/commerce/product_info.php?products_id=8624 please tell me what you think about this
sparktech j444 years ago
sure ill let you know what i do!!
Kryptonite j445 years ago
In Canberra where I am, we have a laser tag arena that we often to go for birthday parties (not so much now...) where you get a vest with sensors on the shoulders, front, back and on the gun. If you should just off the sensor, then it will not sound it. But if the laser points directly at it then you're good. This does not make it much harder at all.

This arena also has "bases" which you have to shoot 3 times (with 3 second down time between each shot at the base) and then you capture it and score 10 points in stead of 1.

Feedback motors are cool, but as a "laser" feel it's alright if they're kept off, to keep with the futuristic feel of just the laser.
Keanan3 years ago
Thank you for your help. The code that registers any IR signal code works great for our application. Oh which I forgot to mention what that was. It is a laser tag plane. Their are two planes that have the IR that can shoot at each other. When they are hit I have them make noise along with the flashing to manually keep track of hits. Also, with the simplicity of the code, the planes could be shot at from the ground by TV remotes, adding a new element.
Once again Thank you for your tutorial and help.

"AerospaceSmith" <(^^,)>
Keanan Keanan3 years ago
Images of our plane
2011-12-09 19.21.14.jpg2011-12-09 20.38.11.jpg2011-12-11 17.02.43.jpg
Keanan3 years ago
No I am still having issues. However, I have gotten it receive the signal 1 time, out of billions. This was only after using an older version of Arduino IDE (00017) to upload the code and after changing the code to 38kHz. I'm thinking the code can't read the IR pulses fast enough.

Using a code from Ladyada (https://github.com/adafruit/Raw-IR-decoder-for-Arduino/blob/master/rawirdecode.pde) I was able to read the raw IR code sent from the other arduino(which was sending 56kHz at the time). So it seems to be the lasertag code can't read fast enough. I am in urgent need of help or a new code as soon as possible. I just need a simple laser tag code to use between mainly two people playing against each other that I can have each players score keep track of independently. Any and all help will be greatly appreciated.
j44 (author)  Keanan3 years ago
I have done some investigation into the latest IDE, it does seem to change a few things, it seems to have slightly slowed the digital write function.

Yeshi88 advice is good try looking at what length pulses you are receiving.

If the values don't come out quite right a simple fix could be:

Add a line in void FrequencyCalculations after line

IRpulses = (int) (IRpulse / (2*IRt));

to

IRpulses = (int) (IRpulse / (2*IRt));
IRpulses = IRpulses - 5;

Where I have put - 5 try a few values between about + 10 and - 10 and see if that sorts things out.

Failing that in the set up change Serial.begin(9600); to Serial.begin(57600);
Note you will also have to change the baud in the Serial Monitor. I dont think it is particularly likely to help but in the latest IDE they do seem to have changed some serial stuff and if it has got slower perhaps that could be a problem.

Sorry I don't really have the facilities to replicate the fault at the moment but hopefully some of the suggestions might help.
Keanan3 years ago
Okay, I am I have copied your code to my Arduinos and have a similar setup as yours, the only main difference being I am using 56kHz receivers and have changed the IRfrequency variable to 56, but when ever I shoot the other arduino, it just reads ERROR
sensor: 1... 2 2 2 2 0 2 2 0 2 2 0 2 2 2 2 2 2

after the sometimes there is all 2s and sometimes zeros randomly in the string. I also set it up to display received[17] specifically and it is always 2. Why is this?

I also have the Arduinos close to each other, could the short distance be affecting the IR signal?

Any advise would be greatly appreciated.
2011-11-16 22-43-36.162.jpgCapture.JPG
yeshi88 Keanan3 years ago
Try removing timeOut paramter in the following line of the code:

received[i] = pulseIn(IRreceivePin, LOW, timeOut);

If it still does not work , try adding

Serial.println(received[i]); before the following lines.

int receivedTemp[18];
receivedTemp[i] = 2;
if(received[i] > (IRpulse - 200) && received[i] < (IRpulse + 200)) {receivedTemp[i] = 0;} // Works out from the pulse length if it was a data 1 or 0 that was received writes result to receivedTemp string

Your output should fall between 400 - 800 for byte 0 and 1000 - 1400 for byte 1
tag Keanan3 years ago
Hi,

My lasertag used to work and as of today I am getting the above error as well ?

I have tried J44's suggestions and still nothing? Weird !

Any other suggestions ?

Thanks
Tag
tag tag3 years ago
just had a thought - can this be related to the new Arduino client ?

Thanks
Tag
j44 (author)  Keanan3 years ago
I suspect what might be happening is that the timings are not quite working out.
The code uses rather basic bit bashing to generate the frequencies and may not be making the pulses the right length with the frequency set to 56k, meaning the receive arduino is rejecting the pulses as not valid.

The best thing to do would be to look at the signal to the transmitter LED on an oscilloscope and make sure the pulses are 600us for 0's and 1200us for 1's, but there are some other simpler fault finding tests you can try.

If you put IRfrequency variable back to 38 and try it you might find it works (even though a 56k reciever is not designed to recieve that frequency it will be slightly sensitive to it and might just give you short ranges.

If that works put the frequency back to 56 then try the following:
In the frequencyCalculation void there is a line IRt = IRt - 4. Try -3 or -5 instead of the -4 and see if that makes it work.
Revolt Lab3 years ago
If I wanted to do this with all hardware could I not just put a PNP transistor on the output of the IR detector and connect lights/sounds etc to power through the PNP transistor?

Thanks!
Will
j44 (author)  Revolt Lab3 years ago
I am not sure I quite understand the design you are proposing, but if you take your hit signal directly from the sensors you will not be able to check the received signal is a valid tagger shot as opposed to just noise or a TV remote.
Revolt Lab3 years ago
Are the receivers interfered with by sunlight? Can it be used on a sunny day with lots of direct sunlight?
j44 (author)  Revolt Lab3 years ago
The sensors are designed to detect a modulated IR signal, this means the signals can be detected even with significant ambient IR. I have not noticed any reduction in performance in bright light but there may be some.
Nate7114 years ago
j44 the first receiver picture is hard to read because of its low quality. Could you upload a better picture? the rest is clear and awesome. the tactapult is also neat
j44 (author)  Nate7114 years ago
Feedback appreciated.

Have done what I can within the resolution and compression limits on the web site. Hope its ok now, if not I will have a think about what else I can do (I would probably have to upload the picture as several smaller parts).
1-40 of 80Next »