cotestatnt / AsyncTelegram2

Powerful, flexible and secure Arduino Telegram BOT library. Hardware independent, it can be used with any MCU capable of handling an SSL connection.
MIT License
83 stars 25 forks source link

myReplyKbd #84

Closed xlettera closed 1 year ago

xlettera commented 1 year ago

Dear Tolentino, I would like to use different ReplyKbd in my project. I have seen that there is no flushData (myKbd.flushData();) option that empty a keyboard. With CTbot library there is this option and in this way you can create different keyboards, very useful.

Is there a way to switch from keyboard1 to keyboard2 in the same sketch?

You library is asynchronous and it's great. Very good starting point.

Thanks for your time. Roberto

cotestatnt commented 1 year ago

Dear Roberto, In my opinion, continuously resizing the same ReplyKeyboard type global object could lead to problems with heap fragmentation.

It would be better to define two distinct keyboards, or even better in order to make everything more efficient, load the keyboard only when really needed using local variables.

Indeed, a Telegram reply keyboard (or inline keyboard) it's just a JSON formatted string. The two classes included in this library InlineKeyboard and ReplyKeyboard offers a convenient way to build it at runtime, but you can pass a keyboard to sendMessage() method directly as String.

For example, this are the same two keyboards used in keyboards.ino, but it's stored in flash memory due to the keyword PROGMEM reducing a lot RAM consumption!

You can add any row or column you want, simply keeping the same schema: each [...] is a new row containing some columns {...} each one with it's own properties.

//ReplyKeyboard myReplyKbd;   // reply keyboard object helper
static const char myReplyKbd[] PROGMEM =  R"EOF(
{
  "keyboard": [
    [
      {"text": "Button1"},
      {"text": "Button2"},
      {"text": "Button3"}
    ],
    [
      {"text": "Send Location", "request_location": true},
      {"text": "Send contact", "request_contact": true}
    ],
    [
      {"text": "/hide_keyboard"}
    ]
  ],
  "resize_keyboard": true
}
)EOF";

//InlineKeyboard myInlineKbd; // inline keyboard object helper
static const char myInlineKbd[] PROGMEM =  R"EOF(
{
  "inline_keyboard": [
    [
      {"text":"ON","callback_data":"ON"},
      {"text":"OFF","callback_data":"OFF"},
      {"text":"←","callback_data":"DELETE"}
    ],
    [
      {"text":"GitHub","url":"https://github.com/cotestatnt/AsyncTelegram2/"}
    ]
  ]
}
)EOF";

So, when you need to send a specific keyboard you could do simply like this:

// outside of this scope, the keyboard will not consume RAM unnecessarily
{
  String keyb = myReplyKbd;   // local String variables
  myBot.sendMessage(msg, "This is reply keyboard:", keyb);
}

Anyway, adding the method you requested is a pretty simple thing so you will find it in next release (although I prefer to call it clear() instead of flushData())

xlettera commented 1 year ago

