ATTAG – changing the address of an OLED display

In the requirements I mentioned a 128×32 pixel OLED in the optional stuff, if you are planning to use more than one display per blaster you will have to change the address of the 128×64 pixel OLED unless you get a 128×32 pixel OLED with changeable addresses.

This needs a bit more advanced soldering skills with SMD, which I never had to use before. However I succeeded with my first approach even though it looks pretty ugly. 😉

Remember this absolutely is NOT required for the functionality of the blasters, it is just a neat addon. But if you want to have it you should get a hot air soldering station, to remove the SMD resistor from its current place.

The resistor is a 4.7K, just in case you kill it during the process. You need to remove it from position R11 (address 0x3C) and solder it to position R12 (address 0x3D).

ATTAG – IR laser measurings

As I wrote in a former post, I would test the laser power of the small IR laser. I got myself a tiny HWLPM-Mini 10W. It isn’t very accurate and usually measures up to 7% higher than the real power. The smallest step is 1mW and pointing the IR laser at it, powered with 3.3V coming from a D1Mini, it leaves the display of the laser-meter at 0.000W.

While the max voltage of the laser is 3.5V , the specs look pretty realistic with 0.6-0.9mW. So at 3.3V it probably will be around 0.7-0.8mW.

You may say the laser-meter is broken, no it actually isn’t. I tested two laser pointers at it and was a bit shocked, the blue one ends up at 28mW after 20 seconds, the red one at 6mW, so they are both far over the 1mW rate they are sold at and the blue is even far above the 5mW labeled sticker on it. So watch your eyes when using these.

Conclusion: The IR laser might actually be an option.

ATTAG – Required components

This post will be updated from time to time, the components might change, so it is up to you if you want to experiment on your own while in the “pre-release” state or wait for a release state. All items get a color status:

yellow = component might change blue = component is final choice, only chance of change is if insurmountable obstacles appear

The prices are those I payed for them and of course depend on the source where you get them. I am not related to any of these shops and I don’t get any bonus or affiliate payments, they are just a suggestion.

NOTE: Not in the list are basic items you will need for sure such as wires, solder, some glue and filament for the printed parts. For the JST connectors I don’t provide a link since there are offers with collections that make more sense including or not including tools etc.

Minimum requirements for ONE blaster, minimum means no audio or visual feedback and no blaster ID, which makes all of this pretty useless but you can fire at a target and the target gives feedback. Actually you just need the IR sender and receiver, one resistor plus the LOLIN S3. But hey, that won’t be fun at all.

qtycomponentdate of change/addpriceurl to shop
1LOLIN S3 mini or clone2024-03-075,47 EURlink
1LD 274-3 IR LED2024-02-030,99 EURlink
11000 uF capacitor2024-02-030,95 EURlink
1TIP 120 transistor2024-02-030,45 EURlink
21N4148 diode2024-02-030,06 EURlink
13,3KOhm resistor2024-02-030,05 EURlink
10,5Ohm resistor2024-02-030,23 EURlink
1TSOP 31238 IR receiver2024-02-030,85 EURlink
15V power bank2024-02-03
1micro switch2024-02-030,17 EURlink

Suggested additional parts for ONE blaster. This is the stuff YOU WANT to get in addition to the minimum requirements.

qtycomponentdate of change/addpriceurl to shop
1128×64 I2C OLED2024-02-031,57 EURlink
14Ohm 3W speaker2024-02-030,82 EURlink
2socket for the S3 mini2024-02-030,16 EURlink
1trimmer 100Ohm2024-02-030,30 EURlink
1PAM 8302A amp2024-03-011,60 EURlink
24 pin JST XH 2,54mm socket2024-02-03
24 pin JST XH 2,54mm
82 pin JST XH 2,54mm
82 pin JST XH 2,54mm
43 pin JST XH 2,54mm
43 pin JST XH 2,54mm

1mainboard PCB2024-02-03
1achromat lens2024-02-03

