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;


Leave a Reply

Your email address will not be published. Required fields are marked *