Dear Tolentino, Thank you for your professional and early reply. In my opinion avoiding heap fragmentation is a MUST and the first thing to consider. (I have seen a lot of ESP32 crashing after few days of work without a clear reason. With few lines of code maybe it's not a problem, but in big sketches it is, before or after it will happen.)

In my case when I program I stay away from String class as much as possible. It takes more time but it avoids future headache.

I will follow your suggestion to make everything more efficient. This is what I was looking for, not the fastest way but the safest one. If the code is crystal clear and error prof an ESP32, in my case, can become a professional tool. Otherwise, it remains a useless piece of plastic.

I have tried the code you suggested and it works like a charm.

I post this few lines of code that can be useful in many application.

--- This first part just load from EEPROM a String of 11 chars (ie. "PROGRAM123") and convert it to a char array. ---

`char char_array[12];`

// Function READ A STRING FROM EEPROM
String EEPROM_ESP32_READ(int min, int max) {
EEPROM.begin(512); delay(10); String buffer;
for (int L = min; L < max; ++L)
buffer += char(EEPROM.read(L));
return buffer;
}

void setup
{
  ... CODE ...
  // READ A STRING FROM EEPROM AND CONVERT IT TO CHAR ARRAY
  String str = "";
  str = (String) EEPROM_ESP32_READ(0,12);  // Read 0...12 address
  Serial.print("str Read from EEPROM: ");
  Serial.println(str);
  // convert string to char array
  str.toCharArray(char_array, 12);
}

In my sketch I need to change the labels of the keys dynamically, I load them from EEprom. From your sketches in your library there was also this code. I took it from you. I have tried it and it works better for me because in this way I can change the labels of the keys when I need it in runtime.

Do you think this can lead to heap fragmentation? Can you do this even with the code you suggest me. I tested it only yesterday. Do you have any suggestion?

--- This second part load 2 keyboards with the possibility to change dynamically the label with the char_array. ---

const char *test="abcd";  // just a test

void getKeyboard1(ReplyKeyboard *kbd) {
  kbd->enableOneTime();
  kbd->enableSelective();
  kbd->addButton(test);    
  kbd->addButton("1");
  kbd->addButton("2");
  kbd->addRow();
  kbd->addButton("/key2");  // call the second keyboard
  kbd->addButton("/x");
}

void getKeyboard2(ReplyKeyboard *kbd) {
  kbd->enableOneTime();
  kbd->enableSelective();
  kbd->addButton(char_array);  // *** this key/label is loaded from EEPROM ***
  kbd->addButton("Menu1");
  kbd->addButton("Menu2");
  kbd->addRow();
  kbd->addButton("/key1");  // call the first keyboard
  kbd->addButton("/x");
}

--- this part shows and hide keyboard and let you switch from keyboard1 to keyboard2

void loop()
{
    ...CODE...
    // show keyboards
    if (msgText.equalsIgnoreCase("/key1")) 
    {
      ReplyKeyboard Keyboard1;
      getKeyboard1(&Keyboard1);
      myBot.sendMessage(msg, "load keyboard1", Keyboard1);
      isKeyboardActive = true;
    }   
    if (msgText.equalsIgnoreCase("/key2")) 
    {
      ReplyKeyboard Keyboard2;
      getKeyboard2(&Keyboard2);
      myBot.sendMessage(msg, "load keyboard2", Keyboard2);
      isKeyboardActive = true;
    }    
    // remove keyboard
    if (msgText.equalsIgnoreCase("/x")) 
    {
       myBot.removeReplyKeyboard(msg, "Reply keyboard removed");
       isKeyboardActive = false;     
    }
}

If the clear() method (in your opinion) can lead to future or unpredictable problems, it's better to avoid it for the sake of stability. If it is safe and you think that the code is error proof, it can be very, very, very useful. I hope this is the case.

Better to have few solid functions or methods than to hear someone saying "he did too much..."

I thank you in advance for your library, very well done. Keep working on it, I think it worths the time. You are a point of reference for many users.

I am going to test it deeply pushing it to its limits. In case I find one I will let you know and you will move it further.

Thank you.

cotestatnt commented 1 year ago

Do not be fooled by what you read online about the Arduino String class ... often you see some really absurd things in an attempt NOT to use the String object and remedy often does more damage than harm. I made the same mistake, wasting precious time.

In particular with the ESP32, the large amount of SRAM available, allows you to use classes that are much "heavier" than String without worries... What really matters is HOW and WHEN you use these classes.

Just as an example, when this library need to parse the JSON received from Telegram server, almost the whole SRAM avalaible was allocated for this purpose as suggested from Benoît Blanchon, a C++ guru, author of the great library ArduinoJSON - Technique 4: use whatever RAM is available. The local allocated DynamicJsonDocument object is then destroyed as soon as is not more needed.

About your sketch, I would not use the EEPROM library ... on ESP32 it is just an emulation of the classic Arduino library because this MCU has no internal EEPROM, but only an SPI flash memory

You could take a look at the library included in the "Preferences" core or even better directly save the JSON of the different keyboards directly as a file in the ESP memory and recall them only when they are needed...

Once the keyboards JSON files was created and saved in flash memory, you could even remove the createKeyboard() functions, for example using a #define CREATE_JSON 1 // 0 or something similar.

#define CREATE_JSON 1 // 0

#if CREATE_JSON 
createKeyboard1() {...}
createKeyboard2() {...}
#endif

A working example about what i mean:

#include <AsyncTelegram2.h>

// Timezone definition
#include <time.h>
#define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3"

#include "FS.h"
#include <LittleFS.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>

#define CREATE_JSON 1         // 0
#define KEYB_1 "/keyb1.json"
#define KEYB_2 "/keyb2.json"

WiFiClientSecure client;

AsyncTelegram2 myBot(client);
const char* ssid  =  "xxxxxxx";     // SSID WiFi network
const char* pass  =  "xxxxxx";     // Password  WiFi network
const char* token =  "xxxxxxxxx";  // Telegram token

// Check the userid with the help of bot @JsonDumpBot or @getidsbot (work also with groups)
// https://t.me/JsonDumpBot  or  https://t.me/getidsbot
int64_t userid = 123456789;

#if CREATE_JSON 
bool createKeyboard1(const char *kbdPath, const char * butLabel) {
  if ( LittleFS.exists(kbdPath)) {
    return true;
  }

  File file = LittleFS.open(kbdPath, FILE_WRITE);
  if (!file || file.isDirectory()) {
    Serial.println("Failed to open file for writing");
    return false;
  }

  ReplyKeyboard kbd;
  kbd.enableOneTime();
  kbd.enableSelective();
  kbd.addButton(butLabel);
  kbd.addButton("1");
  kbd.addButton("2");
  kbd.addRow();
  kbd.addButton("/key2");  // call the second keyboard
  kbd.addButton("/x");
  String keyboard = kbd.getJSONPretty();
  file.print(keyboard);
  file.close();
  return true;
}

bool createKeyboard2(const char * kbdPath, const char * butLabel) {  
  if ( LittleFS.exists(kbdPath)) {
    return true;
  }

  File file = LittleFS.open(kbdPath, FILE_WRITE);
  if (!file || file.isDirectory()) {
    Serial.println("Failed to open file for writing");
    return false;
  }

  ReplyKeyboard kbd;
  kbd.enableOneTime();
  kbd.enableSelective();
  kbd.addButton(butLabel);  // *** this key/label is loaded from EEPROM ***
  kbd.addButton("Menu1");
  kbd.addButton("Menu2");
  kbd.addRow();
  kbd.addButton("/key1");  // call the first keyboard
  kbd.addButton("/x");
  String keyboard = kbd.getJSONPretty();
  file.print(keyboard);
  file.close();
  return true;
}
#endif      

String getKeyboard(const char* kbdPath) {
  Serial.printf("Reading file: %s\r\n", kbdPath);

  File file = LittleFS.open(kbdPath);
  if (!file || file.isDirectory()) {
    Serial.println("- failed to open file for reading");
    return "";
  }

  String keyboard;
  while (file.available()) {
    keyboard += (char) file.read();
  }
  file.close();
  Serial.println(keyboard);
  return keyboard;
}

void setup() {
  // initialize the Serial
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(115200);

  if (!LittleFS.begin(true)) {
    Serial.println("LittleFS Mount Failed");
    return;
  }

#if CREATE_JSON 
  createKeyboard1(KEYB_1, "TEST 1");
  createKeyboard2(KEYB_2, "TEST 2");
#endif      

  // connects to the access point
  WiFi.begin(ssid, pass);
  delay(500);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }

  // Sync time with NTP
  configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org");
  client.setCACert(telegram_cert);

  // Set the Telegram bot properties
  myBot.setUpdateTime(1000);
  myBot.setTelegramToken(token);

  // Check if all things are ok
  Serial.print("\nTest Telegram connection... ");
  myBot.begin() ? Serial.println("OK") : Serial.println("NOK");
  Serial.print("Bot name: @");
  Serial.println(myBot.getBotName());

  char welcome_msg[128];
  snprintf(welcome_msg, 128, "BOT @%s online\n/help all commands avalaible.", myBot.getBotName());

  // Send a message to specific user who has started your bot
  myBot.sendTo(userid, welcome_msg);
}

