Community project
Telugu AI Voice Assistant
Generated with AIThis project builds a Telugu-language AI voice assistant for cooking guidance, powered by an ESP32 microphone and speaker setup. The device captures voice commands through an I²S microphone, sends audio to a cloud backend for processing, and plays back Telugu responses through a stereo amplifier and speaker. A small TFT display shows recipe steps, dish images, and cooking timers, while a joystick provides tactile control.
The guide includes a complete wiring diagram connecting the microphone, amplifier, display, and joystick to the ESP32, a protected LiPo battery supply with charging module, and Arduino firmware that handles audio capture, WiFi communication, and display rendering. Builders will also find setup instructions for configuring the CookMate backend API and calibrating the microphone input levels.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| HiLetgo INMP441 I²S Microphone ModuleINMP441 | 1 | Omnidirectional 24-bit I²S MEMS microphone module based on the TDK InvenSense INMP441 IC. Outputs digital audio over a 3-wire I²S bus (SCK/BCLK, WS/LRCLK, SD/DOUT). Supply 1.8–3.3 V; native 3.3 V operation with no level shifting required. L/R channel select pin: tie to GND for left-channel mono output or VDD for right-channel mono output. Use the ESP32 Arduino core or ESP-IDF I2S peripheral API; no separate PlatformIO library is required. |
| PAM8403 3 W Stereo Class-D Amplifier ModulePAM8403 | 1 | Preassembled 5 V PAM8403 dual-channel audio amplifier module. Use one channel for mono speech output; its BTL speaker terminals must not be grounded. |
| 8Ω Speaker8 Ω, 1–3 W | 1 | Generic small 8Ω 0.5-3W loudspeaker (~28mm typical). Pair with an I2S amp (MAX98357A) or class-D amp (TPA3116D2) for usable volume; do not drive directly from a GPIO pin. Audio output for music/voice playback. |
| 1 µF Audio Coupling Capacitor1 µF | 1 | Non-polarized film or ceramic capacitor that AC-couples ESP32 DAC audio to the PAM8403 input. |
| ST7735 1.8 inch TFT SPI Display1.8 in, 128 x 160 | 1 | 1.8 inch SPI TFT colour display with ST7735 controller, 128x160 resolution, 3.3V logic. Communicates over hardware SPI with MOSI, SCK, CS, DC, and RST control lines. Modules typically include an onboard regulator and level shifting accepting 3.3V or 5V VCC; logic is 3.3V. MISO is optional for write-only display use. |
| KY-023 Dual Axis Joystick ModuleKY-023 | 1 | Dual-axis analog joystick breakout (PSP/PS2-style thumbstick) with two perpendicular 10 kOhm potentiometers and an integrated push-button. Outputs analog voltages on VRx and VRy proportional to stick position, plus an active-low digital switch (SW) that is pulled LOW when the stick is pressed. Operates 3.3 V to 5 V; on 5 V boards the VRx/VRy swing matches the wider ADC range. SW should be read with INPUT_PULLUP. |
| LiPo 3.7V 1000mAh Battery3.7 V, 2000 mAh | 1 | Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging. |
| TP4056 Li-Ion/LiPo charger module with protectionTP4056 + DW01 protection | 1 | TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell. |
| Boost Converter5 V, ≥2 A | 1 | Small adjustable MT3608-style DC-DC boost converter module for stepping a lower DC input up to a higher rail such as 5V, 9V, or 12V. It is a power-path module with VIN/VOUT terminals, not a GPIO peripheral. |
Assembly
5 stepsBuild the protected battery supply
Connect battery_1 positive to bms_charger_1 B+ and battery_1 negative to B-. Connect bms_charger_1 OUT+ to boost_5v_1 VIN+ and OUT- to VIN-. Set the boost converter output to exactly 5.0 V with a multimeter before connecting anything else.
- Tip: Use the TP4056 module's IN+ and IN- only for a 5 V USB charging input.
- Tip: Keep battery leads short and insulated.
- ⚠ Never reverse the Li-ion battery polarity.
- ⚠ Do not charge a swollen, damaged, or unprotected cell.
- ⚠ Do not connect ESP32 USB power while the boost output powers its VIN unless you add proper power-path isolation.
Power the ESP32, microphone, display, and joystick
Connect boost_5v_1 VOUT+ to the ESP32 VIN/5V pin and VOUT- to ESP32 GND. Use the ESP32 3.3 V rail for inmp441_1 VDD, tft_st7735_1 VCC and BLK, and joystick_1 VCC. Connect every module ground to ESP32 GND.
- Tip: All parts must share a common ground.
- Tip: Power the joystick from 3.3 V so its analog outputs remain safe for ESP32 ADC inputs.
- ⚠ Do not power the INMP441 directly from 5 V.
- ⚠ Confirm the TFT module is the ST7735/ST7735S 128×160 SPI version before wiring.
Wire microphone, display, and joystick signals
Wire inmp441_1 SCK to GPIO26, WS to GPIO27, SD to GPIO32, and L/R to GND. Wire tft_st7735_1 SCK to GPIO18, MOSI to GPIO23, CS to GPIO13, DC to GPIO14, and RST to GPIO4. Wire joystick_1 VRx to GPIO34, VRy to GPIO35, and SW to GPIO33.
- Tip: Keep the microphone wiring away from the boost converter and speaker leads to reduce noise.
- Tip: The TFT has no touch wiring; the joystick is the physical interface.
- ⚠ Do not connect a TFT MISO pin; this display design only needs SCK and MOSI plus control pins.
- ⚠ GPIO34 and GPIO35 are input-only and are correctly used for joystick analog signals.
Wire the amplifier and speaker
Connect boost_5v_1 VOUT+ to pam8403_1 VCC and VOUT- to pam8403_1 GND. Connect ESP32 GPIO25 to audio_coupling_cap_1 IN, then audio_coupling_cap_1 OUT to pam8403_1 L_IN. Connect pam8403_1 L_OUT+ to speaker_1 POS and L_OUT- to speaker_1 NEG.
- Tip: Start with the amplifier volume at minimum, then increase slowly.
- Tip: Mount the microphone away from the speaker to avoid feedback.
- ⚠ PAM8403 speaker outputs are bridge-tied: neither speaker lead may connect to GND.
- ⚠ The amplifier operates at 5 V; do not feed its VCC from the ESP32 3.3 V rail.
Use CookMate controls
After deploying, use the joystick up/down directions to select Cook by voice, Current recipe, or Cooking timer. For a cooking request, highlight Cook by voice, hold the joystick button while speaking Telugu, English, or a mix, then release it. CookMate ends early after speech silence, sends recipe context to the backend, shows the active step, speaks the returned response, and stores the current recipe and step in RAM while powered.
- Tip: Ask one cooking task at a time, for example a recipe, the next step, or a timer request.
- Tip: Use up/down on the recipe screen to review current cooking steps.
- ⚠ Recipe state is retained only until the ESP32 is restarted.
- ⚠ The backend must return the documented CookMate JSON plus optional 16 kHz PCM audio and 160×72 RGB565 dish-image URLs.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 3V3 | inmp441_1 VDD | power |
| GND | inmp441_1 GND | ground |
| GPIO 26 | inmp441_1 SCK | digital |
| GPIO 27 | inmp441_1 WS | digital |
| GPIO 32 | inmp441_1 SD | data |
| GND | inmp441_1 L/R | ground |
| 3V3 | tft_st7735_1 VCC | power |
| GND | tft_st7735_1 GND | ground |
| GPIO 18 | tft_st7735_1 SCK | spi |
| GPIO 23 | tft_st7735_1 MOSI | spi |
| GPIO 13 | tft_st7735_1 CS | spi |
| GPIO 14 | tft_st7735_1 DC | digital |
| GPIO 4 | tft_st7735_1 RST | digital |
| 3V3 | tft_st7735_1 BLK | power |
| 3V3 | joystick_1 VCC | power |
| GND | joystick_1 GND | ground |
| GPIO 34 | joystick_1 VRx | adc |
| GPIO 35 | joystick_1 VRy | adc |
| GPIO 33 | joystick_1 SW | digital |
| GPIO 25 | audio_coupling_cap_1 IN | analog |
| EXT | audio_coupling_cap_1 OUT → PAM8403 3 W Stereo Class-D Amplifier Module L_IN | analog |
| 5V | pam8403_1 VCC | power |
| GND | pam8403_1 GND | ground |
| EXT | pam8403_1 L_OUT+ → 8Ω Speaker POS | analog |
| EXT | pam8403_1 L_OUT- → 8Ω Speaker NEG | analog |
| EXT | battery_1 +V → TP4056 Li-Ion/LiPo charger module with protection B+ | power |
| EXT | battery_1 GND → TP4056 Li-Ion/LiPo charger module with protection B- | ground |
| EXT | bms_charger_1 IN+ → 5 V USB charging input positive | power |
| EXT | bms_charger_1 IN- → 5 V USB charging input ground | ground |
| EXT | bms_charger_1 OUT+ → Boost Converter VIN+ | power |
| EXT | bms_charger_1 OUT- → Boost Converter VIN- | ground |
| VIN | boost_5v_1 VOUT+ | power |
| GND | boost_5v_1 VOUT- | ground |
Firmware
ESP32#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <SPI.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include "driver/i2s.h"
#define TFT_DC 14
#define TFT_RST 4
#define MIC_LRCLK 27
#define MIC_DOUT 32
#define JOY_Y 35
#define JOY_SW 33
// CookMate backend contract (configure these after you provide your API details):
// POST /cookmate/voice: raw pcm_s16le body plus headers below.
// JSON reply: {"recipe":{"id","title","servings","steps":["..."]},
// "current_step":0,"assistant_text":"...","audio_pcm_url":"https://...",
// "dish_image_rgb565_url":"https://..."}
// audio_pcm_url returns signed 16-bit mono PCM, 16000 Hz.
// dish_image_rgb565_url returns exactly 160 x 72 RGB565 big-endian bytes.
// Hoisted type definitions
enum Screen { HOME, LISTENING, RECIPE, TIMER, ERROR_SCREEN };
// Forward declarations
void header(const char *subtitle);
void printWrap(const String &text, int x, int y, int maxChars, uint16_t color);
void tone(uint16_t hz, uint16_t ms);
void drawHome();
void drawRecipe();
void showError(const String &message);
void drawListening(bool speech);
void drawTimer();
void configureMic();
bool capturePushToTalk(int16_t *samples, size_t &count);
bool drawDishImage(const String &url);
void playPcmUrl(const String &url);
bool applyBackendReply(const String &json);
void sendCookingVoice();
void connectWiFi();
void handleJoystick();
const char *WIFI_SSID = "YOUR_WIFI_NAME";
const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char *BACKEND_URL = "https://YOUR_BACKEND_HOST/cookmate/voice";
const char *BACKEND_API_KEY = "YOUR_COOKMATE_API_KEY";
constexpr int TFT_CS = 13, TFT_DC = 14, TFT_RST = 4;
constexpr int MIC_BCLK = 26, MIC_LRCLK = 27, MIC_DOUT = 32;
constexpr int AUDIO_DAC = 25;
constexpr int JOY_X = 34, JOY_Y = 35, JOY_SW = 33;
constexpr uint32_t SAMPLE_RATE = 16000;
constexpr size_t MAX_CAPTURE_SECONDS = 6; // 192 kB, safe on a standard ESP32.
constexpr size_t MAX_SAMPLES = SAMPLE_RATE * MAX_CAPTURE_SECONDS;
constexpr uint16_t BG = ST77XX_BLACK, PANEL = 0x1082, ACCENT = ST77XX_CYAN;
constexpr uint16_t IMAGE_W = 160, IMAGE_H = 72;
constexpr int VAD_START_LEVEL = 650, VAD_STOP_LEVEL = 380;
constexpr uint32_t MIN_SPEECH_MS = 250, END_SILENCE_MS = 1200;
Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST);
Screen screen = HOME;
int homeSelection = 0;
int currentStep = 0;
int stepCount = 0;
String recipeId, recipeTitle, assistantText, recipeSteps[12];
String audioUrl, imageUrl;
bool imageAvailable = false, lastButton = HIGH;
uint32_t lastJoyMove = 0, timerEndsAt = 0;
void header(const char *subtitle) {
tft.fillRect(0, 0, 160, 21, ST77XX_BLUE);
tft.setTextSize(1); tft.setTextColor(ST77XX_WHITE, ST77XX_BLUE);
tft.setCursor(5, 4); tft.print("COOKMATE");
tft.setCursor(76, 4); tft.print(subtitle);
}
void printWrap(const String &text, int x, int y, int maxChars, uint16_t color) {
tft.setTextSize(1); tft.setTextColor(color, BG); tft.setCursor(x, y);
int col = 0;
for (size_t i = 0; i < text.length(); ++i) {
char c = text[i];
if (c == '\n' || col >= maxChars) { tft.println(); col = 0; if (c == '\n') continue; }
tft.print(c); ++col;
}
}
void tone(uint16_t hz, uint16_t ms) {
uint32_t period = 1000000UL / hz, endAt = millis() + ms;
while (millis() < endAt) { dacWrite(AUDIO_DAC, 176); delayMicroseconds(period / 2); dacWrite(AUDIO_DAC, 80); delayMicroseconds(period / 2); }
dacWrite(AUDIO_DAC, 128);
}
void drawHome() {
screen = HOME; tft.fillScreen(BG); header(WiFi.status() == WL_CONNECTED ? "ONLINE" : "OFFLINE");
tft.fillRoundRect(7, 29, 146, 36, 4, PANEL);
tft.setTextColor(ST77XX_WHITE, PANEL); tft.setTextSize(1); tft.setCursor(14, 38);
tft.print("Hold button and ask a cooking"); tft.setCursor(14, 50); tft.print("question in Telugu or English.");
const char *options[] = {"Cook by voice", "Current recipe", "Cooking timer"};
for (int i = 0; i < 3; ++i) {
int y = 74 + i * 19; uint16_t fill = i == homeSelection ? ACCENT : PANEL;
uint16_t ink = i == homeSelection ? ST77XX_BLACK : ST77XX_WHITE;
tft.fillRoundRect(9, y, 142, 16, 3, fill); tft.setTextColor(ink, fill); tft.setCursor(17, y + 4);
tft.print(i == homeSelection ? "> " : " "); tft.print(options[i]);
}
tft.setTextColor(ST77XX_YELLOW, BG); tft.setCursor(8, 136); tft.print("UP/DOWN: menu");
tft.setCursor(8, 148); tft.print("HOLD: talk PRESS: select");
}
void drawRecipe() {
screen = RECIPE; tft.fillScreen(BG); header("RECIPE");
tft.setTextColor(ST77XX_YELLOW, BG); tft.setTextSize(1); tft.setCursor(7, 27); tft.print(recipeTitle.length() ? recipeTitle : "No recipe yet");
if (!recipeTitle.length()) { printWrap("Ask CookMate for a recipe first.", 8, 48, 25, ST77XX_WHITE); return; }
if (imageAvailable) tft.drawRect(0, 34, 160, 74, ACCENT);
int textY = imageAvailable ? 112 : 42;
tft.setTextColor(ACCENT, BG); tft.setCursor(7, textY); tft.print("STEP "); tft.print(currentStep + 1); tft.print("/"); tft.print(stepCount);
printWrap(recipeSteps[currentStep], 7, textY + 12, 25, ST77XX_WHITE);
tft.setTextColor(ST77XX_YELLOW, BG); tft.setCursor(7, 148); tft.print("UP/DN STEP PRESS HOME");
}
void showError(const String &message) { screen = ERROR_SCREEN; tft.fillScreen(BG); header("CHECK"); printWrap(message, 10, 43, 24, ST77XX_RED); tft.setTextColor(ST77XX_YELLOW, BG); tft.setCursor(10, 148); tft.print("PRESS TO RETURN"); }
void drawListening(bool speech) {
screen = LISTENING; tft.fillScreen(BG); header("LISTENING");
tft.fillRoundRect(10, 40, 140, 52, 5, speech ? ST77XX_RED : PANEL);
tft.setTextColor(ST77XX_WHITE, speech ? ST77XX_RED : PANEL); tft.setTextSize(2); tft.setCursor(32, 53); tft.print(speech ? "SPEAKING" : "READY");
tft.setTextSize(1); tft.setCursor(22, 108); tft.setTextColor(ST77XX_WHITE, BG); tft.print("Hold button while talking");
tft.setCursor(18, 121); tft.print("Release to send to CookMate");
}
void drawTimer() {
screen = TIMER; tft.fillScreen(BG); header("TIMER");
if (timerEndsAt <= millis()) { tft.setTextColor(ST77XX_YELLOW, BG); tft.setTextSize(2); tft.setCursor(30, 56); tft.print("NO TIMER"); printWrap("Voice request: set a 10 minute timer", 12, 96, 24, ST77XX_WHITE); }
else { uint32_t sec = (timerEndsAt - millis()) / 1000; tft.setTextColor(ACCENT, BG); tft.setTextSize(3); tft.setCursor(33, 56); tft.printf("%02lu:%02lu", sec / 60, sec % 60); printWrap("Timer is running. CookMate will alert you.", 12, 102, 24, ST77XX_WHITE); }
tft.setTextSize(1); tft.setTextColor(ST77XX_YELLOW, BG); tft.setCursor(10, 148); tft.print("PRESS TO RETURN");
}
void configureMic() {
i2s_config_t cfg = {}; cfg.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX); cfg.sample_rate = SAMPLE_RATE;
cfg.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT; cfg.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT;
cfg.communication_format = I2S_COMM_FORMAT_I2S; cfg.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; cfg.dma_buf_count = 8; cfg.dma_buf_len = 256;
i2s_driver_install(I2S_NUM_0, &cfg, 0, nullptr);
i2s_pin_config_t pins = {}; pins.bck_io_num = MIC_BCLK; pins.ws_io_num = MIC_LRCLK; pins.data_out_num = I2S_PIN_NO_CHANGE; pins.data_in_num = MIC_DOUT;
i2s_set_pin(I2S_NUM_0, &pins); i2s_zero_dma_buffer(I2S_NUM_0);
}
bool capturePushToTalk(int16_t *samples, size_t &count) {
count = 0; bool heardSpeech = false; uint32_t speechAt = 0, silenceAt = 0, lastDraw = 0;
drawListening(false); tone(850, 45);
while (digitalRead(JOY_SW) == LOW && count < MAX_SAMPLES) {
int32_t raw[128]; size_t bytesRead = 0;
if (i2s_read(I2S_NUM_0, raw, sizeof(raw), &bytesRead, pdMS_TO_TICKS(200)) != ESP_OK) return false;
int peak = 0; size_t received = bytesRead / sizeof(int32_t);
for (size_t i = 0; i < received && count < MAX_SAMPLES; ++i) { int16_t s = (int16_t)(raw[i] >> 14); samples[count++] = s; peak = max(peak, abs((int)s)); }
bool nowSpeech = peak > VAD_START_LEVEL;
if (nowSpeech && !heardSpeech) { heardSpeech = true; speechAt = millis(); }
if (heardSpeech) { if (peak < VAD_STOP_LEVEL) { if (!silenceAt) silenceAt = millis(); } else silenceAt = 0; }
if (millis() - lastDraw > 250) { drawListening(nowSpeech); lastDraw = millis(); }
if (heardSpeech && silenceAt && millis() - silenceAt >= END_SILENCE_MS && millis() - speechAt >= MIN_SPEECH_MS) break;
}
tone(1200, 45); return heardSpeech && count >= SAMPLE_RATE / 4;
}
bool drawDishImage(const String &url) {
if (!url.length()) return false;
HTTPClient http; http.setTimeout(15000); http.begin(url); int status = http.GET();
if (status != HTTP_CODE_OK) { http.end(); return false; }
WiFiClient *stream = http.getStreamPtr(); uint8_t pair[2]; tft.startWrite();
tft.setAddrWindow(0, 35, IMAGE_W, IMAGE_H);
size_t expected = IMAGE_W * IMAGE_H * 2;
for (size_t i = 0; i < expected; ++i) { if (stream->readBytes(pair, 2) != 2) { tft.endWrite(); http.end(); return false; } tft.writePixel((pair[0] << 8) | pair[1]); }
tft.endWrite(); http.end(); return true;
}
void playPcmUrl(const String &url) {
if (!url.length()) return;
HTTPClient http; http.setTimeout(20000); http.begin(url); int status = http.GET();
if (status != HTTP_CODE_OK) { http.end(); return; }
WiFiClient *stream = http.getStreamPtr(); uint8_t bytes[256];
while (http.connected() || stream->available()) {
size_t n = stream->readBytes(bytes, sizeof(bytes));
for (size_t i = 0; i + 1 < n; i += 2) { int16_t sample = (int16_t)(bytes[i] | (bytes[i + 1] << 8)); dacWrite(AUDIO_DAC, (uint8_t)((sample >> 8) + 128)); delayMicroseconds(62); }
}
dacWrite(AUDIO_DAC, 128); http.end();
}
bool applyBackendReply(const String &json) {
JsonDocument doc; DeserializationError err = deserializeJson(doc, json); if (err) return false;
JsonObject recipe = doc["recipe"]; if (!recipe.isNull()) {
recipeId = recipe["id"] | ""; recipeTitle = recipe["title"] | "CookMate recipe"; stepCount = 0;
for (JsonVariant step : recipe["steps"].as<JsonArray>()) if (stepCount < 12) recipeSteps[stepCount++] = step.as<String>();
}
currentStep = constrain(doc["current_step"] | currentStep, 0, max(0, stepCount - 1));
assistantText = doc["assistant_text"] | "Recipe updated."; audioUrl = doc["audio_pcm_url"] | ""; imageUrl = doc["dish_image_rgb565_url"] | "";
int timerSeconds = doc["timer_seconds"] | 0; if (timerSeconds > 0) timerEndsAt = millis() + (uint32_t)timerSeconds * 1000UL;
return stepCount > 0;
}
void sendCookingVoice() {
if (WiFi.status() != WL_CONNECTED) { showError("Wi-Fi is required for CookMate."); return; }
int16_t *audio = (int16_t *)malloc(MAX_SAMPLES * sizeof(int16_t)); if (!audio) { showError("Not enough memory for audio capture."); return; }
size_t count = 0; if (!capturePushToTalk(audio, count)) { free(audio); showError("I did not hear speech. Hold the button and speak closer to the mic."); return; }
tft.fillScreen(BG); header("THINKING"); printWrap("CookMate is understanding your Telugu or English cooking request...", 12, 48, 24, ACCENT);
HTTPClient http; http.setTimeout(35000); http.begin(BACKEND_URL); http.addHeader("Content-Type", "application/octet-stream");
http.addHeader("Authorization", String("Bearer ") + BACKEND_API_KEY); http.addHeader("X-Audio-Format", "pcm_s16le"); http.addHeader("X-Sample-Rate", "16000");
http.addHeader("X-STT-Mode", "telugu-code-mixed"); http.addHeader("X-CookMate-Recipe-Id", recipeId); http.addHeader("X-CookMate-Step", String(currentStep));
int status = http.POST((uint8_t *)audio, count * sizeof(int16_t)); String reply = status == HTTP_CODE_OK ? http.getString() : ""; free(audio); http.end();
if (status != HTTP_CODE_OK || !applyBackendReply(reply)) { showError("CookMate request failed. Check the backend URL, API key, and JSON response."); return; }
imageAvailable = false; drawRecipe(); imageAvailable = drawDishImage(imageUrl); drawRecipe();
Serial.println(assistantText); playPcmUrl(audioUrl);
}
void connectWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); uint32_t start = millis(); while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) delay(250); }
void handleJoystick() {
bool button = digitalRead(JOY_SW); bool justPressed = lastButton == HIGH && button == LOW; lastButton = button;
if (screen == HOME && button == LOW && homeSelection == 0) { sendCookingVoice(); while (digitalRead(JOY_SW) == LOW) delay(10); return; }
if (justPressed) { if (screen == HOME) { if (homeSelection == 1) drawRecipe(); else if (homeSelection == 2) drawTimer(); } else drawHome(); return; }
if (screen == HOME && millis() - lastJoyMove > 220) { int y = analogRead(JOY_Y); if (y < 900 || y > 3200) { homeSelection = (homeSelection + (y < 900 ? 2 : 1)) % 3; lastJoyMove = millis(); drawHome(); } }
if (screen == RECIPE && millis() - lastJoyMove > 220 && stepCount) { int y = analogRead(JOY_Y); if (y < 900 || y > 3200) { currentStep = constrain(currentStep + (y < 900 ? -1 : 1), 0, stepCount - 1); lastJoyMove = millis(); drawRecipe(); } }
}
void setup() {
Serial.begin(115200); pinMode(JOY_SW, INPUT_PULLUP); pinMode(AUDIO_DAC, OUTPUT);
tft.initR(INITR_BLACKTAB); tft.setRotation(1); configureMic(); tft.fillScreen(BG); header("STARTING"); connectWiFi(); drawHome();
}
void loop() {
handleJoystick();
if (timerEndsAt && millis() >= timerEndsAt) { timerEndsAt = 0; tone(1000, 250); tone(1400, 250); if (screen == TIMER) drawTimer(); }
if (screen == TIMER && millis() % 1000 < 15) drawTimer();
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.