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.

ATTAG Server Box

Alright, forget about the power bank thing I mentioned a couple of days ago, it is pointless. The smartdisplay got an integrated battery management system for a 3.7V lithium cell. So more room means more capacity. I was looking for some pouch cells and found these, which already have the correct connector:

Wiring of the HC-12 antenna.

The bottom of the box got a tab on each side for the usage of a velcro strap, belt or rubber band to place the server on a pole or a tree.

Make sure to add the button before inserting the smartdisplay.

The holes are made for M3 6mm skrews. You need five 6mm in total and two 16mm.

I fixed the battery with double sided tape, same for the HC-12 module and used some two-component glue for the speaker. I guess this could be done a little bit more elegant but anyway. I hope the bending of the antenna doesn’t cause me too much trouble, otherwise I gotta fix that later on. Just try to not bend it that way to avoid communication trouble.

My printer was set a bit too low on Z, so it messed up the logo and the text, but this is some alpha stuff anyway. As you can see it is already running on battery. Double tapping the button switches the server to off.

So here is what you need:

And of course the STL files for printing:

attag server box STL files

OSDYLS power supply for the server

For the server unit I was looking for some small power bank to avoid constructing a battery management and 5V supply. I came across some keychain power banks and picked this one:

It can be opened easily without damaging anything, the inflated pouch pack doesn’t look too trustworthy though. Apart from that the 500mAh should deliver enough power for a 2-3h running time of the server. More than enough time to battle. (From my experience you are really tired after an hour unless your are trained 😉 )

Just need to remove the plug wires and connect to a charging USB socket and solder the 5V to a connector for the display.

OSDYLS – Game master / Host / Server

Since the beginning of the project it was clear that there would be some sort of server, which controls the aspects of the game, like game mode, play time, respawns, ammo and so on. While I was working on the kiln control project I was looking for some tiny touchscreens around and came across these cheap small smart displays on aliexpress. [ link: ESP32-2432S024C ]

Smart Display
Smart Display

Instantly I thought these would be perfect for usage as an OSDYLS host. Though there is a slight drawback, the creators of these displays left very few GPIO pins for external sensors and mods.

However I got myself one of these displays and checked if the HC-12 radio module could be connected to it, and indeed it can. There is also a speaker connector, micro SD slot, WiFi and I think even BT functionality on the board. The vendor provides a pack of examples for it, but I suggest using the [ link: esp32-smartdisplay ] package from github made by [ link: rzeldent ].

You need to get used to visual studio and platformio for it, for a noob like me that was a bit of a challenge but it is quite worth the step. If you are more used to programming with arduino IDE you proably get the libs working there as well of course.

OSDYLS Battle Server

Currently the thing isn’t doing much, I was just creating a simple gui for the beginning using squareline studio, which comes in quite handy for that.

The next step is to create a handshake between players and host, so the server knows who is part of the game. It would automatically fire up the match once all players joined.

Infernomat 1050 – a kiln control Eine Brennofensteuerung

I had this tiny kiln on the shelv for some decades already, my parents gave it to me, when I was a teenager but it never was used due to the lacking temperature control. I only melted some metal with it but never did ceramics. It is an UHLIG U15, I think they are still sold. Max temperature 1050°C, draws 900W, so at 230V little less than 4A.

Now due to 3D printing I thought of some neat ideas that could be done in combination with the kiln. So I deceided to build a control unit for it.

The cheap kiln usually don’t have any control units, with some luck you can buy some random on/off control for them but there is no real temperature control. 

My version got it and you can program different times and temperatures.

You need the following things:

  1. Thermocouple element, I got this one (not yet tested at high-temp) [ link ]
  2. An Arduino UNO or clone
  3. An Elegoo touch display shield [ link ]
  4. A relais which is able to switch with 5V and can handle the amps your kiln is drawing, such as the SLA-05VDC-SL-C
  5. One Adafruit MAX31855 board
  6. Plug, socket and wires for the high voltage stuff
  7. Some PSU for the Arduino (I looted the guts of an old Nokia charger.)
  8. An on/off switch (optional) 
  9. Stuff for soldering, some screws, cable shoes, 3D printer for the housing, all depending on how you are realising it

In order to have some of the GPIOs (I/O pins of the Arduino) left, you need to reduce some functionality of the display, so I cut the pins used for the SD card slot on the display.