void loop() {

  // Show currest stats about heap memory block
  printHeapStats();

  // In the meantime LED_BUILTIN will blink with a fixed frequency
  static uint32_t ledTime = millis();
  if (millis() - ledTime > 200) {    
    ledTime = millis();
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));    
  }

  TBMessage msg;
  if (myBot.getNewMessage(msg)) {

    switch (msg.messageType) {
      case MessageText : {
          String msgText = msg.text;
          Serial.print("\nText message received: ");
          Serial.println(msgText);

           if (msgText.equalsIgnoreCase("/key1")) {                     
            myBot.sendMessage(msg, "This is keyboard 1:", getKeyboard(KEYB_1));
          }
          else if (msgText.equalsIgnoreCase("/key2")) {                     
            myBot.sendMessage(msg, "This is keyboard 2:", getKeyboard(KEYB_2));
          }
          else {
            myBot.sendMessage(msg, "Try /key1 or /key2");
          }
          break;
        }

      default:
        break;
    }
  }
}

void printHeapStats() {
  time_t now = time(nullptr);
  struct tm tInfo = *localtime(&now);
  static uint32_t infoTime;
  if (millis() - infoTime > 1000) {
    infoTime = millis();
    //heap_caps_print_heap_info(MALLOC_CAP_DEFAULT);
    Serial.printf("%02d:%02d:%02d - Total free: %6d - Largest free block: %6d\n",
                  tInfo.tm_hour, tInfo.tm_min, tInfo.tm_sec, heap_caps_get_free_size(0), heap_caps_get_largest_free_block(0) );
  }
}
xlettera commented 1 year ago

