Simple XOpen System

Da X-Open Project.

X-Open System

Il sistema di marcatura IR X-Open si pone nel mondo del lasertag italiano come un sistema entry level ad alte prestazioni.

Vediamone le caratteristiche tecniche:

- Dual core: due microcontrollori comunicanti via UART che gestiscono in modo autonomo ricezione e invio IR per le migliori prestazioni possibili.

- Attivazione del gearbox mediante relay.

- Audio mediante speaker amplificato

- Compatibile con i proiettori e con i sensori X-TaG

- Low Cost

Ricordiamo comunque a tutti i costruttori che si cimenteranno nell'esperienza che X-Open nasce con lo scopo di fare avvicinare i giocatori al mondo lasertag nel modo più economico possibile. Il sistema per essere programmato (cambio punti ferita, cambio numero caricatori e colpi, ecc...) deve essere connesso ad un PC e riprogrammato.

Proprio per queste caratteristiche difficilmente sarà un sistema che verrà accettato a tornei o gare ufficiali in quanto le impostazioni non possono essere controllate e non possono essere bloccate. E' un ottimo sistema, molto performante e stabile, ma nasce per fare avvicinare nuovi giocatori al mondo del lasertag in maniera economica, per consentire a quelle persone che sono dubbiose di provare a giocare a lasertag con il proprio club in modo MILES compatibile spendendo pochissimo o per i più smanettoni di creare proprie realizzazioni partendo dal sistema base pubblicato di seguito.

Elenco componenti

Il progetto si compone dei seguenti componenti:

  • 5x Resistenze 100 Ohm 1/4W
  • 5x Resistenze 10K Ohm 1/4W
  • 1x IRLD024 Mosfet N-Channel
  • 4x BS170 Mosfet N-Channel
  • 1x Speaker
  • 1x Relay bobina 5V
  • 1x Resistenza 1,2 Ohm 1/4W
  • 1x LED IR 940nm
  • 5x LED Rosso


Schematico

Schematico

Breadboard

Bread board

Pinout componenti

  • BS170, mosfet-N ad uso generico.

BS170 Pin out

  • IRLD024, Mosfet-N ad alte prestazioni

IRLD024 Pin out

  • Relay, bobina a 5V, contatto singolo 250VAC 10A

Relay Pin out

Spiegazioni tecniche

  • MOSFET N: o transistor a effetto di campo è un componente a 3 piedini, Gate, Drain e Source.

Il mosfet di base è un amplificatore comandato in tensione tramite il pin Gate, infatti quando la tensione tra Gate e Source (Vgs > Vth) supera una certa soglia (variabile in base al modello di mosfet) tra Drain e Source si crea un canale di silicio fortemente drogato che permette alla corrente elettrica di fluire quasi liberamente. Quasi poichè è sempre presente una certa resistenza tra Drain e Source anch'essa caratteristica di ogni modello di mosfet. Usiamo mosfet di tipo N perchè si accendono portando a valore logico alto il pin del microcontrollore.

  • Resistenze di gate: le resistenze di gate sono così chiamate perchè si posizionano tra pin di uscita del microcontrollore e il gate del mosfet.

Idealmente il gate dovrebbe avere resistenza infinita e quindi non permettere scorrimento di corrente, ma in pratica, sono presenti capacità parassite che durante i fronti di salita del segnale di pilotaggio diventano dei corto circuiti. Grazie a queste resistenze si evita di stressare il microcontrollore.

  • Resistenze di pull-down: sono molto grandi e servono a "tirare" verso massa il segnale presente sul Gate dei mosfet stabilizzandolo e mantengono spento il mosfet (Vgs = 0).

Librerie necessarie

  • Arduino TimerObject: [1]
  • z3t0 IR Remote Library: [2]

Una volta scaricata la libreria IRRemote è bene cancellare la libreria RobotIrRemote inclusa nella IDE di Arduino per evitare conflitti.

Aprite la cartella IRRemote ed effettuate una modifica al file IRRemoteInt.h, alla riga 204:

// Arduino Duemilanove, Diecimila, LilyPad, Mini, Fio, Nano, etc
#else
	//#define IR_USE_TIMER1   // tx = pin 9
	#define IR_USE_TIMER2     // tx = pin 3

#endif

Si deve modificare in:

// Arduino Duemilanove, Diecimila, LilyPad, Mini, Fio, Nano, etc
#else
	#define IR_USE_TIMER1   // tx = pin 9
	//#define IR_USE_TIMER2     // tx = pin 3

#endif
  • Arduino Timer: [3]

Prima di programmare

Quando si programmano i due Arduino sarà necessario interrompere la connessione seriale tra i due.

E' buona norma sfilarli dalla breadboard durante la programmazione per evitare che in caso di errori firmware o hardware la porta USB del PC possa rompersi a causa di un corto circuito o una richiesta di corrente troppo alta.