Yes I know I could have done better, had no side cutter at hand 😉 These pins are required for the connection of the relais (Arduino GPIO PIN 10) and the thermocouple board (Arduino GPIO PINS 11 to 13)

So the wiring goes as follows:

I soldered the power from the old Nokia PSU to the backside of the Arduino, of course you can also use the power socket or the USB socket of the Arduino. Just a matter of available room. The high voltage power L1 connected to the relais on 1 target at 2, as marked on the picture above. If you choose connector 3 as target, the switching of the relais is inverted.

Now to the code which is doing the magic, I am sorry it is a total mess but I am too lazy to clean it up now. It just grew when I was testing the electronics. Of course it can be made way more elegant. Feel free to do so:

// libs for the display
#include <Elegoo_GFX.h>
#include <Elegoo_TFTLCD.h>
#include <TouchScreen.h>
#include <SPI.h>
#include <Wire.h>

// libs for the sensor
#include "Adafruit_MAX31855.h"

// display stuff, these settings are taken from Elegoo code
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RESET A4
#define YP A3
#define XM A2
#define YM 9
#define XP 8
#define TS_MINX 120
#define TS_MAXX 900
#define TS_MINY 70
#define TS_MAXY 920
#define STATUS_X 10
#define STATUS_Y 65

// pin definition for the MAX31855 sensor board, I removed them from the display, where they are used for the SD card
#define MAXDO   12
#define MAXCS   13
#define MAXCLK  11

// define the relais at pin 10 (also removed from the display)
const int relaisIN1 = 10;

// a couple of used variables
unsigned long myTime, newTime, startTime, startTwomin, stopTwomin, newTwomin;

// maxtemp,interval temp, interval time, max time
int parameters[4] = {1050, 100, 60, 30};
int runs = 0;
float rohtemp = 650;
int timepos = 10;
float steptemp = 0;
int looptime = 0;
int oldpos = 0;
int tempcounter = 0;
int simfactor = 1;
int finished = 0;

unsigned long graphcolor = 0xF800;

// #define BLUE    0x001F


float temp = 0;
float tempinc = 10;
float oldtemp = 0;
float deltemp = 0;
float loops = 0;
float numloops = 1;

// initialize the thermocouple sensor
Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);

// initialize the LCD display and touchscreen
Elegoo_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET);
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);

// define array of 14 buttons .. yeah I know only 13 are on the screen, too lazy to fix that ;-)
Elegoo_GFX_Button buttons[14];

void setup(void) {
  Serial.begin(9600);
  pinMode(relaisIN1, OUTPUT);
  delay(1000);
  while (!Serial) delay(1); // wait for Serial on Leonardo/Zero, etc
  delay(500);
  Serial.print("Initializing sensor...");
  if (!thermocouple.begin()) {
    Serial.println("ERROR.");
    while (1) delay(10);
  }
  Serial.println("DONE.");
  Serial.println(F("TFT LCD test"));
  Serial.println(graphcolor);

  tft.reset();
  temp = thermocouple.readCelsius();
  uint16_t identifier = 0x9341;
  tft.begin(identifier);
  tft.setRotation(2);
  tft.fillScreen(0x0000);
  tft.setTextSize(2);
  tft.setCursor(10, 2);   tft.print("mx Temp:");
  tft.setCursor(10, 20);  tft.print("iv Temp:");
  tft.setCursor(10, 38);  tft.print("iv Zeit:");
  tft.setCursor(10, 56);  tft.print("mx Zeit:");
  tft.setCursor(10, 153); tft.print("mx Temp:");
  tft.setCursor(10, 187); tft.print("iv Temp:");
  tft.setCursor(10, 222); tft.print("iv Zeit:");
  tft.setCursor(10, 257); tft.print("mx Zeit:");

  tft.setTextColor(0xB596);
  tft.setCursor(110, 2);  tft.print(parameters[0]); tft.print(" 'C");
  tft.setCursor(110, 20); tft.print(parameters[1]);  tft.print(" 'C");
  tft.setCursor(110, 38); tft.print(parameters[2]);  tft.print(" Min");
  tft.setCursor(110, 56); tft.print(parameters[3]);  tft.print(" Min");

  tft.setCursor(0, 0);
  buttons[1].initButton(&tft, 60, 90, 112, 30, 0xFFFF, 0xDDF2, 0xFFFF, "Keramik", 2);
  buttons[2].initButton(&tft, 180, 90, 112, 30, 0xFFFF,  0x94B2, 0xFFFF, "Schmelze", 2);
  buttons[3].initButton(&tft, 60, 125, 112, 30, 0xFFFF, 0xD340, 0xFFFF, "Glasur", 2);
  buttons[4].initButton(&tft, 180, 125, 112, 30, 0xFFFF, 0x8AC8, 0xFFFF, " ", 2);

  buttons[5].initButton(&tft, 150, 160, 53, 30, 0xFFFF, 0x2C5C, 0xFFFF, "-", 2);
  buttons[6].initButton(&tft, 210, 160, 53, 30, 0xFFFF, 0x2C5C, 0xFFFF, "+", 2);

  buttons[7].initButton(&tft, 150, 195, 53, 30, 0xFFFF, 0x2C5C, 0xFFFF, "-", 2);
  buttons[8].initButton(&tft, 210, 195, 53, 30, 0xFFFF, 0x2C5C, 0xFFFF, "+", 2);

  buttons[9].initButton(&tft, 150, 230, 53, 30, 0xFFFF, 0x2C5C, 0xFFFF, "-", 2);
  buttons[10].initButton(&tft, 210, 230, 53, 30, 0xFFFF, 0x2C5C, 0xFFFF, "+", 2);

  buttons[11].initButton(&tft, 150, 265, 53, 30, 0xFFFF, 0x2C5C, 0xFFFF, "-", 2);
  buttons[12].initButton(&tft, 210, 265, 53, 30, 0xFFFF, 0x2C5C, 0xFFFF, "+", 2);
  buttons[13].initButton(&tft, 60, 300, 112, 30, 0xFFFF, 0x0400, 0xFFFF, "START", 2);
  for (uint8_t b = 1; b < 14; b++) {
    buttons[b].drawButton();
  }
  steptemp = parameters[0];
}

