Ruben Zilzer
Published © GPL3+

Granpa Box

A one-button voice messaging box that lets young children easily send and receive Telegram voice messages with their family.

IntermediateWork in progress13
Granpa Box

Things used in this project

Hardware components

waveshare ESP32-S3 AI Smart Speaker
https://www.waveshare.com/product/esp32-related/ai-chat/esp32-s3-audio-board.htm
×1
Momentary Push Button – large arcade-style button
×1

Software apps and online services

Arduino IDE
ESP32 Arduino Core
Telegram Bot API
WiFi / HTTPS libraries for ESP32
Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D-Printed Enclosure

Story

Read more

Schematics

Grandpa box

The project is intentionally simple from the user's point of view, but some setup is required before uploading the code.

First, create a Telegram bot using **@BotFather** and obtain the bot token. Then create the Telegram groups you want to use (Grandparents and Mom & Dad), add the bot to them, and obtain their **Chat IDs**.

Create a `secrets.h` file in the Arduino project folder containing your Wi-Fi SSID/password, Telegram bot token, and the two Chat IDs. This file is kept separate from the main code so private credentials are not published with the project.

After installing the required Arduino libraries, compile and upload the sketch to the Waveshare ESP32-S3 board.

Once configured, everything else is automatic: the box connects to Wi-Fi, records the voice message, sends it through Telegram, checks for incoming voice messages, downloads them, and plays them through the speaker.

Code

Grandpa box

C/C++
The project is intentionally simple from the user's point of view, but some setup is required before uploading the code.

First, create a Telegram bot using **@BotFather** and obtain the bot token. Then create the Telegram groups you want to use (Grandparents and Mom & Dad), add the bot to them, and obtain their **Chat IDs**.

Create a `secrets.h` file in the Arduino project folder containing your Wi-Fi SSID/password, Telegram bot token, and the two Chat IDs. This file is kept separate from the main code so private credentials are not published with the project.

After installing the required Arduino libraries, compile and upload the sketch to the Waveshare ESP32-S3 board.

Once configured, everything else is automatic: the box connects to Wi-Fi, records the voice message, sends it through Telegram, checks for incoming voice messages, downloads them, and plays them through the speaker.
No preview (download only).

Grandpa box

C/C++
The project is intentionally simple from the user's point of view, but some setup is required before uploading the code.

First, create a Telegram bot using **@BotFather** and obtain the bot token. Then create the Telegram groups you want to use (Grandparents and Mom & Dad), add the bot to them, and obtain their **Chat IDs**.

Create a `secrets.h` file in the Arduino project folder containing your Wi-Fi SSID/password, Telegram bot token, and the two Chat IDs. This file is kept separate from the main code so private credentials are not published with the project.

After installing the required Arduino libraries, compile and upload the sketch to the Waveshare ESP32-S3 board.

Once configured, everything else is automatic: the box connects to Wi-Fi, records the voice message, sends it through Telegram, checks for incoming voice messages, downloads them, and plays them through the speaker.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_NeoPixel.h>
#include <ESP_I2S.h>
#include <esp_system.h>
#include "secrets.h"
#include "TCA9555.h"
#include "es8311.h"
#include <AudioFileSourcePROGMEM.h>
#include <AudioGeneratorOpus.h>
#include <AudioOutput.h>

// BOARD PINS
#define LED_PIN 38
#define LED_COUNT 7
#define I2C_SDA 11
#define I2C_SCL 10
#define I2S_BCLK 13
#define I2S_MCLK 12
#define I2S_LRCK 14
#define I2S_DOUT 16
#define I2S_DIN 15
#define BIG_BUTTON_PIN 7
#define BUTTON_LED_PIN 6
#define ES7210_ADDR 0x40
#define DEFAULT_RATE 48000
#define SPEAKER_VOLUME 80
#define SPEAKER_ENABLE_EXIO 8