Dear Tolentino, very good code. I tested it and it works very well. No doubt. You really know what you do. You are a Guru too. I have to rewrite a lot of code in may project but if you think this is the best way I will do it.

(Regarding to Strings I know they are useful but there are limits if you don't know how to treat them. What really matters is HOW and WHEN... In any case I did a sketch to send data to google sheet using only char arrays and I could send more than 256 values at a time without any problems for days. ESP32 was collecting data for ML. It was collecting wave forms. Before I used a library that was based on Strings and after 83 values it started to crash. It could handle without any problems max 64 values. Anyway I think this is not the right place for this topic.)

I use Preference library too and it is way better than EEPROM, you are right. I have never used LittleFS but I can consider using it too. The idea behind was that you read a label from flash and put it on a keyboard key. In my sketch I used a function to read from flash an put this label on the key. Without clear() method it doesn't work anymore. But I find your library absolutely good. It's very fast and until now very stable. So I can rewrite what ever it is to rewrite.

I will follow your approach but I would like to assign a different label to every key for each keyboard (in my sketch I was even able to customize a key sending a message...), can you show me the best way to assign a different label to these keys ? I know that for you it's very easy. For me it's not, I have never used LittleFS and I am not a specialist. I will learn it.

#if CREATE_JSON 
bool createKeyboard1(const char *kbdPath, const char * butLabel) {
  if ( LittleFS.exists(kbdPath)) {
    return true;
  }

  File file = LittleFS.open(kbdPath, FILE_WRITE);
  if (!file || file.isDirectory()) {
    Serial.println("Failed to open file for writing");
    return false;
  }

  ReplyKeyboard kbd;
  kbd.enableOneTime();
  kbd.enableSelective();
  kbd.addButton(butLabel);
  kbd.addButton(butLabel1);     // read label from memory
  kbd.addButton(butLabel2);     // read label from memory
  kbd.addRow();
  kbd.addButton("/key2");  // call the second keyboard
  kbd.addButton("/x");
  String keyboard = kbd.getJSONPretty();
  file.print(keyboard);
  file.close();
  return true;
}

This topic will be a milestone for many to come. A pleasure to talk to you. Thanks

Roberto

cotestatnt commented 1 year ago

When I have a homogeneous data structure, I usually define the data with a struct that contains all the necessary properties and then I make an array of these structs (or a "linked list" if I need to be dinamic).

If your keyboards for example has always the same shape, first row 3 buttons - second row 2 buttons, it's quite easy.

#include <AsyncTelegram2.h>

// Timezone definition
#include <time.h>
#define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3"

#include <LittleFS.h>
#include <WiFiClientSecure.h>

#define KEYB_1 "/key1"
#define KEYB_2 "/key2"
#define KEYB_3 "/key3"

#define CREATE_JSON 1

WiFiClientSecure client;

AsyncTelegram2 myBot(client);
const char* ssid  =  "xxxxxxx";     // SSID WiFi network
const char* pass  =  "xxxxxxx";     // Password  WiFi network
const char* token =  "xxxxxxxx";  // Telegram token

// Check the userid with the help of bot @JsonDumpBot or @getidsbot (work also with groups)
// https://t.me/JsonDumpBot  or  https://t.me/getidsbot
int64_t userid = 123445678;

// Custom struct which defines single keybaord properties
struct Keyboard_t {
  bool selective;
  bool oneTime;
  const char* name;
  const char* label1;
  const char* label2;
  const char* label3;
  const char* linkedKbd;
};

// Array of Keyboard_t (defined at compile time)
const Keyboard_t keyboards[] PROGMEM = {
  {1, 1, KEYB_1, "TEST1", "Label 1", "Label 2", KEYB_2},
  {1, 1, KEYB_2, "TEST2", "Button 1", "Button 2", KEYB_3},
  {1, 1, KEYB_3, "TEST3", "Menu 1", "Menu 2", KEYB_1},
};

#if CREATE_JSON
bool createKeyboard(Keyboard_t & keyboard, bool overwrite = false) {
  String filePath = keyboard.name;
  filePath += ".json";

  if ( LittleFS.exists(filePath) && !overwrite) {
    return true;
  }

  File file = LittleFS.open(filePath, FILE_WRITE);
  if (!file || file.isDirectory()) {
    Serial.println("Failed to open file for writing");
    return false;
  }

  ReplyKeyboard kbd;
  if ( keyboard.oneTime)
    kbd.enableOneTime();
  if ( keyboard.selective)
    kbd.enableSelective();
  kbd.addButton(keyboard.label1);
  kbd.addButton(keyboard.label2);
  kbd.addButton(keyboard.label3);
  kbd.addRow();
  kbd.addButton(keyboard.linkedKbd);  // call the linked keyboard
  kbd.addButton("/x");
  String kbdJSON = kbd.getJSONPretty();
  file.print(kbdJSON);
  file.close();
  return true;
}
#endif

String getKeyboard(const char* kbdPath) {
  String filePath = kbdPath;
  filePath +=  ".json";

  Serial.printf("Reading file: %s\r\n", filePath);
  File file = LittleFS.open(filePath);
  if (!file || file.isDirectory()) {
    Serial.println("- failed to open file for reading");
    return "";
  }

  String keyboard;
  while (file.available()) {
    keyboard += (char) file.read();
  }
  file.close();
  Serial.println(keyboard);
  return keyboard;
}

void setup() {
  // initialize the Serial
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(115200);

  if (!LittleFS.begin(true)) {
    Serial.println("LittleFS Mount Failed");
    return;
  }

#if CREATE_JSON
  // Range based for loop https://en.cppreference.com/w/cpp/language/range-for
  for (const Keyboard_t& keyb : keyboards ) {
    createKeyboard(keyb, true);
  }
#endif

  // connects to the access point
  WiFi.begin(ssid, pass);
  delay(500);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }

  // Sync time with NTP
  configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org");
  client.setCACert(telegram_cert);

  // Set the Telegram bot properties
  myBot.setUpdateTime(1000);
  myBot.setTelegramToken(token);

  // Check if all things are ok
  Serial.print("\nTest Telegram connection... ");
  myBot.begin() ? Serial.println("OK") : Serial.println("NOK");
  Serial.print("Bot name: @");
  Serial.println(myBot.getBotName());

  char welcome_msg[128];
  snprintf(welcome_msg, 128, "BOT @%s online\n/help all commands avalaible.", myBot.getBotName());

  // Send a message to specific user who has started your bot
  myBot.sendTo(userid, welcome_msg);
}

