ATTAG – new speakers and IR laser

I mentioned it earlier that the formerly used speakers are not anymore available, so I ordered a batch of new ones. They don’t sound much different than the other ones but might if the sampe rate of the sounds would be better. That will be fine tuning it the end. Got them here:

link: https://de.aliexpress.com/item/1005006313439352.html

I am also still experimenting with the best solution for the IR signal and got one of these IR laser modules:

Problem is, they are not trustworthy cause the product description says class 2, the specs say class 3A and the label says class 3B. Lasers with 780nm wavelength cannot be class 2 or 2M so it is 3B. Overall I still don’t feel good with the usage of these even if they are not too focused, there are high chances they are quite above 1mW as many revisions with laserpointers have shown. Not to mention that it depends on the environmental temperature. I’ll get a chance to test them somewhere for their power. Besides all you can read about effects of IR lasers on the eye are vague, either ignoring risks or overrating risks. So it is like lottery and that is a stupid idea with eyesight.

ATTAG – PAM8302A amplifier for audio

So the winner is the PAM8302A, with 2,5W output it is still loud enough, produces way less noise than the PAM8402A but is mono. However I don’t think it is necessary to have stereo sound in the blaster, besides the sound files are mono anyway to reduce their size. Apart from that anybody could solder any other amplifier to the mainboard, it is just that I’ll place the solder eyes to match the PAM302A layout.

ATTAG – hurdles

Why are there always hurdles? While I was experimenting with the WiFi LR mode, I stumbled over the issue, that it doesn’t work in the current espressif 2.0.14 package. No idea why, so I put that aside for a moment in hope there will be a fix soon. If anyone knows a solution, I opened an issue on github about it. Might just be my fault somewhere with the example code I used. I don’t know, it works with 2.0.3 but that version isn’t really compatible with the server/display/LVGL anymore. And I don’t really want to build it upon old lib versions.

link: https://github.com/espressif/arduino-esp32/issues/9140

So meeeanwhile I’ll get back to the blaster stuff, I ordered a batch of new speakers, since those I planned before are not really available anymore. The current amplifier I used catches a lot of static and makes constant noises, so some sort of filter is required, that should be doable with a capacitor. The HW-104 module is a PAM 8403 stereo module which actually is more than required so I might turn this down to a PAM8302A mono module. But since they both are almost the same size/price the one with less noise will win.

ATTAG bye bye HC-12, welcome WiFi LR?

Well, sort of. Maybe. Now once I was almost done with the HC-12 code I recognized it will cause a lot of trouble with multiple senders. The transmitted bits just get mixed up too easily plus the senders read their own sent data. So it means there needs to be far more CRC checking than I expected and with that longer transmissions. My programming capabilities are too limited to reinvent the wheel and I couldn’t find any good libs that can handle bidirectional communication as required.

However, luckily I stumbled across the fact, that espressif implemented a long range/low rate proprior WIFI protocol into their ESP32. And as the server smartdisplay is an ESP32 and the blasters will use one as well, this looks like a good alternative to 433MHz. Another advantage is less cost and less soldering, I just did a quick test and ran around on the backyard and had different results from within the house. However I guess outdoor only or with more line-of-sight it will work quite nice. Sad thing is there is no port for an external antenna on the smartdisplay board. But if this really needs an improvement the server could always switch to another touchdisplay run by a different ESP32 WITH a port for an external antenna, such as the ESP32-WROOM-32U.

Meanwhile here is the code for the server so far, I’ll keep it, in case I will get back to HC-12 for whatever reason may come. It looks far longer than it is, a lot of comments and serial prints in here and as always my amateurish style.

File: attag server code v0.1

attag_server_code_v0.1.zip

Besides from the libs you also need the board definition for the display esp32-2432S024C from here, link: https://github.com/rzeldent/platformio-espressif32-sunton/tree/d89176aa3c1173918844253f53dc17d813a72d94

// This is version 0.1 of the server code
// it was made using platformio and squareline studio, it doesn't work in arduino IDE as it currently is
#include <esp32_smartdisplay.h>      // lib required to use the display 
#include <ui/ui.h>                   // the ui made with SquarelineStudio                       
#include <Audio.h>                   // to enable beeps 
#include <SoftwareSerial.h>          // to enable serial communication, due to mess at gitHUB this is currecntly directly in the /src and src/curclular_queue directories
#include <SD.h>
Audio *audio;