#define MINPRESSURE 10
#define MAXPRESSURE 1000

// read temperature from sensor or do uncomment temp=temp+0.001*simfactor; for simulation
void gettemp() {
  temp = thermocouple.readCelsius();
  // temp = temp + 0.001 * simfactor;
}

void relais(int onoff) {
  switch (onoff) {
    case 0:  if (digitalRead(relaisIN1) == HIGH) {
        digitalWrite(relaisIN1, LOW);  // relais1 off
        // lower the temperature for simulation
        Serial.println("off");
        simfactor = -1;
      } break;

    case 1:   if (digitalRead(relaisIN1) == LOW) {
        digitalWrite(relaisIN1, HIGH);  // relais1 on
        // increase the temperature for simulation
        simfactor = +1;
        Serial.println("on");
      } break;
  }
}


void dostop() {  
  relais(0);
  graphcolor = 0x001F;
  tempcounter++;
  if (tempcounter == 1000) {
    tft.setCursor(150, 22); tft.setTextSize(2); tft.setTextColor(0x0000); tft.print(round(deltemp));
    tft.setCursor(150, 22); tft.setTextSize(2); tft.setTextColor(0x07FF); tft.print(round(temp));
    deltemp = temp;
    tempcounter = 0;
  }
  if (newTwomin > (startTwomin + 120000)) {    
    drawtemp();
    startTwomin = newTwomin;
  }
  if (temp < 30) {
    while (1) {}
  }
}

void drawtemp() {
  Serial.println("draw");
  tft.drawLine(timepos, 308 - (round(oldtemp / 5)), timepos + 1, 308 - (round(temp / 5)), graphcolor);
  timepos = timepos + 1;
  oldtemp = temp;
}

void intervall(long laufzeit) {
  newTime = millis();
  // set a start time
  startTime = millis();
  // start the two minute timer for the graph

  // repeat as long as the running time is lower than the interval time laufzeit*60*1000 (minutes to seconds to milliseconds)
  while ((millis() - startTime) < (laufzeit * 60 * 1000)) {
    // Serial.print("L");
    newTwomin = millis();
    gettemp();

    // draw the curve when the timer hits two minutes, then reset the timer
    if (newTwomin > (startTwomin + 120000)) {
      drawtemp();
      startTwomin = newTwomin;
    }

    // if the temperature gets higher than the set temperature shut down the heating
    if (temp > steptemp + 3) {
      relais(0);
    }

    // if the temperature gets too low enable the heating again
    if (temp < steptemp - 3) {
      relais(1);
    }
    printtemp();
  } // while end
  loops = loops + 1;

  // if there is only one heating intervall stop once it is done
  if (parameters[1] == 0) {
    finished = 1;
    timepos = 10;
    relais(0);
  }

  // once reached the biscuit firing go to the final temperature and hold it
  if (steptemp == rohtemp) {
    // heat up to the final temperature
    steptemp = parameters[0];
    // set the heating time for the final heating
    parameters[2] = parameters[3];
  } else {
    // increase the temperature to the next step
    steptemp = (steptemp + parameters[1]);
    relais(1);
  }

  // if temperature is higher than biscuit them set the current target to biscuit temperature
  if (steptemp > rohtemp && steptemp < parameters[0]) {
    steptemp = rohtemp;
  }


  if (temp > parameters[0] - 5) {
    relais(0);
    timepos = 10;
    dostop();
  }


}

