Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
| ||||||
| ||||||
| ||||||
![]() |
| |||||
Hand tools and fabrication machines | ||||||
| ||||||
My grandchildren live far away from me, so unfortunately I don't get to see them as often as I would like.
Of course, today there are many ways to stay in touch — WhatsApp, video calls, voice messages — but they all have one thing in common: they require a phone or a tablet.
My son prefers not to give the children their own phones yet, and I completely understand and respect that decision.
But that gave me an idea.
What if the children could communicate with their grandparents without having a phone at all?
I wanted something they could use completely by themselves. No screen, no apps, no menus, no contacts to select and nothing to configure.
Just a box with one big button.
That became Grandpa Box.
With a simple press of the button, my grandchildren can record a voice message and send it to us through Telegram. When we answer, the box receives the voice message and they can listen to it directly through its speaker.
Later, I added a second destination so the same box could also be used to send messages to their parents. A short press selects the grandparents, while a long press selects Mom and Dad. Different LED colors and beeps make it easy for the children to know which one was selected.
Behind that single button there is an ESP32-S3, Wi-Fi, Telegram, microphones, a speaker and quite a bit of software.
But from my grandchildren's point of view, none of that exists.
They just press a button and talk to Grandpa.
And that was exactly the idea.
Grandpa box
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.

Grandpa box
C/C++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++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.





Comments