Meglio bruciare un 7805 che il controller USB del computer! ;)

Firmware Weapon

/*
 * (C) Copyright 2016 X-Tag IR (http://www.xtag-ir.com/).
 *
 * Licensed under the GNU GPL license version 3.
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.gnu.org/licenses/gpl-3.0.en.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

#include "TimerObject.h"
#include "IRremote.h"

// Weapon configuration
#define WEAPON_RATE 600            //rateo di fuoco colpi al minuto valori compresi tra [1-1200] 
#define WEAPON_DAMAGE 34           //danno dell'arma
#define WEAPON_MAGAZINES 10        //numero di caricatori
#define WEAPON_AMMO 30             // proiettili in un singolo caricatore
#define WEAPON_RELOAD_TIME_MS 5000 // tempo di ricarica espresso in millisecondi

// Hardware settings
#define PIN_MUZZLE 4
#define PIN_GB 5
#define PIN_TRIGGER 11
#define PIN_RELOAD 12
#define PIN_STATUS 13

#define SERIAL_BAUD_RATE 115200
#define MAX_SERIAL_BUFFER_SIZE 30
#define MIN_IR_SEND_TS 33

// Runtime variables
uint8_t playerTeam;
uint8_t playerNo;
uint16_t ammo;
uint8_t magazines;
uint16_t fireDelay;
bool isActive = false;
bool isReloading = false;
bool isFiring = false;

IRsend irSend;
TimerObject *tmrReload = new TimerObject(WEAPON_RELOAD_TIME_MS);
TimerObject *tmrFire;

char serialBuffer[MAX_SERIAL_BUFFER_SIZE];
uint8_t serialBufferPos = 0;

bool getParityBit(uint32_t data, unsigned int len)
{
  unsigned int bits1count = 0;

  for (int i=0; i<len; i++)
  {
    if ((data >> i) & 0b1) {
      bits1count++;
    }
  }

  return (bits1count % 2);
}

void fireShot() {
  if (ammo == 0) {
    stopFire();
    return;
  }
  
  uint32_t packet = 0x0;
  boolean parityBit;

  packet |= playerTeam & 0b111;

  packet <<= 5;
  packet |= playerNo & 0b11111;

  packet <<= 8;
  packet |= WEAPON_DAMAGE & 0b11111111;

  parityBit = getParityBit(packet, 16);

  packet <<= 1;
  packet |= parityBit;

  digitalWrite(PIN_MUZZLE, HIGH);
  irSend.sendSony(packet, 17);
  digitalWrite(PIN_MUZZLE, LOW);

  ammo--;
}

void hardwareSetup() {
  pinMode(PIN_MUZZLE, OUTPUT);
  pinMode(PIN_GB, OUTPUT);
  pinMode(PIN_STATUS, OUTPUT);

  pinMode(PIN_TRIGGER, INPUT_PULLUP);
  pinMode(PIN_RELOAD, INPUT_PULLUP);
  
  digitalWrite(PIN_MUZZLE, LOW);
  digitalWrite(PIN_GB, LOW);
  digitalWrite(PIN_STATUS, LOW);

  Serial.begin(SERIAL_BAUD_RATE);

  fireDelay = (60000 / WEAPON_RATE);
  if (fireDelay < MIN_IR_SEND_TS*2) fireDelay = MIN_IR_SEND_TS*2;
  fireDelay -= MIN_IR_SEND_TS;

  tmrFire = new TimerObject(fireDelay);
  tmrFire->setOnTimer(&fireShot);
}

void setup() {
  hardwareSetup();
}

void stopOps() {
  tmrReload->Stop();
  stopFire();
}

void ledStatus(bool s) {
  digitalWrite(PIN_STATUS, s);
}

void activate() {
  ledStatus(true);
  isActive = true;
}

void deactivate() {
  ledStatus(false);
  isActive = false;
  stopOps();
}

void reammo() {
  ammo = 0;
  magazines = WEAPON_MAGAZINES;
}

void _reloadMagazine() {
  ledStatus(true);
  magazines--;
  ammo = WEAPON_AMMO;
  tmrReload->Stop();
}

void reloadMagazine() {
  if (!isActive) return;
  if (magazines == 0) return;

  ledStatus(false);
  
  tmrReload->setOnTimer(&_reloadMagazine);
  tmrReload->Start();
}

void onSerialMessage(char *message) {
  uint8_t n;
  
  if (strcmp_P(message, PSTR("#activate")) == 0) {
    activate();
  } else if (strcmp_P(message, PSTR("#deactivate")) == 0) {
    deactivate();
  } else if (strcmp_P(message, PSTR("#reammo")) == 0) {
    reammo();
  } else if (sscanf(message, "#team %d", &n) == 1) {
    playerTeam = n;
  } else if (sscanf(message, "#player %d", &n) == 1) {
    playerNo = n;
  }
}

void startFire() {
  if (ammo == 0) return;
  if (!isActive) return;
  if (isReloading) return;
  if (isFiring) return;

  fireShot();
  digitalWrite(PIN_GB, HIGH);
  isFiring = true;
  tmrFire->Start();
}

void stopFire() {
  if (!isFiring) return;
  digitalWrite(PIN_GB, LOW);
  isFiring = false;
  tmrFire->Stop();
}

void loopTrigger() {
  if (digitalRead(PIN_TRIGGER) == LOW) {
    if (!isFiring) startFire();
  } else {
    if (isFiring) stopFire();
  }
}

void loopReload() {
  if (digitalRead(PIN_RELOAD) == LOW) {
    if (!isReloading) reloadMagazine();
  }
}


void loopSerial() {
  char c;
  
  while (Serial.available()) {
    c = Serial.read();
    if ((c == '\n') || (c == '\r')) {
      serialBuffer[serialBufferPos++] = '\0';
      if (serialBufferPos > 1) {
        onSerialMessage(serialBuffer);
      }
      serialBufferPos = 0;
    } else {
      serialBuffer[serialBufferPos++] = c;
    }
  } 
}

void loop() {
  loopSerial();
  loopTrigger();
  loopReload();
  
  tmrReload->Update();
  tmrFire->Update();
}

Firmware Player

/*
 * (C) Copyright 2016 X-Tag IR (http://www.xtag-ir.com/).
 *
 * Licensed under the GNU GPL license version 3.
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.gnu.org/licenses/gpl-3.0.en.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

#include "Timer.h"
#include "IRremote.h"

// Miles settings
#define TEAM_RED 1
#define TEAM_BLUE 2
#define TEAM_YELLOW 3
#define TEAM_GREEN 4
#define TEAM_BLACK 5
#define TEAM_WHITE 6
#define TEAM_PURPLE 7
#define PLAYER_IMMUNE_MODE_REAL 0
#define PLAYER_IMMUNE_MODE_TEAM 1
#define PLAYER_IMMUNE_MODE_SELF 2

#define XOPEN_SYS_ADMIN 0x09
#define XOPEN_SYS_ADMIN_KILL_PLAYER 0x00
#define XOPEN_SYS_ADMIN_RESPAWN 0x04
#define XOPEN_SYS_ADMIN_SENSOR_TEST 0x15
#define XOPEN_SYS_ADMIN_ENDGAME 0x07
#define XOPEN_SYS_ADMIN_STARTGAME 0x03

//////////////////////////////////////////////////////////////////////
// Player confiurable parameters
#define PLAYER_LIFE 100        // vita del giocatore
#define PLAYER_ARMOR 0         // piastre antiproiettile del giocatore
#define PLAYER_ARMOR_FACTOR 2  // fattore di riduzione del danno
#define PLAYER_TEAM TEAM_RED   // squadra del giocatore
#define PLAYER_NO 15           // numero del giocatore

// Game configuration
#define PLAYER_IMMUNE_MODE PLAYER_IMMUNE_MODE_TEAM  // immunità colpi propri/squadra/realistico
//////////////////////////////////////////////////////////////////////

// Hardware settings
#define PIN_HITLED A1
#define PIN_TSOP 9
#define PIN_SOUND 10
#define PIN_STATUS 13

//#define DEBUG_PLAYER

#define SERIAL_BAUD_RATE 115200

// Runtime variables
uint16_t playerLife;
uint8_t playerArmor;
uint8_t playerArmorFactor;
uint8_t playerTeam;
uint8_t playerNo;
bool playerIsDead;

Timer tmr;

IRrecv irRecv(PIN_TSOP);
decode_results irRes;

void hardwareSetup() {
  pinMode(PIN_HITLED, OUTPUT);
  pinMode(PIN_SOUND, OUTPUT);
  pinMode(PIN_STATUS, OUTPUT);
  
  digitalWrite(PIN_HITLED, HIGH);
  digitalWrite(PIN_SOUND, HIGH);
  digitalWrite(PIN_STATUS, LOW);

  irRecv.enableIRIn();
  Serial.begin(SERIAL_BAUD_RATE);
}

void hitled(bool s) {
  digitalWrite(PIN_HITLED, s);
}

void buzzer(uint16_t frequency, uint16_t duration) {
  tone(PIN_SOUND, frequency, duration);
}

void ledInitPlayer() {
  digitalWrite(PIN_STATUS, HIGH);
  for (uint8_t i=0; i<3; i++) {
    hitled(!(i % 2));
    if (i % 2 == 0) buzzer(1000, 500);
    delay(500);
  }
  hitled(false);
}

void ledPlayerDead() {
  digitalWrite(PIN_STATUS, LOW);
  
  for (uint8_t i=0; i<4; i++) {
    hitled(!(i % 2));
    if (i % 2 == 0) buzzer(500, 500);
    delay(500);
  }
  hitled(true);
  buzzer(500, 2000);
}

void ledSensorTest() {
  hitled(true);
  buzzer(1000, 1000);
  delay(1000);
  hitled(false);
}

void ledShot() {
  tmr.oscillate(PIN_HITLED, 100, LOW, 1);
  buzzer(500, 200);
}

void initPlayer() {
  ledInitPlayer();
  
  playerLife = PLAYER_LIFE;
  playerArmor = PLAYER_ARMOR;
  playerArmorFactor = PLAYER_ARMOR_FACTOR;
  playerTeam = PLAYER_TEAM;
  playerNo = PLAYER_NO;
  playerIsDead = false;

  #ifdef DEBUG_PLAYER
  Serial.println("Player respawn");
  #endif

  Serial.print("#team ");
  Serial.println(playerTeam);
  Serial.print("#player ");
  Serial.println(playerNo);
  Serial.println("#activate");
  Serial.println("#reammo");
}

bool getParityBit(uint32_t data, unsigned int len)
{
  unsigned int bits1count = 0;

  for (int i=0; i<len; i++)
  {
    if ((data >> i) & 0b1) {
      bits1count++;
    }
  }

  return (bits1count % 2);
}

void playerDead() {
  if (playerIsDead)
    return;

  Serial.println("#deactivate");

  playerLife = 0;
  playerIsDead = true;
  ledPlayerDead();

  #ifdef DEBUG_PLAYER
  Serial.println("Player dead");
  #endif
}

void onInvalidMessage() {
  // TODO: Nearmiss shot
}

void onSystemAdminMessage(uint8_t lsb) {
  switch (lsb) {
    case XOPEN_SYS_ADMIN_ENDGAME:
    case XOPEN_SYS_ADMIN_KILL_PLAYER:
      playerDead();
      break;

    case XOPEN_SYS_ADMIN_RESPAWN:
    case XOPEN_SYS_ADMIN_STARTGAME:
      initPlayer();
      break;

    case XOPEN_SYS_ADMIN_SENSOR_TEST:
      ledSensorTest();
      break;
  }
}

void onSystemMessage(uint8_t command, uint8_t lsb) {
  if (command = XOPEN_SYS_ADMIN) {
    return onSystemAdminMessage(lsb);
  }
}

void onShotMessage(uint8_t teamId, uint8_t playerId, uint8_t damage) {
  if (playerIsDead)
    return;

  if (
    ((PLAYER_IMMUNE_MODE == PLAYER_IMMUNE_MODE_TEAM) && (teamId == PLAYER_TEAM)) ||
    ((PLAYER_IMMUNE_MODE == PLAYER_IMMUNE_MODE_SELF) && (teamId == PLAYER_TEAM) && (playerId == PLAYER_NO))
  ) {
    return;
  }

  ledShot();
  
  // Decrease player's armor
  if (playerArmor > 0) {
    damage /= playerArmorFactor;
    playerArmor--;
  }

  // Decrease player's life
  if (damage > playerLife) {
    playerLife = 0;
  } else {
    playerLife-=damage;
  }

  if (playerLife == 0) {
    playerDead();
  }

  #ifdef DEBUG_PLAYER
  Serial.print("Player life: ");
  Serial.print(playerLife);
  Serial.println("");
  #endif
}

void onValidMessage(uint32_t message) {
  if (!message)
    return;

  uint8_t msb = (message >> 8) & 0xff;
  uint8_t lsb = message & 0xff;

  uint8_t teamId = (msb >> 5) & 0b111;
  uint8_t command = msb & 0b11111;

  if (teamId == 0x00)
  {
    onSystemMessage(command, lsb);
  }
  else
  {
    if (lsb > 0)
    {
      onShotMessage(teamId, command, lsb);
    }
    else
    {
      onInvalidMessage();
    }
  }
}

void onIrMessage(decode_results res) {
  if (res.decode_type != SONY)
    return;

  uint32_t rawMessage = res.value & 0x1ffff;
  uint32_t message = (rawMessage >> 1) & 0xffff;
  boolean parityBit = rawMessage & 0b1;

  if ((getParityBit(message, 16) != parityBit) || !message)
  {
    onInvalidMessage();
    return;
  }

  onValidMessage(message);
}

void loopIr() {
  if (irRecv.decode(&irRes))
  {
    onIrMessage(irRes);
    irRecv.resume();
  }
}

void setup() {
  delay(500);
  
  hardwareSetup();
  initPlayer();
}

void loop() {
  loopIr();
  tmr.update();

  if (playerIsDead) {
    digitalWrite(PIN_HITLED, HIGH);
  }
}

Buon lavoro!

Built.png