const long ledinterval = 1000;       // blink pause time for the LED 1000ms = 1s                           
ulong next_millis;                   // helping variable for the blink timer
unsigned long previousMillis = 0;    // helping variable for the blink timer
unsigned long currentMillis = millis();
int players[16];                     // array of the blasters/players
int playerid;                        // id of the blaster/player
int ledswitch = 0;                   // required for the LED blink timer
int numplayers;                      // used to hold the numbe rof players read from the dropdown selectbox
int status=0;                        // current state of the program
int colors[3];                       // array to hold colors for the player state buttons
int s1=0;                            // int to hold numbers of players in status 1
int s2=0;                            // int to hold numbers of players in status 2
int percent=0;                       // into to hold percentage of players who joined, used for the status bar  
byte gd=0;                           // gamedata byte
byte gd2=0;                          // gamedata byte2 with checksum 
#define SD_CS   5
int SDready=0;
File logFile;


// define some colors for the player status "buttons"
lv_color_t color  = lv_color_hex(0x505050);
lv_color_t red    = lv_color_hex(0xAA0000);
lv_color_t yellow = lv_color_hex(0xAAAA00);
lv_color_t green  = lv_color_hex(0x009000);

// set the GPIO pins for 433MHz communication
SoftwareSerial hc12(35,22);

// this function builds the game data byte 
// where each option corresponds to 2 bits
// [GAME MODE][SHOTS PER MAG][RESPAWNS][BATTLE TIME]
// 00 00 00 00
void makegdbyte(int sb,int sel) {  
    switch (sel) {
      case 0: bitClear(gd,7-sb*2); bitClear(gd,6-sb*2);break;
      case 1: bitClear(gd,7-sb*2); bitSet(gd,6-sb*2);  break;
      case 2: bitSet(gd,7-sb*2);   bitClear(gd,6-sb*2);break;
      case 3: bitSet(gd,7-sb*2);   bitSet(gd,6-sb*2);  break;      
    }   
}

void watchgame() {
  // is HC12 433Mhz serial communication available?
   if (hc12.available()) { 
   } 
}

// function read the SD path and look for existing files to create a new id for the new logfile
int getnewfileid(File dir, int numTabs)
{
  int oldid=0;
  while (true)
  {
    File filename =  dir.openNextFile();
    if (! filename) { break; }
    // check for files that contain "attag"
    char *ptr = strstr(filename.name(), "attag");
    if (ptr != nullptr) {
      ptr+=strlen("attag");
      Serial.println(atoi(ptr));
      if (oldid<atoi(ptr)) oldid=atoi(ptr);
    }
    filename.close();
  }
  return (oldid+1);  
}



void startgame() {
  Serial.print("starting game..STATUS:");
  Serial.println(status);
  // switch to ingame screen and go to watchgame
  _ui_screen_change(&ui_Ingame, LV_SCR_LOAD_ANIM_MOVE_LEFT, 500, 0, &ui_Ingame_screen_init);

  // write logfile only if SD card is available
  if (SDready==1) {  

  File path = SD.open("/");
   
  // logfile for the game stored on the SD card
  // logFile = SD.open("fileName", FILE_APPEND);
  char filename[] = "00000000.csv";
  sprintf(filename,"/attag%02d.csv", getnewfileid(path,0));

  // open the logfile for writing
  logFile = SD.open(filename, FILE_WRITE);
 
  // if the logfile is ready for writing
  if (logFile) {   
    // write a header with the game data to the csv
    char buf[16];    
    logFile.println("Game mode;Number of players;Shots per mag;Respawns;Battle time;Friendly fire");
    lv_dropdown_get_selected_str(ui_Dropdown2, buf, sizeof(buf)); logFile.print(buf);logFile.print(";"); 
    lv_dropdown_get_selected_str(ui_Dropdown1, buf, sizeof(buf)); logFile.print(buf);logFile.print(";"); 
    lv_dropdown_get_selected_str(ui_Dropdown3, buf, sizeof(buf)); logFile.print(buf);logFile.print(";"); 
    lv_dropdown_get_selected_str(ui_Dropdown4, buf, sizeof(buf)); logFile.print(buf);logFile.print(";"); 
    lv_dropdown_get_selected_str(ui_Dropdown5, buf, sizeof(buf)); logFile.print(buf);logFile.print(";"); 
    logFile.println(lv_obj_has_state(ui_FF, LV_STATE_CHECKED));
    // logFile.close();  
  }
  } // end if SDready

  status=3;
  watchgame();
}

