/**********************************************************************
* Filename : CM-V6 , Cocktailmixer-V6
* version : 0.1
* Description : Erste Implementierung der Basisfunktionen
* Auther : MiHa
* Modification: 2020/07/11
**********************************************************************/
/**********************************************************************
Modhist
=======
Version: 0.1
Neu:
- Wifi Station Mode
- Feste SSID und PW
- LCD 16x2 Anbindung
- Rotary Encoder Anbindung
- Statemachine
- STS_IDLE
- STS_PUMP_ON
- STS_PUMP_OFF
- STS_INIT
- STS_MIX_SELECT
Änderung:
Korrektur:
----------------------------------------------------------------------
**********************************************************************/
/* Verwendete Libs
LCD : https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library
LCD Char generator : https://maxpromer.github.io/LCD-Character-Creator/
LCD menu: https://github.com/forntoh/LcdMenu
Rotary Encoder: https://github.com/igorantolic/ai-esp32-rotary-encoder
*/
// Includes
#include <LiquidCrystal_I2C.h> //LCD
#include <Wire.h> //I2C
#include <WiFi.h> //WiFi
#include <AiEsp32RotaryEncoder.h> //Rotary Encoder
// Definitionen
//Allgemein
#define WOKWI // Wokwi-Simulation
#define CM_VERSION "0.1" // Softwareversion
// IOs
#define LED_BUILTIN 2
#define P1 4 //Pumpe 1
#define P2 5 //Pumpe 2
#define P3 12 //Pumpe 3
#define P4 27 //Pumpe 4
#define P5 26 //Pumpe 5
#define P6 25 //Pumpe 6
#define ROTARY_ENCODER_A_PIN 32 //CLK
#define ROTARY_ENCODER_B_PIN 21 // DT
#define ROTARY_ENCODER_BUTTON_PIN 33 //SW-Button
#define ROTARY_ENCODER_VCC_PIN -1 /* 27 put -1 of Rotary encoder Vcc is connected directly to 3,3V; else you can use declared output pin for powering rotary encoder */
//depending on your encoder - try 1,2 or 4 to get expected behaviour
//#define ROTARY_ENCODER_STEPS 1
//#define ROTARY_ENCODER_STEPS 2
#define ROTARY_ENCODER_STEPS 4
// I2C
#define SDA 13 //Define SDA pins
#define SCL 14 //Define SCL pins
// LCD
#define LCD_I2C_ADDRESS 0x27 //I2C Address
#define LCD_NO_LINES 4
#define LCD_NO_CHARS 20
//LCD-Positionen
#define LCD_POS_ROW_TITLE 0
#define LCD_POS_COL_TITLE 0
#define LCD_POS_ROW_SUBTITLE 1
#define LCD_POS_COL_SUBTITLE 0
#define LCD_POS_ROW_STS 2
#define LCD_POS_COL_STS 0
#define LCD_POS_ROW_PROGRESS 3
#define LCD_POS_COL_PROGRESS 0
#define LCD_POS_ROW_WIFI_STS 0
#define LCD_POS_COL_WIFI_STS 19
#define LCD_POS_ROW_IP_Adr 1
#define LCD_POS_COL_IP_Adr 0
// ENUMS
//LCD custom chars
enum symbols {
SYM_WIFI,
SYM_BATT_EMPTY,
SYM_BATT_HALF,
SYM_BATTFULL,
SYM_GLASS
};
//States
typedef enum {
STS_START,
STS_INIT,
STS_IDLE,
STS_PUMP_ON,
STS_PUMP_OFF,
STS_MIX_SELECT
} state_p;
//Events
typedef enum {
EVT_INIT_COMPLETE,
EVT_PUMPTIMER,
EVT_BUTTON,
EVT_UP,
EVT_DOWN
} event_p;
// Zutaten
typedef enum {
COLA,
RUM,
VODKA,
ORANGENSAFT,
INGREDIENTS_MAX
}zutaten;
// Classes / Instances
// LCD
LiquidCrystal_I2C lcd(LCD_I2C_ADDRESS, LCD_NO_CHARS, LCD_NO_LINES);
// Consts
//HW
const int p_pin[7] = { 0, 4, 5, 12, 27, 26, 25 }; // Pumpen HW- Pins
//WiFi
#ifdef WOKWI
const char* ssid_Router = "Wokwi-GUEST"; //Enter the router name
const char* password_Router = ""; //Enter the router password
#else
const char* ssid_Router = "FRITZ!Box MM2"; //Enter the router name
const char* password_Router = "32277652857271766098"; //Enter the router password
#endif
//Pump
const int factor_ms_per_ml = 333; // Umrechnungsfaktor Pumpeleistung in ms pro Milliliter
// Variablen
bool t10ms = false; //Timer 10 Millisekunden
int led_state = LOW; // the current state of LED
int drink_capacity = 200; // aktuelle, maximale Glas Füllmenge in ml
state_p acutal_state = STS_INIT; // globale Variable, die den akutellen Status der Pumpe repräsentiert
state_p new_state = STS_IDLE; // globale Variable, die den neuen Status der Pumpe repräsentiert
word pumptimer = 0; // Pumptimer
int lcd_update_counter = 0; // LCD Refresh Delay Counter
int ingredient_volume = 0; //Soll-Füllmenge aktuelle Zutat
bool init_statemachine = false; // Initiale Einstellungen der Statemachine
int selected_mix = 0; //Akutell ausgewählter Mix
// Structs
//Pumpe
typedef struct
{
byte hwpin;
zutaten loaded_ingredient;
} pumpinfo;
pumpinfo pump[6]; //PumpenStruct-Array
//Zutaten
typedef struct
{
char name[21]; //Name
float volalc; //Volumenalkohol
} ingredientinfo;
ingredientinfo ing[INGREDIENTS_MAX] = {
{ "Coca Cola", 0.0 },
{ "Havanna Club", 40.0 },
{ "Three Sixty", 37.5 },
{ "O-Saft", 0.0 }
};
//Rezepte
typedef struct
{
int ingredient; //ENUM
int volume; //Zutatenvolumen in 100ml Fertigmix
} ingredientlist;
typedef struct
{
char name[21];
ingredientlist recipe_ing[6];
int ingredientscount; //Anzahl an Zutaten im Mix
} recipeinfo;
recipeinfo recipe[4]; //Rezepte Struct-Array
// Timer
/* create a hardware timer */
hw_timer_t* timer = NULL;
// Timer Interrupt
void IRAM_ATTR onTimer() {
t10ms = true;
}
// Rotary Encoder
//instead of changing here, rather change numbers above
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS);
void rotary_onButtonClick() {
transitionhandler(EVT_BUTTON);
}
void rotary_loop()
{
static int last_rotary_value = 0;
static int current_rotary_value = 0;
//dont print anything unless value changed
if (rotaryEncoder.encoderChanged())
{
//Serial.print("Value: ");
//Serial.println(rotaryEncoder.readEncoder());
current_rotary_value = rotaryEncoder.readEncoder();
if (last_rotary_value < current_rotary_value)
{
transitionhandler(EVT_UP);
last_rotary_value = current_rotary_value;
}
if (last_rotary_value > current_rotary_value)
{
transitionhandler(EVT_DOWN);
last_rotary_value = current_rotary_value;
}
}
if (rotaryEncoder.isEncoderButtonClicked()) {
rotary_onButtonClick();
}
}
void IRAM_ATTR readEncoderISR() {
rotaryEncoder.readEncoder_ISR();
}
// Board Setup
void setup() {
// Init I/Os
pinMode(LED_BUILTIN, OUTPUT);
pump[0].hwpin = 4;
pump[1].hwpin = 5;
pump[2].hwpin = 12;
pump[3].hwpin = 27;
pump[4].hwpin = 26;
pump[5].hwpin = 25;
pump[0].loaded_ingredient = COLA;
pump[1].loaded_ingredient = RUM;
pump[2].loaded_ingredient = VODKA;
pump[3].loaded_ingredient = ORANGENSAFT;
for (byte i = 0; i < (sizeof(pump)/(sizeof(pump[0])));++i)
{
pinMode(pump[i].hwpin, OUTPUT); // Deklariere I/O als Output
digitalWrite(pump[i].hwpin, LOW); // Output of low setzen
}
// Init LCD
Wire.begin(SDA, SCL); // attach the IIC pin
#ifdef WOKWI
lcd.init(); // LCD driver initialization in WOKWI
#else
lcd.begin(); // LCD driver initialization in real
#endif
lcd.backlight(); // Activate the backlight
// Create and store custom chars
byte lcd_custom_chars_WIFI[] = { 0x00, 0x0E, 0x11, 0x04, 0x0A, 0x00, 0x04, 0x00 };
byte lcd_custom_chars_BATT_EMPTY[] = { 0x0E, 0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F };
byte lcd_custom_chars_BATT_HALF[] = { 0x0E, 0x1F, 0x11, 0x11, 0x11, 0x1F, 0x1F, 0x1F };
byte lcd_custom_chars_BATTFULL[] = { 0x0E, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F };
byte lcd_custom_chars_GLASS[] = { 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x04, 0x1F };
lcd.createChar(SYM_WIFI, lcd_custom_chars_WIFI);
lcd.createChar(SYM_BATT_EMPTY, lcd_custom_chars_BATT_EMPTY);
lcd.createChar(SYM_BATT_HALF, lcd_custom_chars_BATT_HALF);
lcd.createChar(SYM_BATTFULL, lcd_custom_chars_BATTFULL);
lcd.createChar(SYM_GLASS, lcd_custom_chars_GLASS);
// Startscreen
lcd.setCursor(LCD_POS_COL_TITLE, LCD_POS_ROW_TITLE); // Move the cursor to column 0,row 0
lcd.print("CM-V6 "); // The print content is displayed on the LCD
lcd.print(CM_VERSION); // The print content is displayed on the LCD
// Init Timer
/* Use 1st timer of 4 */
/* 1 tick take 1/(80MHZ/80) = 1us so we set divider 80 and count up */
timer = timerBegin(0, 80, true);
/* Attach onTimer function to our timer */
timerAttachInterrupt(timer, &onTimer, true);
/* Set alarm to call onTimer function every second 1 tick is 1us
=> 1 second is 1000000us */
/* Repeat the alarm (third parameter) */
timerAlarmWrite(timer, 1000, true);
/* Start an alarm */
timerAlarmEnable(timer);
// Init serial
Serial.begin(115200); //Anweisung der Vorbereitung des Seriellen Monitors
// Init WiFi
delay(2000);
Serial.println("WiFi Setup start");
WiFi.begin(ssid_Router, password_Router);
Serial.println(String("Connecting to ") + ssid_Router);
lcd.setCursor(LCD_POS_COL_WIFI_STS, LCD_POS_ROW_WIFI_STS); // Move the cursor to column 0,row 0
lcd.write(SYM_WIFI);
lcd.setCursor(LCD_POS_COL_WIFI_STS, LCD_POS_ROW_WIFI_STS); // Move the cursor to column 0,row 0
lcd.blink();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected, IP address: ");
Serial.println(WiFi.localIP());
Serial.println("Setup End");
lcd.noBlink();
//String str_ip = IpAddress2String(WiFi.localIP());
//int pos_ip_lcd = LCD_NO_CHARS - str_ip.length();
lcd.setCursor(LCD_POS_COL_IP_Adr, LCD_POS_ROW_IP_Adr); // Move the cursor to column 0,row 0
lcd.print(WiFi.localIP()); // The print content is displayed on the LCD
delay(3000);
lcd_clear_line(lcd, LCD_POS_ROW_IP_Adr);
// Init Rotary
//we must initialize rotary encoder
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
//set boundaries and if values should cycle or not
//in this example we will set possible values between 0 and 1000;
bool circleValues = false;
rotaryEncoder.setBoundaries(0, 1000, circleValues); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
/*Rotary acceleration introduced 25.2.2021.
* in case range to select is huge, for example - select a value between 0 and 1000 and we want 785
* without accelerateion you need long time to get to that number
* Using acceleration, faster you turn, faster will the value raise.
* For fine tuning slow down.
*/
//rotaryEncoder.disableAcceleration(); //acceleration is now enabled by default - disable if you dont need it
rotaryEncoder.setAcceleration(250); //or set the value - larger number = more accelearation; 0 or 1 means disabled acceleration
// Init Complete
transitionhandler(EVT_INIT_COMPLETE);
}
// Main Loop
void loop()
{
// 1s Timer Interrupt
if (t10ms == true)
{
t10ms = false; //Reset Timer -Flag
if (pumptimer > 0) {
--pumptimer; // decrement Pump-timer
} else {
transitionhandler(EVT_PUMPTIMER);
}
/* // Wifi Status
if (WiFi.status() != WL_CONNECTED)
{
lcd.setCursor(15,0); // Move the cursor to column 0,row 0
lcd.write(SYM_WIFI);
lcd.setCursor(15,0); // Move the cursor to column 0,row 0
lcd.blink();
}
else
{
lcd.setCursor(15,0); // Move the cursor to column 0,row 0
lcd.write(SYM_WIFI);
lcd.setCursor(15,0); // Move the cursor to column 0,row 0
lcd.noBlink();
}
lcd.setCursor(0,1); // Move the cursor to column 0,row 0
lcd.write(SYM_GLASS);
lcd.print(":");
lcd.print(drink_capacity);
lcd.print("ml");
*/
}
// Events
// State machine
statemachine();
// Rotary
//in loop call your custom function which will process rotary encoder values
rotary_loop();
}
//Functions Statemachine
void statemachine()
{
memset(recipe[0].name,0,sizeof(recipe[0].name));
strcpy(recipe[0].name, "Cuba Libre");
recipe[0].ingredientscount = 2;
recipe[0].recipe_ing[0].ingredient = COLA;
recipe[0].recipe_ing[0].volume = 80;
recipe[0].recipe_ing[1].ingredient = RUM;
recipe[0].recipe_ing[1].volume = 20;
memset(recipe[1].name,0,sizeof(recipe[1].name));
strcpy(recipe[1].name, "Rum-Cola");
recipe[1].ingredientscount = 2;
recipe[1].recipe_ing[0].ingredient = COLA;
recipe[1].recipe_ing[0].volume = 60;
recipe[1].recipe_ing[1].ingredient = RUM;
recipe[1].recipe_ing[1].volume = 40;
memset(recipe[2].name,0,sizeof(recipe[2].name));
strcpy(recipe[2].name, "Vodka-O");
recipe[2].ingredientscount = 2;
recipe[2].recipe_ing[0].ingredient = VODKA;
recipe[2].recipe_ing[0].volume = 20;
recipe[2].recipe_ing[1].ingredient = ORANGENSAFT;
recipe[2].recipe_ing[1].volume = 40;
memset(recipe[3].name,0,sizeof(recipe[0].name));
strcpy(recipe[3].name, "Rum pur");
recipe[3].ingredientscount = 1;
recipe[3].recipe_ing[0].ingredient = RUM;
recipe[3].recipe_ing[0].volume = 100;
init_statemachine = true; // Init Statemachine abgeschlossen
switch (new_state) {
case STS_MIX_SELECT:
{
static int last_selected_mix = 0;
// Begin-State
if (acutal_state != new_state) {
//Set State
acutal_state = new_state;
serial_log(millis(), "New State: STS_MIX_SELECT");
//Screen Update
lcd_clear_line(lcd, LCD_POS_ROW_SUBTITLE);
lcd_clear_line(lcd, LCD_POS_ROW_STS);
lcd.setCursor(2, LCD_POS_ROW_SUBTITLE); // Move the cursor to column 0,row 0
lcd.print("Select your Mix");
lcd.setCursor(0, LCD_POS_ROW_STS); // Move the cursor to column 0,row 0
lcd.print(recipe[selected_mix].name);
serial_log(millis(), recipe[selected_mix].name);
lcd_clear_line(lcd, LCD_POS_ROW_PROGRESS);
lcd.setCursor(0, LCD_POS_ROW_PROGRESS); // Move the cursor to column 0,row 0
lcd.print("< [Press Button] >");
}
// While Sate
if (acutal_state == new_state)
{
//Action
//Screen Update
if (selected_mix != last_selected_mix)
{
//Screen Update
lcd_clear_line(lcd, LCD_POS_ROW_SUBTITLE);
lcd_clear_line(lcd, LCD_POS_ROW_STS);
lcd.setCursor(2, LCD_POS_ROW_SUBTITLE); // Move the cursor to column 0,row 0
lcd.print("Select your Mix");
lcd.setCursor(0, LCD_POS_ROW_STS); // Move the cursor to column 0,row 0
lcd.print(recipe[selected_mix].name);
serial_log(millis(), recipe[selected_mix].name);
lcd_clear_line(lcd, LCD_POS_ROW_PROGRESS);
lcd.setCursor(0, LCD_POS_ROW_PROGRESS); // Move the cursor to column 0,row 0
lcd.print("< [Press Button] >");
last_selected_mix = selected_mix;
}
}
// State-End
break;
}
case STS_IDLE:
{
// Begin-State
if (acutal_state != new_state) {
//Set State
acutal_state = new_state;
serial_log(millis(), "New State: STS_IDLE");
//Screen Update
lcd_clear_line(lcd, LCD_POS_ROW_SUBTITLE);
lcd_clear_line(lcd, LCD_POS_ROW_STS);
lcd.setCursor(5, LCD_POS_ROW_SUBTITLE); // Move the cursor to column 0,row 0
lcd.print("Mix it now!");
lcd_clear_line(lcd, LCD_POS_ROW_PROGRESS);
lcd.setCursor(3, LCD_POS_ROW_PROGRESS); // Move the cursor to column 0,row 0
lcd.print("[Press Button]");
}
// While Sate
if (acutal_state == new_state) {
//Action
//Screen Update
}
// State-End
break;
}
case STS_PUMP_ON:
{
//Variablen
static int pumpprogress_last; // Letzter, prozentualer Fortschrittswert
static int pumpprogress_now; // aktueller, prozentualer Fortschrittswert
static int pumpduration; // Pump Laufzeit, wird anhand Zutaten-Füllmenge berechnet
// Begin-State
if (acutal_state != new_state) {
//Set State
acutal_state = new_state;
serial_log(millis(), "New State: STS_PUMP_ON");
pumpprogress_last = 0;
pumpprogress_now = 0;
pumpduration = 0;
/*todo*/ ingredient_volume = (recipe[selected_mix].recipe_ing[0].volume * drink_capacity) / 100 ; //Berechnug Zutatenvolumen
char buffer[100];
/*todo*/ sprintf(buffer, " %d ml of %s calculated for %s ", ingredient_volume, ing[recipe[selected_mix].recipe_ing[0].ingredient].name, recipe[selected_mix].name);
serial_log(millis(), buffer);
//Action
byte selected_pump;
/*todo*/ while (pump[selected_pump].loaded_ingredient != recipe[selected_mix].recipe_ing[0].ingredient) //Passende Pumpe zur Zutat suchen
{
++selected_pump;
}
digitalWrite(pump[selected_pump].hwpin, HIGH);
pumpduration = (factor_ms_per_ml * ingredient_volume) / 10; // Pump Laufzeit in 10ms Schritten, wird anhand Zutaten-Füllmenge berechnet
pumptimer = pumpduration; // Timer der Pumpe
//Screen Update
lcd_clear_line(lcd, LCD_POS_ROW_SUBTITLE);
lcd.setCursor(1, LCD_POS_ROW_SUBTITLE); // Move the cursor to column 0,row 0
lcd.print("Mix in Progress...");
lcd_clear_line(lcd, LCD_POS_ROW_STS);
lcd.setCursor(LCD_POS_COL_STS, LCD_POS_ROW_STS); // Move the cursor to column 0,row 0
lcd.print(ing[pump[selected_pump].loaded_ingredient].name);
lcd_progressbar(lcd, LCD_POS_COL_PROGRESS, LCD_POS_ROW_PROGRESS, pumpprogress_now);
}
// While-State
if (acutal_state == new_state) {
//Action
//Calc Progress
pumpprogress_now = (pumpduration - pumptimer) / (pumpduration / 100);
//Screen Update
if (pumpprogress_now >= (pumpprogress_last + 10)) {
String thisString = String(pumpprogress_now);
serial_log(millis(), thisString);
lcd_progressbar(lcd, LCD_POS_COL_PROGRESS, LCD_POS_ROW_PROGRESS, pumpprogress_now);
pumpprogress_last = pumpprogress_now;
}
}
// State-End
break;
}
case STS_PUMP_OFF:
{
// Begin-State
if (acutal_state != new_state) {
//Set State
acutal_state = new_state;
serial_log(millis(), "New State: STS_PUMP_OFF");
//Action
/*todo*/ digitalWrite(p_pin[1], LOW);
//Screen Update
lcd_clear_line(lcd, LCD_POS_ROW_STS);
lcd.setCursor(LCD_POS_COL_STS, LCD_POS_ROW_STS); // Move the cursor to column 0,row 0
lcd.print("Pump off");
}
// While-State
if (acutal_state == new_state) {
//Action
//Screen Update
}
// State-End
break;
}
}
}
//Funcions - Transitionhandler
void transitionhandler(event_p event) {
switch (event) {
case EVT_INIT_COMPLETE:
{
//Transitions
if (acutal_state == STS_INIT) {
new_state = STS_IDLE;
}
break;
}
case EVT_BUTTON:
{
static unsigned long lastTimePressed = 0;
//ignore multiple press in that time milliseconds
if (millis() - lastTimePressed > 100) //Entprellung
{
//Transitions
if (acutal_state == STS_PUMP_OFF)
{
new_state = STS_IDLE;
}
if (acutal_state == STS_IDLE)
{
new_state = STS_MIX_SELECT;
}
if (acutal_state == STS_MIX_SELECT)
{
new_state = STS_PUMP_ON;
}
}
break;
}
case EVT_PUMPTIMER:
{
//Transitions
if (acutal_state == STS_PUMP_ON)
{
new_state = STS_PUMP_OFF;
}
break;
}
case EVT_UP:
{
//Log
serial_log(millis(), "New Event: EVT_UP");
//Transitions
if (acutal_state == STS_MIX_SELECT)
{
new_state = STS_MIX_SELECT;
if (selected_mix < (sizeof(recipe)/sizeof(recipe[0])))
{
++selected_mix;
}
else
{
selected_mix = 0;
}
char buffer[40];
sprintf(buffer, " %d Ingredients in Mix %s ", recipe[selected_mix].ingredientscount, recipe[selected_mix].name);
serial_log(millis(), buffer);
}
break;
}
case EVT_DOWN:
{
//Log
serial_log(millis(), "New Event: EVT_DOWN");
//Transitions
if (acutal_state == STS_MIX_SELECT)
{
new_state = STS_MIX_SELECT;
if(selected_mix > 0)
{
--selected_mix;
}
else
{
selected_mix = (sizeof(recipe)/sizeof(recipe[0])) -1 ;
}
}
break;
}
}
}
//Functions - Allgemein
// Wifi
// Converts Wfi.local Ip to stirng
String IpAddress2String(const IPAddress& ipAddress) {
return String(ipAddress[0]) + String(".") + String(ipAddress[1]) + String(".") + String(ipAddress[2]) + String(".") + String(ipAddress[3]);
}
//LCD
// Clear defined line of LCD
void lcd_clear_line(LiquidCrystal_I2C& objekt, int line) {
objekt.setCursor(0, line); // Move the cursor to column, row
for (int i = 0; i <= LCD_NO_CHARS - 1; ++i) {
objekt.print(" ");
}
}
// LCD Progressbar
void lcd_progressbar(LiquidCrystal_I2C& objekt, int startpos, int line, int percentage) {
lcd_clear_line(objekt, line);
objekt.setCursor(startpos, line); // Move the cursor to column, row
objekt.print("[");
for (int i = 1; i <= 10; ++i) {
if (i <= percentage / 10) {
objekt.write(255);
} else {
objekt.print(" ");
}
}
objekt.print("]");
}
// Serial log
void serial_log(unsigned long tme, String txt) {
int hr = tme / 3600000; //Number of seconds in an hour
int mins = (tme - hr * 3600000) / 60000; //Remove the number of hours and calculate the minutes.
int sec = (tme - hr * 3600000 - mins * 60000) / 1000; //Remove the number of hours and minutes, leaving only seconds.
word msec = tme - hr * 3600000 - mins * 60000 - sec * 1000; //Remove the number of hours and minutes, seconds leaving only milliseconds.
char hrMinSec[14];
sprintf(hrMinSec, "%02d:%02d:%02d:%03d,", hr, mins, sec, msec);
Serial.print(hrMinSec);
//String hrMinSec = (String(hr) + ":" + String(mins) + ":" + String(sec)+ ":" + String(msec)); //Converts to HH:MM:SS string. This can be returned to the calling function.
Serial.println(txt);
}