// GLOBALS
Adafruit_NeoPixel pixels(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
TCA9555 tca(0x20);
I2SClass i2s;
es8311_handle_t codec = NULL;
long lastUpdateId = 0;
uint8_t* audioBuffer = nullptr;
size_t audioBufferSize = 0;
volatile bool playbackRunning = false;
bool messageWaiting = false;
bool lastBigButtonState = HIGH;

// TWO TELEGRAM TARGETS
enum TargetGroup {
  TARGET_GRANDPARENTS, TARGET_IDAN_NUFAR };
TargetGroup selectedTarget = TARGET_GRANDPARENTS;

// BUTTON TIMING - 400 ms: 1 beep = GRANDPARENTS
const unsigned long SHORT_BEEP_MS = 400;

// 1500 ms: 2 beeps = IDAN + NUFAR
const unsigned long LONG_PRESS_MS = 1500;
unsigned long buttonPressStartTime = 0;
bool shortBeepPlayed = false;
bool longPressReached = false;

// WHITE BUTTON LED
bool buttonLedState = HIGH;
unsigned long lastButtonLedToggle = 0;
const unsigned long BUTTON_LED_BLINK_MS = 100;

// TELEGRAM POLLING
const unsigned long TELEGRAM_POLL_MS = 1000;
unsigned long lastTelegramPoll = 0;

// RECORDING
#define RECORD_RATE 16000
#define RECORD_CHANNELS 2
#define RECORD_BITS 16
#define MAX_RECORD_SECONDS 20
#define MAX_RECORD_BYTES (RECORD_RATE * RECORD_CHANNELS * 2 * MAX_RECORD_SECONDS)
uint8_t* recordBuffer = nullptr;
size_t recordLength = 0;
bool recording = false;
unsigned long recordStart = 0;

// RGB LED
void setLED(uint8_t r, uint8_t g, uint8_t b) {
  pixels.fill(pixels.Color(r, g, b));
  pixels.show();
}

// TARGET COLORS
void ledGrandparents() {

  // VIOLET = GRANDPARENTS
  setLED(0, 255, 200);
}

void ledParents() {

  // YELLOW = IDAN + NUFAR
  setLED(140, 255, 0);
}

void ledIdle() {

  // Dim blue
  setLED(0, 0, 30);
}

void setTargetColor() {
  if (selectedTarget == TARGET_GRANDPARENTS) {
    ledGrandparents();
  } else {
    ledParents();
  }
}


// WiFi - try known networks in priority order
struct KnownWiFi { const char* ssid; const char* password; };
KnownWiFi knownWiFi[] = {
  { WIFI1_SSID, WIFI1_PASSWORD },
  { WIFI2_SSID, WIFI2_PASSWORD },
  { WIFI3_SSID, WIFI3_PASSWORD }
};
const int KNOWN_WIFI_COUNT = sizeof(knownWiFi) / sizeof(knownWiFi[0]);

bool connectKnownWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.disconnect(true);
  delay(200);

  while (true) {
    Serial.println("Scanning WiFi...");
    int n = WiFi.scanNetworks();

    for (int k = 0; k < KNOWN_WIFI_COUNT; k++) {
      bool found = false;
      for (int i = 0; i < n; i++) {
        if (WiFi.SSID(i) == knownWiFi[k].ssid) {
          found = true;
          break;
        }
      }
      if (!found) continue;

      Serial.print("Trying WiFi: ");
      Serial.println(knownWiFi[k].ssid);
      WiFi.begin(knownWiFi[k].ssid, knownWiFi[k].password);

      unsigned long start = millis();
      while (WiFi.status() != WL_CONNECTED && millis() - start < 12000) {
        Serial.print(".");
        delay(500);
      }
      Serial.println();

      if (WiFi.status() == WL_CONNECTED) {
        Serial.print("Connected to: ");
        Serial.println(knownWiFi[k].ssid);
        Serial.print("IP: ");
        Serial.println(WiFi.localIP());
        WiFi.scanDelete();
        return true;
      }

      Serial.println("Connection failed, trying next known WiFi...");
      WiFi.disconnect(true);
      delay(300);
    }

    WiFi.scanDelete();
    Serial.println("No known WiFi connected. Retrying in 5 seconds...");
    for (int i = 0; i < 10; i++) {
      setLED((i % 2) ? 0 : 0, (i % 2) ? 0 : 0, (i % 2) ? 0 : 80);
      delay(500);
    }
    ledIdle();
  }
}

// CUSTOM AUDIO OUTPUT - OPUS -> PCM -> I2S
class GrandpaAudioOutput : public AudioOutput {
public: GrandpaAudioOutput() {
    currentRate = DEFAULT_RATE;
    currentChannels = 2;
  }

  bool begin() override {
    Serial.println("Audio output begin");
    return true;
  }

  bool SetRate(int hz) override {
    AudioOutput::SetRate(hz);
    if (hz <= 0) {
      return false;
    }
    Serial.print("Decoder sample rate: ");
    Serial.println(hz);
    if (hz != currentRate) {
      bool ok = i2s.configureTX(hz, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO);
      if (!ok) {
        Serial.println("I2S reconfigure FAILED!");
        return false;
      }
      currentRate = hz;
    }
    return true;
  }