void dohandshake() {
   
   // is HC12 433Mhz serial communication available?
   if (hc12.available()) {    
    Serial.println("at handshake, received HC12");
    // RGB colors for the LED
    colors[0]=25;colors[1]=0;colors[2]=0;

    // read the incoming message and out it into a byte 
    byte received = hc12.read();

    //  checks if the received byte got a 111 tail = very basic attag identification
    int check=bitRead(received,0)+bitRead(received,1)+bitRead(received,2);
    // get the playerid
    int playerid = (received >> 4);     
    // what is the player status?
    int playerstatus=bitRead(received,3);

    Serial.print("senderID:"); Serial.print(playerid);   Serial.print(" / "); Serial.println(playerid,BIN);
    Serial.print("received:"); Serial.print(received);   Serial.print(" / "); Serial.println(received,BIN);
    Serial.print("checked:");  Serial.println(check);

    // if check is 3, sum up the first 3 bits read from right
    // && status = 0 = blaster calling with "I am here"
    if (check==3 && playerstatus==0) { 

      lv_obj_has_state(ui_FF, LV_STATE_CHECKED);     

      // set the color of the display LED RGB
      colors[0]=25;colors[1]=25;colors[2]=25;
      color=yellow;

      // if the player already was at status 2 but started a new connection
      // in case someone switched off the blaster or it lost power somehow
      // decrease the number of players set to status 2 
      if (players[playerid]==2) {
        s2=s2-1;
      }

      // set the player status to 1,  unless it already was on 1
      // actually this is an unused variable
      if (players[playerid]!=1) {
       players[playerid]=1;
       s1=s1+1; 
      } 

      // once the first player asks for join broadcast message the game data
      // that way some of the players join straight with ready status
      // so less total messages are required      
      hc12.write(gd);
      hc12.write(gd2);  


    } else if (check==3 && playerstatus==1)  {
      // set the color of the display LED RGB
      colors[0]=0;colors[1]=25;colors[2]=0;
      color=green; 

      // if the player had previous status 1 then remove one from the counter
      if (players[playerid]==1) {
        s1=s1-1;
      }      

      // set the player status to 2, unless it already was on 2 
      if (players[playerid]!=2) {        
        players[playerid]=2;
        s2=s2+1; 
      }      
    } 


    char numplayerstxt[10];
    lv_dropdown_get_selected_str(ui_Dropdown1, numplayerstxt, sizeof(numplayerstxt));

    // calculate the status bar progress percentage and update it
    percent=(100/numplayers*s2);         
    lv_bar_set_value(ui_Bar1, percent, LV_ANIM_OFF);

    Serial.print(" Numplayers:");Serial.print(numplayers); 
    Serial.print(" S1:");Serial.print(s1); 
    Serial.print(" S2:");Serial.print(s2); 
    Serial.print(" Prozent:"); Serial.println(percent);

    // paint the player buttons yellow or green depending on status
    // the buttons don't yet have any other function besides showing the status
    // but maybe they can be used for a link to a data page or whatever in a future version
    switch(playerid) {
     case 1:  lv_obj_set_style_bg_color(ui_Button1,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 2:  lv_obj_set_style_bg_color(ui_Button2,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 3:  lv_obj_set_style_bg_color(ui_Button3,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 4:  lv_obj_set_style_bg_color(ui_Button4,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 5:  lv_obj_set_style_bg_color(ui_Button5,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 6:  lv_obj_set_style_bg_color(ui_Button6,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;        
     case 7:  lv_obj_set_style_bg_color(ui_Button7,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 8:  lv_obj_set_style_bg_color(ui_Button8,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 9:  lv_obj_set_style_bg_color(ui_Button9,  color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 10: lv_obj_set_style_bg_color(ui_Button10, color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 11: lv_obj_set_style_bg_color(ui_Button11, color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 12: lv_obj_set_style_bg_color(ui_Button12, color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 13: lv_obj_set_style_bg_color(ui_Button13, color, LV_PART_MAIN | LV_STATE_DEFAULT);break;            
     case 14: lv_obj_set_style_bg_color(ui_Button13, color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 15: lv_obj_set_style_bg_color(ui_Button15, color, LV_PART_MAIN | LV_STATE_DEFAULT);break;    
     case 16: lv_obj_set_style_bg_color(ui_Button16, color, LV_PART_MAIN | LV_STATE_DEFAULT);break;            
    }
   
    // done with collecting players? proceed to game launch
    if (s2==numplayers) {
      Serial.println("going to set status=2");
      colors[0]=0;colors[1]=75;colors[2]=0;
      status=2;
      startgame();
    }  
    
   }  

   
  
  // blink the LED
  if (currentMillis - previousMillis >= ledinterval) {
    previousMillis = currentMillis;  
    if (ledswitch == 0) {      
      smartdisplay_led_set_rgb(colors[0], colors[1], colors[2]);  
      colors[0]=25;colors[1]=0;colors[2]=0;
      ledswitch=1;
    } else {
      smartdisplay_led_set_rgb(0, 0, 0);  
      ledswitch=0;
    }    
  }  
}


// doing the handshake between blasters and server
void gotostatus1(lv_event_t *e) {
 
    char buf[32];
    // get the game mode from selectbox and print it on handhsake screen
    lv_dropdown_get_selected_str(ui_Dropdown2, buf, sizeof(buf));
    lv_textarea_set_text(ui_TextArea1, "Game mode: ");
    lv_textarea_add_text(ui_TextArea1, buf);
    
    // get the number of players from selectbox and print it on handhsake screen
    lv_dropdown_get_selected_str(ui_Dropdown1, buf, sizeof(buf));
    lv_textarea_add_text(ui_TextArea1, "\nNumber of players: ");
    lv_textarea_add_text(ui_TextArea1, buf);    
    // get the number of players and convert to int  
    numplayers=atoi(buf);

    // get the number of shots per mag from selectbox and print it on handhsake screen
    lv_dropdown_get_selected_str(ui_Dropdown3, buf, sizeof(buf));
    lv_textarea_add_text(ui_TextArea1, "\nShots per mag: ");
    lv_textarea_add_text(ui_TextArea1, buf);    

    // get the number of respawns from selectbox and print it on handhsake screen
    lv_dropdown_get_selected_str(ui_Dropdown4, buf, sizeof(buf));
    lv_textarea_add_text(ui_TextArea1, "\nRespawns: ");
    lv_textarea_add_text(ui_TextArea1, buf);        

    // get the battle time from selectbox and print it on handhsake screen
    lv_dropdown_get_selected_str(ui_Dropdown5, buf, sizeof(buf));
    lv_textarea_add_text(ui_TextArea1, "\nBattle time: ");
    lv_textarea_add_text(ui_TextArea1, buf); 
   
    int gd_gamemode=lv_dropdown_get_selected(ui_Dropdown1);  
    int gd_shots=lv_dropdown_get_selected(ui_Dropdown3);  
    int gd_respawns=lv_dropdown_get_selected(ui_Dropdown4);  
    int gd_time=lv_dropdown_get_selected(ui_Dropdown5);   

    gd=gd_gamemode << 6 | gd_shots << 4 | gd_respawns << 2 | gd_time;

    // generate a basic checksum for the gamedata
    int csum=gd_gamemode+gd_shots+gd_respawns+gd_time;

    gd2=lv_obj_has_state(ui_FF, LV_STATE_CHECKED) <<6 | csum;   

    Serial.print(lv_obj_has_state(ui_FF, LV_STATE_CHECKED));
    Serial.print(" all the game data, well almost all:");
    Serial.println(gd,BIN);
    Serial.print("checksumbyte:");
    Serial.println(gd2,BIN);


    status=1;
}


void setup()
{
    delay(250);
    Serial.begin(115200);
    hc12.begin(9600);
    // Serial.begin(9600);
    Serial.println("launching...");
    Serial.setDebugOutput(true);
    
   // check if a SD card is abailable 
   if (SD.begin(SD_CS))  {
    SDready=1;
   } 
    smartdisplay_init();
    auto disp = lv_disp_get_default();
    lv_disp_set_rotation(disp, LV_DISP_ROT_90);
    smartdisplay_lcd_set_brightness_cb(NULL,10000);
    ui_init();

    // hide the red SD card icon if SD card is available
    if (SDready==1) { 
      lv_obj_set_x(ui_Image5,320);
    } 
}

void loop()
{
  // Serial.println(SDready);
  // waiting for players calling doing the handshake
  switch (status) {
    case 1: dohandshake();  break;
    case 2: startgame();    break;
    case 3: watchgame();    break;
  }
  lv_timer_handler();

}

ATTAG The legal stuff about 433MHz

I did mention this already when I came to the idea to use 433MHz HC-12 module for the basic communication. So here we go again, depending on the country you live in it might be required to choose a module with a different frequency or you have to limit the power and usage (duty cycle) of the module to stay within legal usage.

A power level of 4 (6.3 mW / 8dBm) is legal in Gemany on this frequency, and there is no duty cycle limit. Offical document: link: https://data.bundesnetzagentur.de/Bundesnetzagentur/SharedDocs/Downloads/DE/Sachgebiete/Telekommunikation/Unternehmen_Institutionen/Frequenzen/20210114_frequenzplan.pdf

The 6.3mW still allow a distance of 50-80m at 9600baud, using a capacitor even more. This should be enough for the battlefield. If this isn’t enough a step down to 1200 baud will be the solution.

Remember it is not only about being legal, it is also about not annoying other people using the 433MHz frequency too. So be fair and set it down.

#include <SoftwareSerial.h>
SoftwareSerial hc12(13,12); // change the pins to your requirement. I used a D1 mini for the programming.
void setup() {
  Serial.begin(9600);
  hc12.begin(9600);
  Serial.println("use AT+RX to read the settings of the module, use AT+Pn to set the Power of the module where n needs to be replaced by a number between 1 and 8");
}

void loop() { 
  if (Serial.available()) {
    hc12.write(Serial.read());
  }
}

ATTAG handshake between blaster and host part 2

My intentions to keep the communication as small as possible reached some insane level I guess. After I got mad about some limits of C++ that really made me angry (Goddamn I am not a programmer.) I came to the conclusion that 1 byte is enough for the talk between blaster and host. As follows:

A byte contains 8 bits. I will use the first 4 bits to identify the blaster as 4 bits can describe 2^4 states. The 5th bit will describe the status and the remaining bits will be used as identification as ATTAG.

Why so short? Speed and reduction of possible transmission errors.

I am not yet sure if this will make any sense at the end but to run the HC-12 even at very low baudrates, it makes sense to keep the traffic as low as possible.

// the first player 0 starts at decimal 7, the binary code 0000 0111
// the first 4 bits are the player id, the 5th bit describes status available=0 and ready=1
// the last 3 bits 111 stay as identification of attag
// the next player 1 with status "available" is baseplayer+16=23 
// the status ready for that player then is baseplayer+24=31

// get the playerid from a potvalue later
playerid=4; // just for simulation now

int baseplayer=7; // player zero
int pavailable=baseplayer+playerid*16; 
int pready=baseplayer+playerid*16+8;

ATTAG handshake between blaster and host

Working on a simple handshake protocol between the blaster and the host using the 433MHz connection.

Question: Why not Blutooth or Wifi, ESP32 got both already?

Answer: Yes it might also work with BT or Wifi and maybe I am wasting too much energy on the 433MHz component but there are certain advantages in my opinion. First is the range, you can easily increase the range by using a long antenna on the host and there is no failing or loss of connection. Simple serial messaging. The disadvantage is the speed, so I have to keep the sent strings as short as possible. However I am no specialist, if anyone wants to provide a reliable BT or WiFi alternative in a later mod, go for it. It would reduce costs of course.

So this is my plan:

  • Blaster: asks host
  • Host: replies with game settings if detected and adds blaster to player list
  • Blaster: replies with confirmation of received game data
  • Host: replies with status “ready”
// doing the handshake between blasters and server

// doing the handshake between blasters and server
void dohandshake(lv_event_t *e) {
  Serial.println("We are at handshake.");
 
   // is HC12 available?
   if (hc12.available()) {
    // Serial.write(hc12.read());    
    // get the message
    message = hc12.readString();    

    // @G0XX = attag blaster asking to join, XX=id of the blaster
    // @G1XX = attag blaster received game data and is ready to start, XX=id of the blaster

    // Reading messages from the blasters
    if (message.substring(0,2)=="@G0") {
       // adding blaster to the player list, but don't set on confirmed yet
       // sending game data
      
    } else if (message.substring(0,2)=="@G1") {
       // setting blaster to confirmed
       // sending ready status                
    } 
   }
}

The ID of the blaster could be transmitted in hex of course, this would shorten the string to 4 chars, 16 players (0-F) should be quite enough. There needs to be some sort of identification, so the server isn’t collecting any garbage from somewhere else, I guess the @G should do it, it isn’t rocket science here and anything in range at 433MHz would be some garage doors, weather stations and toy vehicles.