Additional blaster components for maximum fun 😉

qtycomponentdate of change/addpriceurl to shop
3470 Ohm resistor2024-02-030,81 EURlink
14 pin magnetic
2024-02-032,02 EURlink
1MB85RC256V FRAM2024-02-032,48 EURlink
25mm LED red2024-02-03
1WS2812B 5V
LED strip
1128×32 OLED display2024-02-031,20 EURlink

Components for the server

qtycomponentdate of change/addpriceurl to shop
sunton display
WITH touch
2024-02-0310,80 EURlink
14Ohm 3W speaker2024-02-030,82 EURlink
1rechargable battery2024-02-034,71EURlink

Update 2024-03-07:

Since the S3 Mini causes massive trouble with many libs I probably have to replace it again by something else, I ordered some ESP32 WROOM dual core boards to check them out and to compare their behaviour to the bitching of the S3 Mini.

I hope the S3 is still an option in the future (damn, I got ten of them 😉 ) but at the moment it suffers from incompatibility because the development of many important libs is still not aware of it or just doesn’t want to be aware of it.

ATTAG – don’t waste the light?

While I was watching the IR LED with some old dashcam I noticed that still a lot of the light, which is not in the main cone of light, is unused plus the lens is 31mm instead of the ideal 47,7mm. So the best choice would be a lens about 50mm with the focal length of ~135mm. However this would make a big gun barrel and usually those bigger lenses have a longer focal length so it won’t match the LED again with its 20° angle. Also the spot diameter would be bigger though with less incrementation at distance.

So instead I’ll try to get more light from the LED and push it into a smaller angle with these reflectors:

However they don’t have a 5mm bore so the LED needs to be fixed behind it. Don’t know if there will be any advantage at all but I’ll try. Using two LEDs instead of one is still an idea but in first tests this turned into a display of two spots.

Update: Test with and without reflector, with and without achromat. It looks like the reflector is of no real use. However, these tests are made without the peak amperage of 1A which finally will be used for some microseconds per shot. So the LED emission from the LED still might be different then.

And this is what happens when using two diodes while hoping that the intensity can be doubled. It is not working with lens usage, it probably would requires some diffusor or whatever, I don’t know. I am open for ideas 😉

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:


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.


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

Besides from the libs you also need the board definition for the display esp32-2432S024C from here, link:

// 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
// 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(, "attag");
    if (ptr != nullptr) {
      if (oldid<atoi(ptr)) oldid=atoi(ptr);
  return (oldid+1);  

void startgame() {
  Serial.print("starting game..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 ="/");
  // logfile for the game stored on the SD card
  // logFile ="fileName", FILE_APPEND);
  char filename[] = "00000000.csv";
  sprintf(filename,"/attag%02d.csv", getnewfileid(path,0));

  // open the logfile for writing
  logFile =, 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


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

    // read the incoming message and out it into a byte 
    byte received =;

    //  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

      // 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) {

      // set the player status to 1,  unless it already was on 1
      // actually this is an unused variable
      if (players[playerid]!=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      

    } else if (check==3 && playerstatus==1)  {
      // set the color of the display LED RGB

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

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

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

    // calculate the status bar progress percentage and update it
    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");

  // blink the LED
  if (currentMillis - previousMillis >= ledinterval) {
    previousMillis = currentMillis;  
    if (ledswitch == 0) {      
      smartdisplay_led_set_rgb(colors[0], colors[1], colors[2]);  
    } else {
      smartdisplay_led_set_rgb(0, 0, 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  

    // 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:");


void setup()
    // Serial.begin(9600);
   // check if a SD card is abailable 
   if (SD.begin(SD_CS))  {
    auto disp = lv_disp_get_default();
    lv_disp_set_rotation(disp, LV_DISP_ROT_90);

    // hide the red SD card icon if SD card is available
    if (SDready==1) { 

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;


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:

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.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()) {