  bool SetChannels(int channels) override {
    AudioOutput::SetChannels(channels);
    currentChannels = channels;
    Serial.print("Decoder channels: ");
    Serial.println(channels);
    return true;
  }

  bool ConsumeSample(int16_t sample[2]) override {
    int16_t frame[2];
    if (currentChannels == 1) {
      frame[0] = sample[0];
      frame[1] = sample[0];
    } else {
      frame[0] = sample[0];
      frame[1] = sample[1];
    }
    size_t written = i2s.write((uint8_t*)frame, sizeof(frame));
    return written == sizeof(frame);
  }

  bool stop() override {
    Serial.println("Audio output stop");
    return true;
  }
private: int currentRate;
  int currentChannels;
};
GrandpaAudioOutput audioOut;

// SPEAKER + I2C HARDWARE SETUP
bool setupHardware() {
  Wire.begin(I2C_SDA, I2C_SCL);
  if (!tca.begin()) {
    Serial.println("TCA9555 not found!");
    return false;
  }
  tca.pinMode1(SPEAKER_ENABLE_EXIO, OUTPUT);
  tca.write1(SPEAKER_ENABLE_EXIO, HIGH);
  i2s.setPins(I2S_BCLK, I2S_LRCK, I2S_DOUT, -1, I2S_MCLK);
  if (!i2s.begin(I2S_MODE_STD, DEFAULT_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)) {
    Serial.println("I2S begin FAILED!");
    return false;
  }
  codec = es8311_create(I2C_NUM_0, ES8311_ADDRESS_0);
  if (!codec) {
    Serial.println("ES8311 create FAILED!");
    return false;
  }
  es8311_clock_config_t clock_cfg = {
    .mclk_inverted = false, .sclk_inverted = false, .mclk_from_mclk_pin = true, .mclk_frequency = DEFAULT_RATE * 256, .sample_frequency = DEFAULT_RATE };
  ESP_ERROR_CHECK(es8311_init(codec, &clock_cfg, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
  es8311_voice_volume_set(codec, SPEAKER_VOLUME, NULL);
  es8311_voice_mute(codec, false);
  Serial.println("Speaker ready.");
  Serial.println("Big button ready on GPIO7.");
  return true;
}

// BEEP
//

// Used only while speaker I2S is active.
//

// 1 beep = GRANDPARENTS

// 2 beeps = IDAN + NUFAR
//
void beep(int count) {
  const int frequency = 900;
  const int durationMs = 120;
  const int pauseMs = 120;
  const int sampleRate = DEFAULT_RATE;
  const int samples = sampleRate * durationMs / 1000;
  for (int b = 0;
  b < count;
  b++) {
    for (int i = 0;
    i < samples;
    i++) {
      float phase = 2.0 * PI * frequency * i / sampleRate;
      int16_t value = (int16_t)(sin(phase) * 5000);
      int16_t frame[2] = {
        value, value };
      i2s.write((uint8_t*)frame, sizeof(frame));
    }
    if (b < count - 1) {
      delay(pauseMs);
    }
  }
  delay(80);
}

// TELEGRAM: GET FILE PATH
String getTelegramFilePath(const String& fileId) {
  String url = "https://api.telegram.org/bot" + String(BOT_TOKEN) + "/getFile?file_id=" + fileId;
  for (int attempt = 1;
  attempt <= 3;
  attempt++) {
    Serial.print("getFile attempt ");
    Serial.println(attempt);
    WiFiClientSecure client;
    client.setInsecure();
    HTTPClient https;
    if (!https.begin(client, url)) {
      Serial.println("getFile HTTPS begin FAILED");
      delay(500);
      continue;
    }
    int code = https.GET();
    Serial.print("getFile HTTP: ");
    Serial.println(code);
    if (code < 0) {
      Serial.print("getFile error: ");
      Serial.println(https.errorToString(code));
      https.end();
      delay(500);
      continue;
    }
    if (code != 200) {
      https.end();
      delay(500);
      continue;
    }

    String response = https.getString();
    https.end();
    String marker = "\"file_path\":\"";
    int p = response.indexOf(marker);
    if (p < 0) {
      Serial.println("getFile: file_path not found");
      delay(500);
      continue;
    }
    p += marker.length();
    int end = response.indexOf("\"", p);
    if (end < 0) {
      Serial.println("getFile: invalid response");
      delay(500);
      continue;
    }

    String filePath = response.substring(p, end);
    Serial.println("getFile OK");
    return filePath;
  }
  Serial.println("getFile FAILED after 3 attempts");
  return "";
}

// DOWNLOAD VOICE INTO PSRAM
bool downloadTelegramFile(const String& filePath) {
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient https;
  String url = "https://api.telegram.org/file/bot" + String(BOT_TOKEN) + "/" + filePath;
  Serial.println();
  Serial.println("Downloading voice...");
  if (!https.begin(client, url)) {
    Serial.println("Download HTTPS failed");
    return false;
  }
  int code = https.GET();
  Serial.print("HTTP: ");
  Serial.println(code);
  if (code < 0) {
    Serial.print("HTTP error: ");
    Serial.println(https.errorToString(code));
  }
  if (code != 200) {
    https.end();
    return false;
  }
  int expectedSize = https.getSize();
  Serial.print("Size: ");
  Serial.println(expectedSize);
  if (expectedSize <= 0) {
    https.end();
    return false;
  }
  if (audioBuffer) {
    free(audioBuffer);
    audioBuffer = nullptr;
    audioBufferSize = 0;
  }
  audioBuffer = (uint8_t*)ps_malloc(expectedSize);
  if (!audioBuffer) {
    Serial.println("PSRAM allocation FAILED!");
    https.end();
    return false;
  }
  WiFiClient* stream = https.getStreamPtr();
  size_t total = 0;
  unsigned long lastData = millis();
  while (total < (size_t)expectedSize) {
    int available = stream->available();
    if (available > 0) {
      size_t remaining = expectedSize - total;
      size_t amount = min((size_t)available, remaining);
      int n = stream->readBytes(audioBuffer + total, amount);
      if (n > 0) {
        total += n;
        lastData = millis();
      }
    }
    if (millis() - lastData > 5000) {
      Serial.println("Download timeout");
      break;
    }
    delay(1);
  }
  https.end();
  audioBufferSize = total;
  Serial.print("Downloaded: ");
  Serial.println(audioBufferSize);
  if (audioBufferSize == (size_t)expectedSize) {
    Serial.println("*** DOWNLOAD OK ***");
    return true;
  }
  Serial.println("*** DOWNLOAD FAILED ***");
  return false;
}

// OPUS PLAYBACK TASK
void opusPlaybackTask(void* parameter) {
  Serial.println();
  Serial.println("============================");
  Serial.println(" PLAYING VOICE MESSAGE");
  Serial.println("============================");
  AudioFileSourcePROGMEM file(audioBuffer, audioBufferSize);
  AudioGeneratorOpus opus;
  if (!opus.begin(&file, &audioOut)) {
    Serial.println("Opus begin FAILED!");
    playbackRunning = false;
    messageWaiting = true;

    // Actual red on board
    setLED(0, 255, 0);
    vTaskDelete(NULL);
    return;
  }
  while (opus.isRunning()) {
    if (!opus.loop()) {
      opus.stop();
      break;
    }
    vTaskDelay(1);
  }
  Serial.println();
  Serial.println("Playback finished.");
  playbackRunning = false;
  messageWaiting = false;
  ledIdle();
  vTaskDelete(NULL);
}

// START PLAYBACK
bool playVoiceMessage() {
  if (!audioBuffer || audioBufferSize == 0) {
    Serial.println("No audio available.");
    return false;
  }
  if (playbackRunning) {
    return false;
  }
  playbackRunning = true;
  setLED(255, 120, 0);
  BaseType_t result = xTaskCreatePinnedToCore(opusPlaybackTask, "opusPlayer", 32768, NULL, 1, NULL, 1);
  if (result != pdPASS) {
    Serial.println("Could not create Opus task!");
    playbackRunning = false;
    return false;
  }
  return true;
}

// CHECK TELEGRAM
void checkTelegram() {
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient https;
  String url = "https://api.telegram.org/bot" + String(BOT_TOKEN) + "/getUpdates?offset=" + String(lastUpdateId + 1) + "&timeout=0";
  if (!https.begin(client, url)) {
    return;
  }
  int code = https.GET();
  if (code != 200) {
    https.end();
    return;
  }

  String response = https.getString();
  https.end();
  int pos = response.lastIndexOf("\"update_id\":");
  if (pos < 0) {
    return;
  }
  pos += 12;
  lastUpdateId = response.substring(pos).toInt();
  int voicePos = response.indexOf("\"voice\":");
  if (voicePos < 0) {
    Serial.println("Non-voice Telegram message.");
    return;
  }
  Serial.println();
  Serial.println("*** VOICE MESSAGE RECEIVED ***");
  int filePos = response.indexOf("\"file_id\":\"", voicePos);
  if (filePos < 0) {
    Serial.println("No file_id.");
    return;
  }
  filePos += 11;
  int fileEnd = response.indexOf("\"", filePos);
  String fileId = response.substring(filePos, fileEnd);
  String filePath = getTelegramFilePath(fileId);
  if (filePath.length() == 0) {
    Serial.println("No file path.");
    return;
  }
  Serial.print("File: ");
  Serial.println(filePath);
  if (downloadTelegramFile(filePath)) {
    messageWaiting = true;

    // Actual red
    setLED(0, 255, 0);
    Serial.println();
    Serial.println("============================");
    Serial.println(" NEW VOICE MESSAGE WAITING");
    Serial.println(" PRESS BIG BUTTON TO PLAY");
    Serial.println("============================");
  }
}

// ES7210 WRITE
bool es7210Write(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(ES7210_ADDR);
  Wire.write(reg);
  Wire.write(value);
  uint8_t e = Wire.endTransmission();
  if (e != 0) {
    Serial.print("ES7210 I2C error reg 0x");
    Serial.print(reg, HEX);
    Serial.print(" code=");
    Serial.println(e);
    return false;
  }
  return true;
}

// ES7210 SETUP
bool setupES7210() {
  Serial.println("Configuring ES7210...");
  es7210Write(0x00, 0xFF);
  es7210Write(0x00, 0x32);
  es7210Write(0x09, 0x30);
  es7210Write(0x0A, 0x30);
  es7210Write(0x23, 0x2A);
  es7210Write(0x22, 0x0A);
  es7210Write(0x21, 0x2A);
  es7210Write(0x20, 0x0A);
  es7210Write(0x11, 0x60);
  es7210Write(0x12, 0x02);
  es7210Write(0x40, 0xC3);
  es7210Write(0x41, 0x70);
  es7210Write(0x42, 0x70);

  // Maximum analog microphone gain
  es7210Write(0x43, 0x1E);
  es7210Write(0x44, 0x1E);
  es7210Write(0x45, 0x1E);
  es7210Write(0x46, 0x1E);
  es7210Write(0x47, 0x08);
  es7210Write(0x48, 0x08);
  es7210Write(0x49, 0x08);
  es7210Write(0x4A, 0x08);
  es7210Write(0x07, 0x20);
  es7210Write(0x02, 0xC1);
  es7210Write(0x04, 0x01);
  es7210Write(0x05, 0x00);
  es7210Write(0x06, 0x04);
  es7210Write(0x4B, 0x0F);
  es7210Write(0x4C, 0x0F);
  es7210Write(0x00, 0x71);
  es7210Write(0x00, 0x41);
  es7210Write(0x1B, 0xBF);
  es7210Write(0x1C, 0xBF);
  es7210Write(0x1D, 0xBF);
  es7210Write(0x1E, 0xBF);
  Serial.println("ES7210 ready.");
  return true;
}

// ENTER RECORD MODE
bool enterRecordMode() {
  if (playbackRunning) {
    return false;
  }
  if (codec) {
    es8311_voice_mute(codec, true);
    es8311_delete(codec);
    codec = NULL;
  }
  tca.write1(SPEAKER_ENABLE_EXIO, LOW);
  i2s.end();
  delay(50);
  i2s.setPins(I2S_BCLK, I2S_LRCK, I2S_DOUT, I2S_DIN, I2S_MCLK);
  i2s.setTimeout(1000);
  if (!i2s.begin(I2S_MODE_STD, RECORD_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)) {
    Serial.println("I2S record begin FAILED!");
    return false;
  }
  delay(100);
  return setupES7210();
}

// RESTORE SPEAKER MODE
bool restoreSpeakerMode() {
  i2s.end();
  delay(50);
  i2s.setPins(I2S_BCLK, I2S_LRCK, I2S_DOUT, -1, I2S_MCLK);
  if (!i2s.begin(I2S_MODE_STD, DEFAULT_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)) {
    Serial.println("I2S speaker restore FAILED!");
    return false;
  }
  codec = es8311_create(I2C_NUM_0, ES8311_ADDRESS_0);
  if (!codec) {
    Serial.println("ES8311 recreate FAILED!");
    return false;
  }
  es8311_clock_config_t clock_cfg = {
    .mclk_inverted = false, .sclk_inverted = false, .mclk_from_mclk_pin = true, .mclk_frequency = DEFAULT_RATE * 256, .sample_frequency = DEFAULT_RATE };
  ESP_ERROR_CHECK(es8311_init(codec, &clock_cfg, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
  es8311_voice_volume_set(codec, SPEAKER_VOLUME, NULL);
  es8311_voice_mute(codec, false);
  tca.write1(SPEAKER_ENABLE_EXIO, HIGH);
  Serial.println("Speaker restored.");
  return true;
}

// WAV HEADER
void makeWavHeader(uint8_t* h, uint32_t dataSize) {
  memset(h, 0, 44);
  uint32_t fileSize = dataSize + 36;
  uint32_t byteRate = RECORD_RATE * RECORD_CHANNELS * (RECORD_BITS / 8);
  uint16_t blockAlign = RECORD_CHANNELS * (RECORD_BITS / 8);
  uint32_t fmtSize = 16;
  uint16_t format = 1;
  uint16_t channels = RECORD_CHANNELS;
  uint32_t rate = RECORD_RATE;
  uint16_t bits = RECORD_BITS;
  memcpy(h, "RIFF", 4);
  memcpy(h + 4, &fileSize, 4);
  memcpy(h + 8, "WAVE", 4);
  memcpy(h + 12, "fmt ", 4);
  memcpy(h + 16, &fmtSize, 4);
  memcpy(h + 20, &format, 2);
  memcpy(h + 22, &channels, 2);
  memcpy(h + 24, &rate, 4);
  memcpy(h + 28, &byteRate, 4);
  memcpy(h + 32, &blockAlign, 2);
  memcpy(h + 34, &bits, 2);
  memcpy(h + 36, "data", 4);
  memcpy(h + 40, &dataSize, 4);
}

// SEND TELEGRAM WAV
bool sendTelegramWav() {
  WiFiClientSecure client;
  client.setInsecure();
  Serial.println("Connecting to Telegram to send WAV...");
  bool connected = false;
  for (int attempt = 1;
  attempt <= 3;
  attempt++) {
    Serial.print("Telegram connect attempt ");
    Serial.println(attempt);
    if (client.connect("api.telegram.org", 443, 10000)) {
      connected = true;
      break;
    }
    Serial.println("Telegram connection FAILED");
    Serial.print("WiFi status: ");
    Serial.println(WiFi.status());
    Serial.print("RSSI: ");
    Serial.println(WiFi.RSSI());
    client.stop();
    delay(1000);
  }
  if (!connected) {
    Serial.println("Telegram connection failed after 3 attempts.");
    return false;
  }

  String boundary = "----GrandpaBoxBoundary";
  String part;
  part += "--" + boundary + "\r\n";
  part += "Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n";
  const char* targetChatId = (selectedTarget == TARGET_GRANDPARENTS) ? CHAT_ID_GRANDPARENTS : CHAT_ID_IDAN_NUFAR;
  Serial.print("Sending to: ");
  Serial.println(selectedTarget == TARGET_GRANDPARENTS ? "GRANDPARENTS" : "IDAN + NUFAR");
  part += String(targetChatId) + "\r\n";
  part += "--" + boundary + "\r\n";
  part += "Content-Disposition: form-data; name=\"document\"; filename=\"grandpa_message.wav\"\r\n";
  part += "Content-Type: audio/wav\r\n\r\n";
  String footer = "\r\n--" + boundary + "--\r\n";
  uint8_t wav[44];
  makeWavHeader(wav, recordLength);
  size_t contentLength = part.length() + 44 + recordLength + footer.length();
  String path = "/bot" + String(BOT_TOKEN) + "/sendDocument";
  client.print("POST ");
  client.print(path);
  client.println(" HTTP/1.1");
  client.println("Host: api.telegram.org");
  client.print("Content-Type: multipart/form-data; boundary=");
  client.println(boundary);
  client.print("Content-Length: ");
  client.println(contentLength);
  client.println("Connection: close");
  client.println();
  client.print(part);
  client.write(wav, 44);
  const size_t CHUNK = 4096;
  for (size_t pos = 0;
  pos < recordLength;
  ) {
    size_t amount = min(CHUNK, recordLength - pos);
    size_t sent = client.write(recordBuffer + pos, amount);
    if (sent == 0) {
      Serial.println("Upload stopped!");
      client.stop();
      return false;
    }
    pos += sent;
    delay(1);
  }
  client.print(footer);
  unsigned long started = millis();
  while (!client.available() && millis() - started < 10000) {
    delay(10);
  }
  if (!client.available()) {
    Serial.println("Telegram response timeout");
    client.stop();
    return false;
  }

  String status = client.readStringUntil('\n');
  status.trim();
  Serial.print("Telegram: ");
  Serial.println(status);
  bool ok = status.indexOf("200") >= 0;
  client.stop();
  return ok;
}

// START RECORDING
void startRecording() {
  if (messageWaiting || playbackRunning) {
    Serial.println("Cannot record while a received message is waiting/playing.");
    return;
  }
  if (!enterRecordMode()) {
    Serial.println("Could not enter RECORD mode.");
    restoreSpeakerMode();
    return;
  }
  recordLength = 0;
  recording = true;
  recordStart = millis();
  setTargetColor();
  Serial.println();
  Serial.println("============================");
  Serial.println(" RECORDING");
  Serial.print(" TARGET: ");
  Serial.println(selectedTarget == TARGET_GRANDPARENTS ? "GRANDPARENTS" : "IDAN + NUFAR");
  Serial.println(" BIG BUTTON again = STOP + SEND");
  Serial.println("============================");
}

// RECORD AUDIO - MIC1 ONLY - DIGITAL GAIN X5 - DUPLICATED TO L/R
void recordAudio() {
  if (!recording) {
    return;
  }
  uint8_t temp[1024];
  size_t n = i2s.readBytes((char*)temp, sizeof(temp));
  if (n > 0) {
    size_t remaining = MAX_RECORD_BYTES - recordLength;
    size_t amount = min(n, remaining);
    int16_t* samples = (int16_t*)temp;
    int frameCount = amount / (sizeof(int16_t) * 2);
    for (int i = 0;
    i < frameCount;
    i++) {

      // MIC1 = first I2S channel
      int16_t mic1 = samples[i * 2];
      int32_t amplified = (int32_t)mic1 * 5;
      if (amplified > 32767) {
        amplified = 32767;
      }
      if (amplified < -32768) {
        amplified = -32768;
      }
      samples[i * 2] = (int16_t)amplified;
      samples[i * 2 + 1] = (int16_t)amplified;
    }
    memcpy(recordBuffer + recordLength, temp, amount);
    recordLength += amount;
    if (recordLength >= MAX_RECORD_BYTES) {
      recording = false;
    }
  }
  if (millis() - recordStart >= MAX_RECORD_SECONDS * 1000UL) {
    recording = false;
  }
}

// STOP RECORDING + SEND
void stopRecordingAndSend() {
  recording = false;
  Serial.println();
  Serial.println("============================");
  Serial.println(" RECORDING STOPPED");
  Serial.print("Recorded bytes: ");
  Serial.println(recordLength);
  i2s.end();
  bool ok = false;
  if (recordLength >= 1000) {
    setLED(255, 120, 0);
    Serial.println("Sending WAV...");
    ok = sendTelegramWav();
  } else {
    Serial.println("Recording too short.");
  }
  if (ok) {
    Serial.println("*** MESSAGE SENT! ***");
    setLED(255, 0, 0);
  } else {
    Serial.println("*** SEND FAILED / TOO SHORT ***");

    // Actual RED
    setLED(0, 255, 0);
  }
  delay(800);
  restoreSpeakerMode();
  recordLength = 0;
  ledIdle();
  Serial.println("READY");
  Serial.println("HOLD TO 1 BEEP = GRANDPARENTS");
  Serial.println("KEEP HOLDING TO 2 BEEPS = IDAN + NUFAR");
}

// SETUP
void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.print("RESET REASON: ");
  Serial.println((int)esp_reset_reason());
  Serial.println();
  Serial.println("============================");
  Serial.println(" GRANDPA BOX - TWO TARGETS");
  Serial.println("============================");
  Serial.print("PSRAM: ");
  Serial.println(ESP.getPsramSize());
  pinMode(BIG_BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUTTON_LED_PIN, OUTPUT);
  digitalWrite(BUTTON_LED_PIN, HIGH);
  recordBuffer = (uint8_t*)ps_malloc(MAX_RECORD_BYTES);
  if (!recordBuffer) {
    Serial.println("Record PSRAM allocation FAILED!");
    while (1) {
      delay(1000);
    }
  }
  Serial.print("Record buffer allocated: ");
  Serial.println(MAX_RECORD_BYTES);
  pixels.begin();
  pixels.clear();
  pixels.show();
  ledIdle();
  if (!setupHardware()) {
    Serial.println("Hardware initialization FAILED!");
    while (1) {
      delay(1000);
    }
  }
  connectKnownWiFi();
  Serial.println();
  Serial.println("READY");
  Serial.println("HOLD BUTTON");
  Serial.println("1 BEEP = GRANDPARENTS / VIOLET");
  Serial.println("2 BEEPS = IDAN + NUFAR / YELLOW");
  Serial.println("RELEASE = START RECORDING");
}

// LOOP
void loop() {

  // WHITE BUTTON LED
  if (messageWaiting && !playbackRunning) {
    if (millis() - lastButtonLedToggle >= BUTTON_LED_BLINK_MS) {
      lastButtonLedToggle = millis();
      buttonLedState = !buttonLedState;
      digitalWrite(BUTTON_LED_PIN, buttonLedState);
    }
  } else {
    buttonLedState = HIGH;
    digitalWrite(BUTTON_LED_PIN, HIGH);
  }

  // READ BIG BUTTON
  bool bigButton = digitalRead(BIG_BUTTON_PIN);

  // BUTTON JUST PRESSED
  if (lastBigButtonState == HIGH && bigButton == LOW) {
    delay(20);
    if (digitalRead(BIG_BUTTON_PIN) == LOW) {
      Serial.println();
      Serial.println("BIG BUTTON DOWN");

      // Playback: ignore
      if (playbackRunning) {
        Serial.println("Playback in progress - button ignored.");
      }

      // Recording: - any press stops recording and sends
      else if (recording) {
        Serial.println("Stopping recording and sending...");
        stopRecordingAndSend();
      }

      // Message waiting: - play immediately
      else if (messageWaiting) {
        Serial.println("Playing waiting Telegram message...");
        playVoiceMessage();
      }

      // Idle: - start target selection
      else {
        buttonPressStartTime = millis();
        shortBeepPlayed = false;
        longPressReached = false;
        Serial.println("Hold until target beep...");
      }
    }
  }

  // BUTTON BEING HELD - 400 ms  -> one beep -> GRANDPARENTS - 1500 ms -> two beeps -> IDAN + NUFAR
  if (bigButton == LOW && !recording && !playbackRunning && !messageWaiting && buttonPressStartTime != 0) {
    unsigned long heldTime = millis() - buttonPressStartTime;

    // FIRST LEVEL - GRANDPARENTS
    if (!shortBeepPlayed && heldTime >= SHORT_BEEP_MS) {
      shortBeepPlayed = true;
      selectedTarget = TARGET_GRANDPARENTS;
      ledGrandparents();
      Serial.println();
      Serial.println("TARGET READY = GRANDPARENTS");
      Serial.println("ONE BEEP");
      beep(1);
    }

    // SECOND LEVEL - IDAN + NUFAR
    if (!longPressReached && heldTime >= LONG_PRESS_MS) {
      longPressReached = true;
      selectedTarget = TARGET_IDAN_NUFAR;
      ledParents();
      Serial.println();
      Serial.println("TARGET READY = IDAN + NUFAR");
      Serial.println("TWO BEEPS");
      beep(2);
    }
  }

  // BUTTON RELEASED - START RECORDING ONLY NOW
  if (lastBigButtonState == LOW && bigButton == HIGH) {
    delay(20);
    if (digitalRead(BIG_BUTTON_PIN) == HIGH) {
      if (!recording && !playbackRunning && !messageWaiting && buttonPressStartTime != 0) {
        unsigned long pressTime = millis() - buttonPressStartTime;
        Serial.println();
        Serial.print("Button duration: ");
        Serial.print(pressTime);
        Serial.println(" ms");

        // Too short: - no beep was reached
        if (!shortBeepPlayed) {
          Serial.println("PRESS TOO SHORT - CANCELLED");
          ledIdle();
        }

        // Valid target already selected. - Start recording NOW.
        else {
...

This file has been truncated, please download it to see its full contents.

Credits

Ruben Zilzer
8 projects • 7 followers
For nearly four decades I developed, supported, and taught digital mediaNow retired, I keep creating, experimenting and sharing projects.

Comments