// this function sets the presets
void setparameters(int x, int y, int color, int mt, int it, int iz, int mz) {
  tft.setTextColor(0x0000);
  tft.setCursor(x, y);      tft.print(parameters[0]); tft.print(" 'C");
  tft.setCursor(x, y + 18); tft.print(parameters[1]); tft.print(" 'C");
  tft.setCursor(x, y + 36); tft.print(parameters[2]); tft.print(" Min");
  tft.setCursor(x, y + 54); tft.print(parameters[3]); tft.print(" Min");

  parameters[0] = mt;
  parameters[1] = it;
  parameters[2] = iz;
  parameters[3] = mz;

  tft.setTextColor(color);
  tft.setCursor(x, y);      tft.print(mt); tft.print(" 'C");
  tft.setCursor(x, y + 18); tft.print(it); tft.print(" 'C");
  tft.setCursor(x, y + 36); tft.print(iz); tft.print(" Min");
  tft.setCursor(x, y + 54); tft.print(mz); tft.print(" Min");
}

// this function updates the values time and temperature when pressing +/-
void incdec(int x, int y, int color, int orig, int factor) {
  tft.setCursor(x, y); tft.setTextColor(0x0000); tft.print(parameters[orig]); tft.print(" 'C");
  tft.setCursor(x, y); tft.setTextColor(0x0000); tft.print(parameters[orig]); tft.print(" Min");
  newTime = millis();

  // wenn touchscreen länger gedrückt beschleunige auf 10er Schritte, ansonsten +/- 1

  if (newTime - myTime < 160) {
    parameters[orig] = parameters[orig] + 10 * factor;
  } else {
    parameters[orig] = parameters[orig] + 1 * factor;
  }
  if (parameters[orig] < 0) parameters[orig] = 0;


  tft.setCursor(x, y); tft.setTextColor(color); tft.print(parameters[orig]);
  if (orig > 1) {
    tft.print(" Min");
  } else {

    tft.print(" 'C");
  }
  myTime = millis();
}

void printtemp() {
  tempcounter++;
  if (tempcounter == 1000) {
    // print the old temp with black
    tft.setCursor(150, 22); tft.setTextSize(2); tft.setTextColor(0x0000); tft.print(round(deltemp));
    // print the current temp
    tft.setCursor(150, 22); tft.setTextSize(2); tft.setTextColor(0xB596); tft.print(round(temp));
    deltemp = temp;
    tempcounter = 0;
  }
}