void loop() {

  // Show currest stats about heap memory block
  printHeapStats();

  // In the meantime LED_BUILTIN will blink with a fixed frequency
  static uint32_t ledTime = millis();
  if (millis() - ledTime > 200) {
    ledTime = millis();
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }

  TBMessage msg;
  if (myBot.getNewMessage(msg)) {

    switch (msg.messageType) {
      case MessageText : {
          String msgText = msg.text;
          Serial.print("\nText message received: ");
          Serial.println(msgText);

          if (msgText.equalsIgnoreCase(KEYB_1)) {
            myBot.sendMessage(msg, "This is keyboard 1:", getKeyboard(KEYB_1));
          }
          else if (msgText.equalsIgnoreCase(KEYB_2)) {
            myBot.sendMessage(msg, "This is keyboard 2:", getKeyboard(KEYB_2));
          }
          else if (msgText.equalsIgnoreCase(KEYB_3)) {
            myBot.sendMessage(msg, "This is keyboard 3:", getKeyboard(KEYB_3));
          }
          else {
            String reply = "Try one of this commands:\n";
            for (const Keyboard_t& keyb : keyboards ) {              
              reply += keyb.name;      
              reply += " ";                      
            }
            myBot.sendMessage(msg, reply);
          }
          break;
        }

      default:
        break;
    }
  }
}

void printHeapStats() {
  time_t now = time(nullptr);
  struct tm t = *localtime(&now);
  static uint32_t infoTime;
  if (millis() - infoTime > 1000) {
    infoTime = millis();
    //heap_caps_print_heap_info(MALLOC_CAP_DEFAULT);
    Serial.printf("%02d:%02d:%02d - Total free: %6d - Largest free block: %6d\n",
                  t.tm_hour, t.tm_min, t.tm_sec, heap_caps_get_free_size(0), heap_caps_get_largest_free_block(0) );
  }
}
xlettera commented 1 year ago

Dear Tolentino, another interesting solution, high quality code. But when I try to compile it with Arduino:1.8.19 (Linux), for "DOIT ESP32 DEVKIT V1, 80MHz, 921600 there is an error:

_/home/utente/Arduino/TolentinoNewKeyKernel/newK/newK.ino: In function 'void setup()': newK:112:20: error: binding reference of type 'Keyboard_t&' to 'const Keyboard_t' discards qualifiers createKeyboard(keyb, true); ^~~~ /home/utente/Arduino/TolentinoNewKeyKernel/newK/newK.ino:46:6: note: initializing argument 1 of 'bool createKeyboard(Keyboard_t&, bool)' bool createKeyboard(Keyboardt & keyboard, bool overwrite = false) { ^~~~~~

This is the part where the compiler stops.

#if CREATE_JSON
  // Range based for loop https://en.cppreference.com/w/cpp/language/range-for
  for (const Keyboard_t& keyb : keyboards ) {
    createKeyboard(keyb, true);
  }
#endif

I don't know what exactly is going on so I prefer to show you this error.

My keyboards have different shape. So I suppose I will have to replicate your code for different layouts. I hope I will be up to it.

Anyway thank you also for this.

Roberto

cotestatnt commented 1 year ago

@xlettera sorry, it's my mistake.

I forgot to add the const qualifier to the first argument passed by reference to function createKeyboard()

In fact, the array keyboards[] has been declared as a constant to be able to store the content in the flash using the keyword PROGMEM and avoid unnecessary duplication in RAM.

Therefore, it must also be specified in the function to be treated as const

bool createKeyboard(const Keyboard_t & keyboard, bool overwrite = false) {
....
}
xlettera commented 1 year ago

Dear Tolentino, It's not a problem. Good job. I have tested it and it works like a charm.

I flash it on ESP32 and I will leave it switched on. I want to see if after 3 or 5 days it still works. Just with your sketch.

I don't know why but after a random number of days ESP32 just stops to respond to telegram. It is not blocked, it still have an IP address but it is unable to reply.. It's not easy to debug... it works perfectly for days until it stops.

I am sure that it's not your library but something else that I use or the hardware... If after a week there is no issue I will exclude the hardware.

Thank you for your help. AsyncTelegram2 is the best library out there!

Roberto

xlettera commented 1 year ago

Dear Tolentino, I added more keyboard layouts to your last sketch. It works and I hope it's fine. But is it possible to change a key label to a certain keyboard on runtime. If I need at certain point to change a key label how can I do? I have no idea which is the best way...

loop() {
  ...
  else if (msgText.equalsIgnoreCase("/change")) {
  // code... to change a single key to a keyboard  
  ...
}

this is the working code:

#include <AsyncTelegram2.h>
// Timezone definition
#include <time.h>
#define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3"
#include <WiFiClientSecure.h>
WiFiClientSecure client;

AsyncTelegram2 myBot(client);
const char* ssid  =  "xxxxxxxxxxxx";      // SSID WiFi network
const char* pass  =  "xxxxxxxxxxxx";      // Password  WiFi network
const char* token =  "xxxxxxxxxxxx";      // Telegram token

// Check the userid with the help of bot @JsonDumpBot or @getidsbot (work also with groups)
// https://t.me/JsonDumpBot  or  https://t.me/getidsbot
int64_t userid = 123445678;

// TO SAVE KEYBOARDS
#include <LittleFS.h>

// DEFINE HOW MANY KEYBOARDS DO YOU NEED
// LAYOUT1
#define KEYB_1 "/key1"
#define KEYB_2 "/key2"
#define KEYB_3 "/key3"
// LAYOUT2
#define KEYB_4 "/key4"
#define KEYB_5 "/key5"
#define KEYB_6 "/key6"

#define CREATE_JSON 1
// LAYOUT1
// Custom struct which defines single keybaord properties
struct Keyboard_1 {
  bool selective;                       // Use this parameter if you want to show the keyboard for specific users only.
  bool oneTime;                         // hide the reply keyboard as soon as it's been used
  const char* name;                     // the name of the keyboard
  const char* label1;
  const char* label2;
  const char* label3;
  const char* linkedKbd;
};

// Array of Keyboard_1 (defined at compile time)
const Keyboard_1 keyboards[] PROGMEM = {
  {1, 0, KEYB_1, "KEYBOARD1", "KEY1", "KEY2", KEYB_2},
  {1, 0, KEYB_2, "KEYBOARD2", "KEY3", "KEY4", KEYB_3},
  {1, 0, KEYB_3, "KEYBOARD3", "KEY5", "KEY6", KEYB_4},
};

//LAYOUT2
// Custom struct which defines single keybaord properties
struct Keyboard_2 {
  bool selective;
  bool oneTime;
  const char* name;
  const char* label1;
  const char* label2;
  const char* linkedKbd;
};

// Array of Keyboard_1 (defined at compile time)
const Keyboard_2 keyboards2[] PROGMEM = {
  {1, 0, KEYB_4, "KEYBOARD4", "KEY7", KEYB_5},
  {1, 0, KEYB_5, "KEYBOARD5", "KEY8", KEYB_6},
  {1, 0, KEYB_6, "KEYBOARD6", "KEY9", KEYB_1},
};

// LAYOUT1
#if CREATE_JSON
bool createKeyboardLayout1(const Keyboard_1 & keyboard, bool overwrite = false) {
  String filePath = keyboard.name;
  filePath += ".json";

  if ( LittleFS.exists(filePath) && !overwrite) {
    return true;
  }

  File file = LittleFS.open(filePath, FILE_WRITE);
  if (!file || file.isDirectory()) {
    Serial.println("Failed to open file for writing");
    return false;
  }

  ReplyKeyboard kbd1;
  if ( keyboard.oneTime)
    kbd1.enableOneTime();
  if ( keyboard.selective)
    kbd1.enableSelective();
  kbd1.addButton(keyboard.label1);
  kbd1.addButton(keyboard.label2);
  kbd1.addButton(keyboard.label3);
  kbd1.addRow();
  kbd1.addButton(keyboard.linkedKbd);  // call the linked keyboard
  kbd1.addButton("/exit");
  String kbdJSON = kbd1.getJSONPretty();
  file.print(kbdJSON);
  file.close();
  return true;
}
#endif

// LAYOUT2
#if CREATE_JSON
bool createKeyboardLayout2(const Keyboard_2 & keyboard, bool overwrite = false) {
  String filePath = keyboard.name;
  filePath += ".json";

  if ( LittleFS.exists(filePath) && !overwrite) {
    return true;
  }

  File file = LittleFS.open(filePath, FILE_WRITE);
  if (!file || file.isDirectory()) {
    Serial.println("Failed to open file for writing");
    return false;
  }

  ReplyKeyboard kbd2;
  if ( keyboard.oneTime)
    kbd2.enableOneTime();
  if ( keyboard.selective)
    kbd2.enableSelective();
  kbd2.addButton(keyboard.label1);
  kbd2.addButton(keyboard.label2);
  kbd2.addRow();
  kbd2.addButton(keyboard.linkedKbd);  // call the linked keyboard
  kbd2.addButton("/exit");
  String kbdJSON = kbd2.getJSONPretty();
  file.print(kbdJSON);
  file.close();
  return true;
}
#endif

// SHOW KEYBOARD FUNCTION
String getKeyboard(const char* kbdPath) {
  String filePath = kbdPath;
  filePath +=  ".json";

  Serial.printf("Reading file: %s\r\n", filePath);
  File file = LittleFS.open(filePath);
  if (!file || file.isDirectory()) {
    Serial.println("- failed to open file for reading");
    return "";
  }

  String keyboard;
  while (file.available()) {
    keyboard += (char) file.read();
  }
  file.close();
  Serial.println(keyboard);
  return keyboard;
}

void setup() {
  // initialize the Serial
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(115200);

  if (!LittleFS.begin(true)) {
    Serial.println("LittleFS Mount Failed");
    return;
  }

#if CREATE_JSON
  // Range based for loop https://en.cppreference.com/w/cpp/language/range-for
  for (const Keyboard_1& keyb : keyboards ) {
    createKeyboardLayout1(keyb, true);
  }
#endif

#if CREATE_JSON
  // Range based for loop https://en.cppreference.com/w/cpp/language/range-for
  for (const Keyboard_2& keyb2 : keyboards2 ) {
    createKeyboardLayout2(keyb2, true);
  }
#endif

  // connects to the access point
  WiFi.begin(ssid, pass);
  delay(500);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }

  // Sync time with NTP
  configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org");
  client.setCACert(telegram_cert);

  // Set the Telegram bot properties
  myBot.setUpdateTime(1000);
  myBot.setTelegramToken(token);

  // Check if all things are ok
  Serial.print("\nTest Telegram connection... ");
  myBot.begin() ? Serial.println("OK") : Serial.println("NOK");
  Serial.print("Bot name: @");
  Serial.println(myBot.getBotName());

  char welcome_msg[128];
  snprintf(welcome_msg, 128, "BOT @%s online\n/help all commands avalaible.", myBot.getBotName());

  // Send a message to specific user who has started your bot
  myBot.sendTo(userid, welcome_msg);
}

void loop() {

  // Show currest stats about heap memory block
  printHeapStats();

  // In the meantime LED_BUILTIN will blink with a fixed frequency
  static uint32_t ledTime = millis();
  if (millis() - ledTime > 200) {
    ledTime = millis();
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }

  TBMessage msg;
  if (myBot.getNewMessage(msg)) {

    switch (msg.messageType) {
      case MessageText : {
          String msgText = msg.text;
          Serial.print("\nText message received: ");
          Serial.println(msgText);

          if (msgText.equalsIgnoreCase(KEYB_1)) {
            myBot.sendMessage(msg, "This is keyboard 1:", getKeyboard(KEYB_1));
          }
          else if (msgText.equalsIgnoreCase(KEYB_2)) {
            myBot.sendMessage(msg, "This is keyboard 2:", getKeyboard(KEYB_2));
          }
          else if (msgText.equalsIgnoreCase(KEYB_3)) {
            myBot.sendMessage(msg, "This is keyboard 3:", getKeyboard(KEYB_3));
          }
          else if (msgText.equalsIgnoreCase(KEYB_4)) {
            myBot.sendMessage(msg, "This is keyboard 4:", getKeyboard(KEYB_4));
          }
          else if (msgText.equalsIgnoreCase(KEYB_5)) {
            myBot.sendMessage(msg, "This is keyboard 5:", getKeyboard(KEYB_5));
          }
          else if (msgText.equalsIgnoreCase(KEYB_6)) {
            myBot.sendMessage(msg, "This is keyboard 6:", getKeyboard(KEYB_6));
          }
          else if (msgText.equalsIgnoreCase("/change")) {
            // code... to change a single key to a keyboard  
          }
          else if (msgText.equalsIgnoreCase("/exit")) {
            myBot.removeReplyKeyboard(msg, "Reply keyboard removed");
          }
          else {
            String reply = "Try one of this commands:\n";
            for (const Keyboard_1& keyb : keyboards ) {
              reply += keyb.name;
              reply += " ";
            }
            for (const Keyboard_2& keyb2 : keyboards2 ) {
              reply += keyb2.name;
              reply += " ";
            }
            myBot.sendMessage(msg, reply);
          }
          break;
        }

      default:
        break;
    }
  }
}

void printHeapStats() {
  time_t now = time(nullptr);
  struct tm t = *localtime(&now);
  static uint32_t infoTime;
  if (millis() - infoTime > 1000) {
    infoTime = millis();
    //heap_caps_print_heap_info(MALLOC_CAP_DEFAULT);
    Serial.printf("%02d:%02d:%02d - Total free: %6d - Largest free block: %6d\n",
                  t.tm_hour, t.tm_min, t.tm_sec, heap_caps_get_free_size(0), heap_caps_get_largest_free_block(0) );
  }
}

I have learnt a lot of things in these days and I have to thank you. The ESP32 with your code is still running. No doubts.

I can't wait to see your next release with .clear() method... and I think much more

xlettera commented 1 year ago

Dear Tolentino, I studied JSON and now I understand. Very powerful. I can change the key label. I am sure the way you solve it is far way better than mine. Anyway it works.

xlettera commented 1 year ago

Thank you for your help.

Don't forget to include clear() method in a future release.

Thank you again

Regards

Roberto