void loop(void) {

  // some copied routines from elegoo for the touch control and buttons
  digitalWrite(13, HIGH);
  TSPoint p = ts.getPoint();
  digitalWrite(13, LOW);
  pinMode(XM, OUTPUT);
  pinMode(YP, OUTPUT);

  if (p.z > MINPRESSURE && p.z < MAXPRESSURE) {
    p.x = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
    p.y = (tft.height() - map(p.y, TS_MINY, TS_MAXY, tft.height(), 0));
  }

  // go thru all the buttons, checking if they were pressed
  for (uint8_t b = 0; b < 14; b++) {
    if (buttons[b].contains(p.x, p.y)) {
      buttons[b].press(true);  // tell the button it is pressed
    } else {
      buttons[b].press(false);  // tell the button it is NOT pressed
    }
  }

  if (runs == 1) {
    // clear screen
    tft.fillScreen(0x0000);
    // draw grid
    tft.setTextColor(0xEEEE);
    tft.setCursor(2, 22); tft.print("Temperatur:");

    for (uint8_t i = 1; i < 27; i++) {
      tft.drawLine(10, 38 + i * 10, 230, 38 + i * 10, 0x5AEB);
      if ((i - 1) % 3 == 0) {
        tft.drawLine(0 + i * 10, 48, 0 + i * 10, 309, 0x00EB);
      } else {
        tft.drawLine(0 + i * 10, 48, 0 + i * 10, 300, 0x00EB);
      }
      tft.setTextSize(1); tft.setTextColor(0xB596);
      tft.setCursor(0, 30 + i * 10);
      tft.print(1350 - i * 50);
    }


    // draw coords X axis, sadly no linear spacing so one by one
    tft.setCursor(8, 310); tft.print("0");
    tft.setCursor(35, 310); tft.print("60");
    tft.setCursor(62, 310); tft.print("120");
    tft.setCursor(92, 310); tft.print("180");
    tft.setCursor(121, 310); tft.print("240");
    tft.setCursor(152, 310); tft.print("300");
    tft.setCursor(182, 310); tft.print("360");
    tft.setCursor(212, 310); tft.print("420");

    if (parameters[1] > 0) {
      numloops = ceil(rohtemp / parameters[1]);
    }

    // if there are no more heating intervals go to the main temperature, otherwise go to next loop
    if (parameters[1] == 0 && loops < 1) {
      steptemp = parameters[0];
      parameters[2] = parameters[3];
    } else if (parameters[1] > 0 && loops < numloops) {
      steptemp = parameters[1];
    }

    // relais on, start heating
    relais(1);

    // go to the heating loops
    runs = 2;
  }

  if (runs == 2) {
    // since there is no rtc, create a timer
    if (startTime == 0) {
      startTime = millis();
    }
    // zwei Minuten Timer starten
    if (startTwomin == 0) {
      startTwomin = millis();
    }
    // read temperature from sensor (or simulate)
    gettemp();

    // for how long is the program running already?
    newTime = millis();
    newTwomin = millis();

    // draw graph all two minutes
    if (newTwomin > (startTwomin + 120000)) {
      drawtemp();
      startTwomin = newTwomin;
    }

    // if destination temperature reached start the heating loop
    if (temp > steptemp - 1 && parameters[1] > 0 && loops < numloops + 1) {
      intervall(parameters[2]);                                   // loop time
    } else if (temp > parameters[0] - 1 && loops < 1) {
      intervall(parameters[3]);                                   // final time
    }
    // draw cooling graph in blue
    if (numloops == loops) {
      graphcolor = 0x001F;
    }
    printtemp();
  }  else {

    for (uint8_t b = 1; b < 14; b++) {
      if (buttons[b].justReleased()) {
        buttons[b].drawButton();       // unpressed
      }

      if (buttons[b].justPressed()) {
        buttons[b].drawButton(true);  // pressed

        switch (b) {
          // presets
          case 1: setparameters(110, 2, 0xB596, 1050, 100, 60, 30);    break;           // Keramik
          case 2: setparameters(110, 2, 0xB596, 250, 0, 0, 30);        break;           // Schmelze
          case 3: setparameters(110, 2, 0xB596, 1050, 0, 0, 30);       break;           // Glasur
          case 5:  incdec(110, 2, 0xB596, 0, -1);                       break;          // if (parameters[0]<30)   parameters[0]=30;   - maxtemp
          case 6:  incdec(110, 2, 0xB596, 0, 1);                       break;           // if (parameters[0]>1050) parameters[0]=1050; + maxtemp
          case 7:  incdec(110, 20, 0xB596, 1, -1);                      break;          // if (parameters[1]<30)   parameters[1]=30;   - ivtemp
          case 8:  incdec(110, 20, 0xB596, 1, 1);                      break;           // if (parameters[1]>1050) parameters[1]=1050; + ivtemp
          case 9:  incdec(110, 38, 0xB596, 2, -1);                      break;          // - iv Time
          case 10: incdec(110, 38, 0xB596, 2, 1);                      break;           // + iv Time
          case 11: incdec(110, 56, 0xB596, 3, -1);                      break;          // - max Time
          case 12: incdec(110, 56, 0xB596, 3, 1);                      break;           // + max Time
          case 13: runs = 1;                                          break;            // start pressed
        }

        // little delay to prevent uncontrolled ui reactions
        delay(100);
      }
    }
  }

}

If you get some weird results from the thermocouple, make sure you have it wired correctly.    Try switching the cables if the numbers look weird.

There is no ° character in the lib and I am too lazy to embed a new font. 😉

Yes I know the color bleeding in the 3D print sucks. Who cares, doesn’t need to be a beauty.

Questions? Just ask.