I Made the Perfect Gadget for My Room Using ESP32 - Unihiker K10

by alaminashik in Circuits > Gadgets

157 Views, 1 Favorites, 0 Comments

I Made the Perfect Gadget for My Room Using ESP32 - Unihiker K10

Cover.png

I have always wanted to automate my room using voice commands. Also, something that easily fits with my existing switchboard, has a beautiful interface, touch controls, and can be connected to my phone.

In this project, I will describe in detail all the processes of making such a device.

The device features:

  1. Voice commands to perform various tasks. (with dual mic)
  2. Appliance control using TRIAC (1 Fan with dimmer, and 1 Light)
  3. Appliance control using Infra-Red (1 AC by cloning the remote signal)
  4. Wifi synced NTP Clock.
  5. Alarm clock.
  6. Four touch-sensitive buttons.
  7. A 2.8-inch color LCD screen.
  8. Temperature, Humidity, and Light Intensity Measurement.
  9. Auditory feedback.
  10. Mobile phone/webpage control (future implementation)

These features may seem like a lot of work, but luckily, I used the Unihiker k10, which has most of the components on board already. Made my life 10 times easier!

The processor is an ESP32-S3 with 16MB flash and 512kB SRAM. Even with all these features, it currently consumes only 30% of flash storage! The code utilizes FreeRTOS and dual-core use for a better real-world experience.

Additionally, I over-engineered the hardware parts to increase their lifespan. I will share all the details, so let's get started!

Supplies

Screenshot 2026-06-05 at 9.34.23 AM.png
Screenshot 2026-06-05 at 9.34.35 AM.png
Screenshot 2026-06-05 at 9.34.45 AM.png
Screenshot 2026-06-05 at 9.34.29 AM.png

Items needed for this project, but are not limited to:

  1. 1 x UNIHIKER K10 AI Coding Board LINK
  2. 4 x Capacitive Touch Switch TTP223B LINK
  3. 4 x 15pF capacitors LINK
  4. 1 x USB Samsung adapter 5V 2A
  5. 1 x 5mm IR LED LINK
  6. 2 x BT134-600E 4A TRIAC LINK
  7. 1 x MOV 471k LINK
  8. 2 x 275V 0.1uF suppression capacitor LINK
  9. 2 x 0.1uF 450V 104k LINK
  10. Some resistors and wires

Features and Functionalities

device annotated working.png
device annotated 2.png

I wanted to make a very practical project. With that in mind, I added multiple features. They are:

There are mainly two Pages:

  1. Home page
  2. Alarm Page


Home page

  1. shows time, WiFi, appliance status, light intensity, humidity, temperature, and alarm status.
  2. Responds to the four buttons:
  3. Alarm button:
  4. Short press: Switches between fan speeds (0 to 4) and stops the alarm if ringing.
  5. long press: toggles between home and alarm pages.
  6. AC button:
  7. Short press: Toggles AC ON and OFF with a single press.
  8. Fan button:
  9. Short press: Toggles Fan ON and OFF with a single press.
  10. Light button:
  11. Short press: Toggles Light ON and OFF with a single press.
  12. Responds to voice commands:
  13. Voice command "Show Alarm": switches to the alarm page.
  14. Voice command "Hide Alarm": switches to the main page.
  15. Voice command "Fan": Toggles fan ON and OFF.
  16. Voice command "Lights": Toggles light ON and OFF.
  17. Voice command "AC": Toggles AC ON and OFF.
  18. Voice command "Fan Full speed": sets fan speed to max speed.
  19. Voice command "Fan low speed": set the fan speed to the lowest speed.

Alarm page (24-hour format)

  1. shows time, WiFi, alarm logo, alarm time, alarm status, and button functions.
  2. Responds to the four buttons:
  3. Alarm button:
  4. Short press: stops the alarm if ringing.
  5. long press: switches to the Home page.
  6. AC button:
  7. Short press: Toggles alarm on and OFF.
  8. Fan button:
  9. Short press: increments the minute hand.
  10. Light button:
  11. Short press: increments the hour hand.
  12. Responds to voice commands: Same as Home page.

Taking Measurements

20260316_195131.jpg
switch board dimension.jpg
unihiker k10 dimension.png

Like every other project, the most important aspect of a project is making the enclosure. The project will be mounted on a regular wall socket.

I found the dimensions of my socket as 85 x 85 x 60mm (LxWxD)

Additionally, the precise dimensions and 3d model of the dev board (Unihiker K10) are available online. The maximum length of the unihiker is 83mm, which is a perfect fit for my 85mm socket.

3D Modelling & Printing

3d printing.jpg
3d printing video (1).gif

There are two parts to this device. One at the front that holds the Display, sensors, and dev board. The other is on the back side that holds the high-voltage appliance control circuitry and wiring.

I have used orange filament to create a good contrast, so the lights can pass through the enclosure. DfRobot openly published their STL files, so it was quite easy to accurately construct the structure.

The enclosure contains grooves to hold sensors and an IR LED. I had kept enough space for the appliance control circuitry.

This was printed using ABS plastic with 70% infill. Printed on Ender-3 V3-KE. Two M3 screws are used to attach the front cover to the back. It also has the Type-C port exposed, which makes it super easy to upload code via wire.

I have attached the Fusion 360 files.

STL files are uploaded to my drive: https://drive.google.com/drive/folders/1VTTiMSqH7U_fU79PbRRuitZcICIyWLob?usp=sharing

Schematic

Main circuit V1.0.png

The schematic is divided into several sections for better understanding.

  1. The top left section shows the connection of the touch sensors. The VCC is connected to 3.3V, GND is connected to GND, and the outputs (TouchA, TouchB, etc.) are connected to the corresponding ESP32 GPIO pins.
  2. The section below is the IR LED control section. An infrared LED can consume a large current during operation. So, it is driven by an A03400 N-channel MOSFET. A series 20R resistor is connected as a current-limiting resistor. The GPIO_IR net drives the MOSFET gate.
  3. The section below is the 220V mains power input section. The input power has a 0.1uF 275V Myler capacitor for filtering.
  4. The Top right section is the TRIAC circuitry. I found that BT134-600 is enough to control a household light and fan. Note that a microcontroller cannot directly run a TRIAC, so a TRIAC driver, MOC3041, is used in this case. It is like an optocoupler. It is important to add protection circuitry since the fan and lights can easily damage the TRIAC. A snubber circuit using a 220R 1W resistor and 104j 630 capacitor is used in parallel to the TRIAC's cathode and anode. Additionally, I added a 471k Varistor (MOV) across the fan terminal to protect the circuit from any surge voltage.
  5. Finally, the bottom right section shows all the connections of the edge connector of the Unihiker K10. Note that for infrared and LCD backlight functions, I was not able to utilize the edge connector. So the GPIO_IR and LCD_BLK are connected directly on the ESP32 pins at P0 and P1. Details of this modification are given in the following steps.

Adding the Touch Sensors

cap conn to sensor.jpg
touch sensor works.jpg
placing sensor gif.gif

I used the very common TTP223B touch sensors. These are easy to connect and can work from a distance. However, the default sensitivity causes the sensor to trigger when my hand is 2-3cm away from the sensor pad.

So to reduce the sensitivity, a 15pF capacitor is used as shown in the image. I realized that this value is close to human capacitance and thus reduces sensitivity to a few mm.

Adjusting the Unihiker K10 to Fit

connector.png
edge conn connection.png
removing cam gif.gif

I had to modify the main board to fit in the enclosure. At first, I removed the camera to make room for the 5V adapter (discussed in the later part).

The edge connector had a flaw! The pins that are facing the display are connected to the main board. The edge connector of the Unihiker has a connection only on the front side, which I cannot access.

The pins of the edge connector at the back are not connected to anything. Since I won't be able to access the front pins, I had to solder the front pins to their corresponding pins at the back. This way, I can access the useful pins even when the Unihiker k10 is screwed to the enclosure. See the first two images for better understanding.

Building the IR Circuitry

ir schematic.png
ir circuit.jpg
ir led conn.jpg
ir led.jpg

As discussed earlier, an infrared LED can consume a large current during operation. So, it is driven by an A03400 N-channel MOSFET. A series 20R resistor is connected as a current-limiting resistor. The GPIO_IR net drives the MOSFET gate. This net is directly connected to the GPIO pin of the ESP32 (See the third image)

The whole IR circuit came out to be very small and cute.

Connecting the Adapter

placing charger gif.gif
charger side view.jpg

Luckily, the unihiker board has a screw mount on the back where I could place the charger. Even though I had to carefully drill a hole in the adapter PCB to fit a screw.

The GPIO pins are used to take input from touch sensors and output to the TRIAC circuit, IR LED driver, and LCD back light.

Front Panel Complete

connector 2.png

Finally, it was pretty simple to place all the components and glue them together. The unihiker did not require any glue since it fits properly, and the Type C housing holds it in place.

Building Power Circuitry

osc testing triac.jpg
power circuitry schematic.png
triac circuit.jpg
pcb back side.jpg

The TRIAC circuit contains two 4-ampere TRIACs to control the lights and fan. I could've used relays, but it would take too much space, and the fan would no longer have speed control.

As discussed in step 4, for protection, I added a snubber circuit using a 104J 630V capacitor in series with 220ohm resistors for both lights and the fan. For the EMI filter from the fan, a 14mH toroidal inductor is used in series. Additionally, a 471k MOV is used to reduce spikes that may induce from the fan.

Note: You can also use the MOC3021 TRIAC driver instead of MOC3041, but in that case, you need an external ZCD circuit, because without it, the fan will behave unexpectedly at lower speeds.

Protection

pcb back side.jpg
protection tape.png
connector 2.png

Keep in mind that we are working with mains voltage, which can be dangerous. I used electrical tape to insulate the adapter and double-sided tape for the TRIAC circuitry.

This is not only dangerous but also can damage other components permanently. Always take precautions.

Connecting LCD Backlight to ESP32 GPIO

testing backlight.gif
lcd backlit replace 1.jpg
lcd backlit replace 2.jpg
lcd backlit replace 3.jpg

I wanted the LCD backlight to auto-adjust its brightness based on ambient light intensity. But the backlight is connected via the IO expander, and I was unable to control it via the LEDC library. (I am not that great of a coder :)) So, I removed the on-board tiny 2k resistor that drives the backlight transistor. Later, I connected that transistor base to the P1 pin via a 2.2k resistor. (look at 2nd and 3rd image)

The P1 pin is a direct GPIO of the ESP32 and supports LEDC PWM. Thus, I was able to control the brightness of the backlight based on light intensity using code. PWM is basically turning the backlight on and off very frequently.

This was important for me since the display can get quite bright, especially at night.

Connecting the Back Panel With the Front Panel

placing component1.png
connectors annotated.png
connecting screws.gif

This is a very important step. The connections are pretty simple. There is a 3-pin JST connector and one 2-pin power connector connecting the front panel to the back panel. The front and back panel connections are:

Front Panel -------- Back panel

GND -------- GND

ep9 -------- MOC3041 (Light)

ep10 -------- MOC3041 (Fan)

Live (adapter) -------- Live (outlet)

Neutral (adapter) -------- Neutral (outlet)

Follow the main schematic from step 4 to verify the connections.

Replacing Old Switch Board

placing on the wall mount.png
assemble view.gif

Finally, the device is complete. I simply removed the old switchboard and used its screws to mount the device.

4 wires are coming out of our device. They are live, neutral, triac-1, and triac 2. These 4 wires are connected to the outlet according to the following connections:

Back panel -------- Outlet

TRIAC-1 (MT2) -------- Light (~40W)

TRIAC-2 (MT2) -------- Fan (~150W)

Live --------Live

Neutral --------Neutral

Note that one end of the light/fan is connected to our device, and the other end is connected to neutral. When turned on, it completes the circuit.

Be careful, while working with high voltage, there is a risk of getting shocked!

Designing the Interface

Ai interface graphics.png
Screenshot 2026-06-25 at 8.53.46 AM.png
parameters to generate c code from image unihiker k10.png

Now it's time to power the device and code!

Like every other project, planning is very important before coding. I wanted the UI to look modern and feature-rich. So, I created a canvas with 240x320 pixels in Illustrator. The dimension matches the actual display. It helps to get the coordinates of the individual icon to be placed on the screen.

To convert an icon into C code, I used a free resource: https://notisrac.github.io/FileToCArray/. Upload the image, set the code format to Decimal, palette mod to 2byte/pixel (for color), and uint8_t data type.

The background is a solid blue rectangle, and the icons also have a blue background. I could not get a transparent background using C code. Let me know if there are better ways for this!

Code Part 1 - Void Setup()

The whole code is divided into several sections, and the most important is the Void setup() part.

The code under setup() :

  1. Initialize the libraries.
  2. Create the semaphores for FreeRTOS.
  3. Connects to wifi and gets time from pool.ntp.org
  4. Add the voice commands.
  5. Set the input and output pins.
  6. Create several tasks to perform various functions.


void setup() {
k10.begin();
Serial.begin(115200);
irsend.begin();

// ── Create ALL mutexes before any task is spawned ─────────────────────
stateMutex = xSemaphoreCreateMutex();
irMutex = xSemaphoreCreateMutex();
AlarmMutex = xSemaphoreCreateMutex();

// ── WiFi ──────────────────────────────────────────────────────────────
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("Connecting to WiFi");
for (int n = 20; n > 0 && WiFi.status() != WL_CONNECTED; n--) {
vTaskDelay(pdMS_TO_TICKS(500));
Serial.print('.');
}
configTime(6 * 3600, 0, "pool.ntp.org", "time.nist.gov");

// ── ASR ───────────────────────────────────────────────────────────────
asr.asrInit(1, EN_MODE, 3000);
while (asr._asrState == 0) {
Serial.print('.');
vTaskDelay(pdMS_TO_TICKS(100));
}
asr.addASRCommand(0+1, const_cast<char*>("Lights"));
asr.addASRCommand(1+1, const_cast<char*>("Fan"));
asr.addASRCommand(2+1, const_cast<char*>("AC"));
asr.addASRCommand(3+1, const_cast<char*>("Show Alarm"));
asr.addASRCommand(4+1, const_cast<char*>("Hide Alarm"));
asr.addASRCommand(5+1, const_cast<char*>("Fan Full Speed"));
asr.addASRCommand(6+1, const_cast<char*>("Fan Low Speed"));


// ── GPIO ──────────────────────────────────────────────────────────────
pinMode(LeftTopButton, INPUT_PULLDOWN);
pinMode(LeftBottomButton, INPUT_PULLDOWN);
pinMode(RightTopButton, INPUT_PULLDOWN);
pinMode(RightBottomButton,INPUT_PULLDOWN);
pinMode(TriacLight, OUTPUT);
pinMode(TriacFan, OUTPUT);
digital_write(TriacLight, LOW);
digital_write(TriacFan, LOW);

// ── Display ───────────────────────────────────────────────────────────
k10.initScreen(screen_dir);
k10.creatCanvas();
k10.canvas->canvasSetLineWidth(1);
k10.setScreenBackground(backGroundColor); // draw background once at startup

//setup backlight PWM (display backlight is wired to P1 via a transistor, so we can use PWM to control brightness if desired. For now we just turn it fully on)
ledcSetup(2, 5000, 8); // channel 0, 5 KHz, 8-bit resolution
ledcAttachPin(BackLED, 2); // attach pin to channel 0

// ── AHT sensor ───────────────────────────────────────────────────────
if (!aht.begin()) Serial.println("AHT10/AHT20 not found");

// ── Tasks ─────────────────────────────────────────────────────────────
Serial.print("Free heap before ButtonTasks: "); Serial.println(ESP.getFreeHeap());
xTaskCreatePinnedToCore(ButtonTasks, "ButtonTasks", 1024*8, NULL, 5, NULL, 0);
Serial.print("Free heap before VoiceTasks: "); Serial.println(ESP.getFreeHeap());
xTaskCreatePinnedToCore(VoiceTasks, "VoiceTasks", 1024*3, NULL, 5, NULL, 1);
Serial.print("Free heap before UITasks: "); Serial.println(ESP.getFreeHeap());
xTaskCreatePinnedToCore(UITasks, "UITasks", 1024*8, NULL, 5, NULL, 1);
Serial.print("Free heap before alarm tasks: "); Serial.println(ESP.getFreeHeap());
xTaskCreatePinnedToCore(AlarmTasks, "AlarmTasks", 1024*5, NULL, 5, NULL, 1);
}

Code Part 1

To keep the code understandable for everyone, I kept the whole code in a single file: "Main.cpp". Additionally, many comments are added to clearly explain what each line/code means.

I will describe the major portion of the code in detail:

The functions necessary for codes:

  1. Function to toggle the light state. A simple code to toggle the light state from high to low or vice versa.
void toggleLight() {
xSemaphoreTake(stateMutex, portMAX_DELAY);
TriacLightState = !TriacLightState;
digital_write(TriacLight, TriacLightState ? HIGH : LOW);
xSemaphoreGive(stateMutex);
}
  1. Function to toggle the state of the fan; this is similar to the toggleLight function.
void toggleFan() {
xSemaphoreTake(stateMutex, portMAX_DELAY);
TriacFanState = !TriacFanState; //only triggers the RunFanSpeed active/inactive
xSemaphoreGive(stateMutex);
}
  1. Function to switch between difference speed of the fan but turning on/off the optocoupler.
void RunFanSpeed(uint8_t speed){
static bool optoState = false; //must be static
long optoOnTime; // Time optocoupler in on
long optoOffTime; // Time optocoupler is off

static unsigned long lastToggle = 0;
unsigned long now = millis();

switch (speed) {
case 0: //off
digital_write(TriacFan, LOW); // fully off
return;
case 1: //low
optoOnTime = 100;
optoOffTime = 200;
break;
case 2: //medium
optoOnTime = 200;
optoOffTime = 100;
break;
case 3: //high
optoOnTime = 300;
optoOffTime = 80;
break;
case 4: //full on
digital_write(TriacFan, HIGH);
return;
}
long interval = optoState ? optoOnTime : optoOffTime;

if (now - lastToggle >= interval && speed != 4) {
lastToggle = now;
optoState = !optoState;
digital_write(TriacFan, optoState); // Toggle the fan state
}
}
  1. Function to toggle the AC on or off. Additionally, this function generates the infrared signal by suspending all the tasks on this core; without this, the correct infrared signal will not be generated.
void toggleAC() {
xSemaphoreTake(stateMutex, portMAX_DELAY);
bool newState = !ACState;
xSemaphoreGive(stateMutex);

xSemaphoreTake(irMutex, portMAX_DELAY);
//suspend all other tasks on this code during transmission (very important)
vTaskSuspendAll();
if (newState) {
irsend.sendRaw(AC_ONrawData, 197, 38);
Serial.println("IR → AC ON");
} else {
irsend.sendRaw(AC_OFFrawData, 197, 38);
Serial.println("IR → AC OFF");
}
xTaskResumeAll();
xSemaphoreGive(irMutex);

xSemaphoreTake(stateMutex, portMAX_DELAY);
ACState = newState;
xSemaphoreGive(stateMutex);
}
  1. Function to generate a PWM signal for adjusting the brightness of baclkight led of the display. The lowest brightness is 0, but the minimum brightness is 50. Maximum brightness is 255.
void setBacklight(uint16_t lux) {
uint8_t duty;
if (lux < minLightIntensity) {
duty = 0; // display off
} else if (lux >= maxLightIntensity) {
duty = 255; // full brightness
} else {
// linear ramp
// minimum 50 (not 0) so the screen is always barely visible at lux=minLightIntensity
duty = (uint8_t)(50 + ((lux - minLightIntensity) * (255 - 50)) / (maxLightIntensity - minLightIntensity));
}
ledcWrite(BackLED, duty);
}
  1. Function to draw intensity level using graphical vertical lines based on light intensity, temperature, and humidity.
void drawVerticalBar(int x, int y, float value, float minVal, float maxVal, const char* label, const char* unit, int barHeight = 10) {
// Clamp & compute how many filled bars
float clamped = constrain(value, minVal, maxVal);
int filled = (int)roundf((clamped - minVal) / (maxVal - minVal) * barHeight);

// Draw label + numeric value above the bar
char header[20];
snprintf(header, sizeof(header), "%s:%.1f%s", label, value, unit);
k10.canvas->canvasText(header, x, y, 0xFFFFFF, k10.canvas->eCNAndENFont16, strlen(header), false);

// Build the bar string: filled = '|', empty = '.'
char bar[barHeight + 1];
for (int i = 0; i < barHeight; i++)
bar[i] = (i < filled) ? '|' : '.';
bar[barHeight] = '\0';
k10.canvas->canvasText(bar, x, y +18 , 0x00FF88, k10.canvas->eCNAndENFont24, barHeight, false);
}
  1. A function to enter the wifi configuration portal to change the WiFi credentials.
void Configure_WIFI() {
wm.resetSettings();
wm.setConfigPortalTimeout(100);
wm.setShowInfoErase(false);
wm.setClass("invert");
bool res = wm.autoConnect(WIFI_SSID_DEBUG, WIFI_PASS_DEBUG);
if (!res) Serial.println("WiFi portal: failed to connect");
else Serial.println("New WiFi saved successfully");
wm.stopConfigPortal();
}


The entire code is divided into different tasks. I utilized FreeRTOS to divide these functions into various tasks. These functions are called under various tasks. The tasks are mentioned in step-16.

To read more about FreeRTOS, you can check the article: https://www.freertos.org/Documentation/01-FreeRTOS-quick-start/01-Beginners-guide/00-Overview

Uploading the Code

The code utilizes FreeRTOS and the dual-core processor is used for a seamless user interface. If you don't know about FreeRTOS, think of it as having multiple "void loop ()" running in synchrony.

The code mainly has the following parts:

  1. Some pre-defined functions that can perform simple tasks (toggle appliance, IR data send, draw graphics, etc)
  2. 4 tasks to handle:
  3. button interfacing
  4. Voice detection
  5. Alarm task
  6. UI task
  7. Three semaphores to transfer data between the tasks.

The code is written in a single script in arduino framework. Some header files such as icons, are also used. You can find those from my github: https://github.com/AlAminAshik/Multipurpose-Home-Automation-Socket-Using-Unihiker-K10

#include "unihiker_k10.h"
#include <IRsend.h>
#include "asr.h"
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <WiFiManager.h>
#include <time.h>
#define sensor_t adafruit_sensor_t
#include <Adafruit_AHTX0.h>
#undef sensor_t
#include "RedOffSwitchIcon.h"
#include "GreenOnSwitchIcon.h"
#include "alarmLogo.h"
#include "noWifiIcon.h"
#include "wifiIcon.h"

Adafruit_AHTX0 aht;
WiFiManager wm;

#define WIFI_SSID "Amin Residence GP"
#define WIFI_PASS "alamiralif86"
#define WIFI_SSID_DEBUG "Smart Socket by Alamin"
#define WIFI_PASS_DEBUG "87654321"

UNIHIKER_K10 k10;
uint8_t screen_dir = 2;
uint16_t lightIntensity;
#define BackLED P1 //this pin was soldered with the backled transistor of the display since eLCD_BLK pin interferes with the voice

ASR asr;
Music music;

const uint16_t kIrLed = P0; // this pin was soldered with the IR LED on the back of the board
IRsend irsend(kIrLed);

uint16_t AC_ONrawData[197] = {6058, 7340, 608, 1684, 586, 1684, 584, 1682, 586, 1682, 586, 1682, 584, 1684, 586, 1684, 586, 1682, 586, 564, 586, 566, 582, 568, 584, 566, 584, 566, 584, 566, 584, 564, 586, 566, 582, 1684, 584, 1686, 582, 1684, 584, 1684, 582, 1688, 582, 1684, 584, 1686, 582, 1684, 584, 566, 586, 564, 582, 566, 584, 574, 576, 566, 584, 574, 576, 566, 584, 566, 586, 1684, 582, 1692, 578, 1682, 586, 1682, 584, 1684, 584, 1682, 584, 1684, 584, 1684, 586, 564, 584, 566, 586, 564, 586, 566, 584, 568, 584, 564, 584, 566, 586, 564, 582, 1688, 584, 564, 586, 564, 586, 1684, 584, 564, 586, 564, 586, 562, 586, 1682, 586, 564, 586, 1684, 584, 1684, 582, 566, 582, 1686, 588, 1680, 558, 1710, 586, 564, 584, 1686, 582, 1684, 584, 576, 576, 568, 580, 1686, 586, 564, 584, 1684, 582, 1684, 584, 568, 586, 564, 584, 1684, 584, 1684, 586, 562, 586, 1684, 582, 566, 586, 564, 586, 564, 586, 1684, 584, 564, 584, 1692, 574, 568, 586, 1682, 584, 568, 584, 1682, 586, 1684, 584, 566, 586, 1684, 584, 566, 582, 1686, 582, 566, 584, 1684, 584, 564, 586, 7386, 586};
uint16_t AC_OFFrawData[197] = {6094, 7330, 618, 1654, 620, 1650, 614, 1650, 620, 1654, 616, 1652, 616, 1650, 618, 1650, 618, 1650, 616, 536, 614, 536, 616, 536, 614, 538, 612, 536, 614, 534, 616, 536, 614, 536, 616, 1654, 614, 1650, 616, 1654, 616, 1654, 614, 1652, 616, 1654, 616, 1652, 612, 1658, 612, 534, 616, 534, 616, 538, 586, 562, 612, 538, 612, 536, 612, 540, 610, 536, 614, 1652, 616, 1652, 614, 1656, 612, 1654, 614, 1654, 616, 1652, 616, 1654, 614, 1650, 616, 536, 614, 536, 616, 534, 616, 534, 614, 534, 616, 536, 614, 534, 616, 536, 614, 1656, 614, 1654, 612, 536, 616, 1654, 612, 538, 614, 534, 616, 536, 614, 1654, 616, 536, 612, 536, 616, 1652, 612, 536, 588, 1682, 612, 1656, 612, 1654, 588, 566, 610, 1654, 612, 1658, 612, 540, 612, 538, 612, 1656, 612, 536, 586, 1684, 612, 1652, 612, 538, 616, 538, 610, 1654, 614, 1656, 612, 538, 614, 1654, 614, 536, 614, 536, 612, 536, 614, 1656, 612, 538, 612, 1654, 616, 536, 610, 1660, 612, 538, 612, 1652, 618, 1652, 614, 538, 612, 1656, 612, 536, 616, 1652, 614, 536, 614, 1652, 616, 536, 614, 7354, 618};

// ── Button & output pins ──────────────────────────────────────────────────────
#define LeftTopButton eP5_KeyA
#define LeftBottomButton eP4
#define RightTopButton eP6
#define RightBottomButton eP8
#define TriacLight eP9
#define TriacFan eP10
//#define BaclLED eLCD_BLK //using this pin blocks voice, idk why
uint8_t fanSpeed = 4; // fan speed changes from 0 to 4

// ── Page enum ─────────────────────────────────────────────────────────────────
enum Page { PAGE_MAIN, PAGE_ALARM};
static volatile Page currentPage = PAGE_MAIN;

// Alarm state ───────────────────────────────────────────────────────────────
static volatile int alarmHour = 6; // default alarm time 6:00 AM, can be changed via buttons on the alarm page
static volatile int alarmMinute = 0;
static volatile bool alarmEnabled = false;
static volatile bool alarmFired = false;
static volatile bool alarmRunning = false;
// Local variable to track whether we already started the alarm sound
static bool alarmSoundActive = false;

// Shared appliance state ────────────────────────────────────────────────────
static volatile bool TriacLightState = false;
static volatile bool TriacFanState = false;
static volatile bool ACState = false;

// Semaphores / mutexes ──────────────────────────────────────────────────────
static SemaphoreHandle_t stateMutex = NULL; // protects appliance states + page
static SemaphoreHandle_t irMutex = NULL; // protects irsend
static SemaphoreHandle_t AlarmMutex = NULL; // protects alarm state

//Page-change tracking (UI task only, no mutex needed) ─────────────────────
static volatile bool backgroundNeedsRedraw = true; // force draw on first frame

// ─────────────────────────────────────────────────────────────────────────────
void toggleLight() {
xSemaphoreTake(stateMutex, portMAX_DELAY);
TriacLightState = !TriacLightState;
digital_write(TriacLight, TriacLightState ? HIGH : LOW);
xSemaphoreGive(stateMutex);
}

// ─────────────────────────────────────────────────────────────────────────────
void toggleFan() {
xSemaphoreTake(stateMutex, portMAX_DELAY);
TriacFanState = !TriacFanState; //only triggers the RunFanSpeed active/inactive
xSemaphoreGive(stateMutex);
}

void RunFanSpeed(uint8_t speed){
static bool optoState = false; //must be static
long optoOnTime; // Time optocoupler in on
long optoOffTime; // Time optocoupler is off

static unsigned long lastToggle = 0;
unsigned long now = millis();

switch (speed) {
case 0: //off
digital_write(TriacFan, LOW); // fully off
return;
case 1: //low
optoOnTime = 100;
optoOffTime = 200;
break;
case 2: //medium
optoOnTime = 200;
optoOffTime = 100;
break;
case 3: //high
optoOnTime = 300;
optoOffTime = 80;
break;
case 4: //full on
digital_write(TriacFan, HIGH);
return;
}
long interval = optoState ? optoOnTime : optoOffTime;

if (now - lastToggle >= interval && speed != 4) {
lastToggle = now;
optoState = !optoState;
digital_write(TriacFan, optoState); // Toggle the fan state
}
}

// ─────────────────────────────────────────────────────────────────────────────
void toggleAC() {
xSemaphoreTake(stateMutex, portMAX_DELAY);
bool newState = !ACState;
xSemaphoreGive(stateMutex);

xSemaphoreTake(irMutex, portMAX_DELAY);
//suspend all other tasks on this code during transmission (very important)
vTaskSuspendAll();
if (newState) {
irsend.sendRaw(AC_ONrawData, 197, 38);
Serial.println("IR → AC ON");
} else {
irsend.sendRaw(AC_OFFrawData, 197, 38);
Serial.println("IR → AC OFF");
}
xTaskResumeAll();
xSemaphoreGive(irMutex);

xSemaphoreTake(stateMutex, portMAX_DELAY);
ACState = newState;
xSemaphoreGive(stateMutex);
}

// ─────────────────────────────────────────────────────────────────────────────
void setBacklight(uint16_t lux) {
uint8_t duty;
if (lux < 2) {
duty = 0; // display off
} else if (lux >= 15) {
duty = 255; // full brightness
} else {
// linear ramp
// minimum 50 (not 0) so the screen is always barely visible at lux=2
duty = (uint8_t)(50 + ((lux - 2) * (255 - 50)) / (15 - 2));
}
ledcWrite(BackLED, duty);
}

// ─────────────────────────────────────────────────────────────────────────────
// Helper – draws a vertical bar gauge using pipe characters
// x, y = top-left of the bar area
// value = current reading
// minVal, maxVal = scale range
// label = e.g. "Lux", "Hum", "Tmp"
// unit = e.g. "%", "C", ""
// barHeight = total number of '|' characters at full scale
void drawVerticalBar(int x, int y, float value, float minVal, float maxVal, const char* label, const char* unit, int barHeight = 10) {
// Clamp & compute how many filled bars
float clamped = constrain(value, minVal, maxVal);
int filled = (int)roundf((clamped - minVal) / (maxVal - minVal) * barHeight);

// Draw label + numeric value above the bar
char header[20];
snprintf(header, sizeof(header), "%s:%.1f%s", label, value, unit);
k10.canvas->canvasText(header, x, y, 0xFFFFFF, k10.canvas->eCNAndENFont16, strlen(header), false);

// Build the bar string: filled = '|', empty = '.'
char bar[barHeight + 1];
for (int i = 0; i < barHeight; i++)
bar[i] = (i < filled) ? '|' : '.';
bar[barHeight] = '\0';
k10.canvas->canvasText(bar, x, y +18 , 0x00FF88, k10.canvas->eCNAndENFont24, barHeight, false);
}

// ─────────────────────────────────────────────────────────────────────────────
void Configure_WIFI() {
wm.resetSettings();
wm.setConfigPortalTimeout(100);
wm.setShowInfoErase(false);
wm.setClass("invert");
bool res = wm.autoConnect(WIFI_SSID_DEBUG, WIFI_PASS_DEBUG);
if (!res) Serial.println("WiFi portal: failed to connect");
else Serial.println("New WiFi saved successfully");
wm.stopConfigPortal();
}
// ─────────────────────────────────────────────────────────────────────────────

// TASKS ─────────────────────────────────────────────────────────────────────────────
// TASK: VoiceTasks
// ─────────────────────────────────────────────────────────────────────────────
void VoiceTasks(void *pvParameters) {
while (true) {
if (asr.isWakeUp()) {
music.playMusic(Melodies::BA_DING, OnceInBackground);
asr._wakeUp = false;
}
if (asr.isDetectCmdID(0+1)) {
asr._isDetectCmdID = 0;
music.playMusic(Melodies::BA_DING, OnceInBackground);
toggleLight();
} else if (asr.isDetectCmdID(1+1)) {
asr._isDetectCmdID = 0;
music.playMusic(Melodies::BA_DING, OnceInBackground);
toggleFan();
} else if (asr.isDetectCmdID(2+1)) {
asr._isDetectCmdID = 0;
music.playMusic(Melodies::BA_DING, OnceInBackground);
toggleAC();
}
else if (asr.isDetectCmdID(3+1)) {
asr._isDetectCmdID = 0;
music.playMusic(Melodies::BA_DING, OnceInBackground);
xSemaphoreTake(stateMutex, portMAX_DELAY);
currentPage = PAGE_ALARM;
xSemaphoreGive(stateMutex);
backgroundNeedsRedraw = true;
}
else if (asr.isDetectCmdID(4+1)) {
asr._isDetectCmdID = 0;
music.playMusic(Melodies::BA_DING, OnceInBackground);
xSemaphoreTake(stateMutex, portMAX_DELAY);
currentPage = PAGE_MAIN;
xSemaphoreGive(stateMutex);
backgroundNeedsRedraw = true;
}
else if (asr.isDetectCmdID(5+1)) {
asr._isDetectCmdID = 0;
music.playMusic(Melodies::BA_DING, OnceInBackground);
xSemaphoreTake(stateMutex, portMAX_DELAY);
fanSpeed = 4; // full speed
xSemaphoreGive(stateMutex);
Serial.println("Fan set to full speed via voice command");
}
else if (asr.isDetectCmdID(6+1)) {
asr._isDetectCmdID = 0;
music.playMusic(Melodies::BA_DING, OnceInBackground);
xSemaphoreTake(stateMutex, portMAX_DELAY);
fanSpeed = 1; // low speed
xSemaphoreGive(stateMutex);
Serial.println("Fan set to low speed via voice command");
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}

// TASK: ButtonTasks
// ─────────────────────────────────────────────────────────────────────────────
void ButtonTasks(void *pvParameters) {
bool prevLeftTop = false;
bool prevLeftBottom = false;
bool prevRightTop = false;
bool prevRightBottom = false;

bool lbHeld = false;
uint32_t lbPressTime = 0;
bool lbLongFired = false;

while (true) {
bool curLeftTop = digital_read(LeftTopButton);
bool curLeftBottom = digital_read(LeftBottomButton);
bool curRightTop = digital_read(RightTopButton);
bool curRightBottom = digital_read(RightBottomButton);

bool pressedLeftTop = curLeftTop && !prevLeftTop;
bool pressedLeftBottom = curLeftBottom && !prevLeftBottom;
bool pressedRightTop = curRightTop && !prevRightTop;
bool pressedRightBottom = curRightBottom && !prevRightBottom;
bool releasedLeftTop = !curLeftTop && prevLeftTop;

Page page;
xSemaphoreTake(stateMutex, portMAX_DELAY);
page = currentPage;
xSemaphoreGive(stateMutex);

// ── LeftBottomButton ─────────────────────────────────────────────────
// AC functionality in main page and alarm toggle in alarm page
if (pressedLeftBottom) {
if (page == PAGE_MAIN) {
toggleAC();
} else {
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
alarmEnabled = !alarmEnabled;
if (alarmEnabled) alarmFired = false; // re-arm when enabling
xSemaphoreGive(AlarmMutex);
}
}

// ── RightBottomButton ─────────────────────────────────────────────
// toggle fan in home page and increase minute in alarm page
if (pressedRightBottom) {
if (page == PAGE_MAIN) {
toggleFan();
} else {
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
alarmMinute = (alarmMinute + 2) % 60;
alarmFired = false; // time changed, re-arm
xSemaphoreGive(AlarmMutex);
}
}

// ── LeftTopButton ──────────────────────────────────────────────
// switch pages and alarm off in both pages with long-press and short-press respectively and speed control
if (pressedLeftTop) {
lbHeld = true;
lbPressTime = millis();
lbLongFired = false;
}
//if pressed for long time, toggle page. if released before long-press threshold, toggle alarm (if on alarm page) or change fan speed (if on main page)
if (lbHeld && curLeftTop) {
if (!lbLongFired && (millis() - lbPressTime >= 1000)) {
// long press, alarm was not running switch page
lbLongFired = true;
xSemaphoreTake(stateMutex, portMAX_DELAY);
currentPage = (currentPage == PAGE_MAIN) ? PAGE_ALARM : PAGE_MAIN;
xSemaphoreGive(stateMutex);
backgroundNeedsRedraw = true;
Serial.println(currentPage == PAGE_MAIN ? "Page: MAIN" : "Page: ALARM");
}
}
// short press action (only if not long-pressed): toggle alarm if on alarm page, change fan speed if on main page
if (releasedLeftTop) {
// Dismiss alarm if running (check before page-toggle)
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
bool running = alarmRunning;
if (running) {
alarmRunning = false; //stop alarm
alarmFired = true; // prevent re-fire this minute
}
xSemaphoreGive(AlarmMutex);

//change speed of fan at short presses when fan is on.
if (page == PAGE_MAIN && TriacFanState == true) {
xSemaphoreTake(stateMutex, portMAX_DELAY);
fanSpeed = (fanSpeed % 4) + 1; // cycle through 1,2,3,4 (0 is off, 4 is full on)
xSemaphoreGive(stateMutex);
}

lbHeld = false;
}

// ── RightTopButton ────────────────────────────────────────────────
// toggle light in home page and increase hour in alarm page
if (pressedRightTop) {
if (page == PAGE_MAIN) {
toggleLight();
} else {
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
alarmHour = (alarmHour + 1) % 24;
alarmFired = false; // time changed, re-arm
xSemaphoreGive(AlarmMutex);
}
}

// ── enter config mode ─────────────────────────────────────────────
if (pressedLeftBottom && pressedRightBottom) {
Serial.println("Entering OTA Mode");
Configure_WIFI(); //enable WiFi and block here until new credentials are entered
}

prevLeftTop = curLeftTop;
prevLeftBottom = curLeftBottom;
prevRightTop = curRightTop;
prevRightBottom = curRightBottom;

//vTaskDelay(pdMS_TO_TICKS(20));
xSemaphoreTake(stateMutex, portMAX_DELAY);
bool triacfanstatee = TriacFanState;
uint8_t fanspeede = fanSpeed;
xSemaphoreGive(stateMutex);

// Run fan speed control in button task for more responsive control since it uses timing-based control
if(triacfanstatee){
RunFanSpeed(fanspeede); // from speed 1 to 4
}
else{
RunFanSpeed(0); // fully off
}
}
}

//Task: AlarmTasks
// ─────────────────────────────────────────────────────────────────────────────
void AlarmTasks(void *pvParameters) {
struct tm timeinfo; // local copy, not shared
bool alarmSoundActive = false; // local to this task — no race
while (true) {
bool timeOkk = false;
if(WiFi.status() == WL_CONNECTED) {
timeOkk = getLocalTime(&timeinfo, 0); // ← 0ms timeout, never blocks
}

// ── Snapshot alarm state ──────────────────────────────────────────
//Serial.println("AlarmTasks: Checking alarm...");
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
int aHour = alarmHour;
int aMin = alarmMinute;
bool aEnabled = alarmEnabled;
bool aFired = alarmFired;
bool aRunning = alarmRunning;
xSemaphoreGive(AlarmMutex);

//serial print for debugging alarm logic
Serial.printf("Alarm check: timeOk=%d enabled=%d fired=%d running=%d "
"now=%02d:%02d alarm=%02d:%02d sound=%d\n",
timeOkk, aEnabled, aFired, aRunning,
timeinfo.tm_hour, timeinfo.tm_min,
aHour, aMin, alarmSoundActive);

// ── Check alarm ───────────────────────────────────────────────────
if (aEnabled && timeOkk && !aFired && !aRunning) {
if (timeinfo.tm_hour == aHour && timeinfo.tm_min == aMin) {
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
alarmRunning = true;
xSemaphoreGive(AlarmMutex);
aRunning = true;
}
}
// Start looping sound when alarm begins
if (aRunning && !alarmSoundActive) {
music.playMusic(Melodies::ENTERTAINER, ForeverInBackground); // loop until stopped
alarmSoundActive = true;
}

// Stop sound when alarm is dismissed (alarmRunning goes false via button)
if (!aRunning && alarmSoundActive) {
music.stopPlayAudio(); // stop the looping melody
music.stopPlayTone(); // stop any tone that might be playing
alarmSoundActive = false;
}

// Reset alarmFired when minute advances past the alarm minute
// stop alarm after 3 minutes of play
if (timeinfo.tm_hour != aHour || timeinfo.tm_min == aMin + 3) {
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
alarmRunning = false;
alarmFired = true; // prevent re-fire this minute
xSemaphoreGive(AlarmMutex);
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}

//UI TASKS
// ─────────────────────────────────────────────────────────────────────────────
void UITasks(void *pvParameters) {
char timeStringBuff[20];
struct tm timeinfo;
Page lastPage = PAGE_MAIN;

while (true) {
// ── Snapshot appliance + page state ──────────────────────────────
xSemaphoreTake(stateMutex, portMAX_DELAY);
bool lightOn = TriacLightState;
bool fanOn = TriacFanState;
bool acOn = ACState;
Page page = currentPage;
xSemaphoreGive(stateMutex);
// ── Redraw background only on page change ─────────────────────────
if (backgroundNeedsRedraw) {
//k10.canvas->canvasClear(13); doesnt work
k10.canvas->clearLocalCanvas(0, 0, 240, 320); // full clear before redraw
k10.setScreenBackground(0x2C4C9C);
backgroundNeedsRedraw = false;
}

// ── Get time ONCE, outside WiFi check, with a hard timeout ────────
// getLocalTime()'s second argument is the timeout in ms.
// Default is 5000ms — set it to 0 to return immediately from cache.
// This prevents blocking UITasks when WiFi is down.
bool timeOk = getLocalTime(&timeinfo, 0); // 0ms timeout, never blocks

// ── Top bar ───────────────────────────────────────────────────────
if (WiFi.status() == WL_CONNECTED) {
if (timeOk) {
strftime(timeStringBuff, sizeof(timeStringBuff), "%I:%M %p", &timeinfo);
k10.canvas->canvasText(timeStringBuff, 10, 5, 0xFFFFFF,
k10.canvas->eCNAndENFont24, 8, true);
} else {
k10.canvas->canvasText("!Time!", 10, 5, 0xFF0000,
k10.canvas->eCNAndENFont24, 6, true);
}
k10.canvas->canvasDrawBitmap(190, 7, WIFIICON_WIDTH, WIFIICON_HEIGHT, WifiIcon);
} else {
// Still show cached time if available even without WiFi
if (timeOk) {
strftime(timeStringBuff, sizeof(timeStringBuff), "%I:%M %p", &timeinfo);
k10.canvas->canvasText(timeStringBuff, 10, 5, 0xAAAAAA, // greyed out = stale
k10.canvas->eCNAndENFont24, 8, true);
}
k10.canvas->canvasDrawBitmap(190, 7, NOWIFIICON_WIDTH, NOWIFIICON_HEIGHT, NoWifiIcon);
}
k10.canvas->canvasLine(10, 35, 230, 35, 0xFFFFFF);

// DRAW: MAIN PAGE — runs regardless of WiFi status
// ─────────────────────────────────────────────────────────────────────────────
if (page == PAGE_MAIN) {
k10.canvas->canvasText(" [LIGHT]", 3, 0xFFFFFF);
k10.canvas->canvasText(" [FAN] (" + String(fanSpeed) + ")", 5, 0xFFFFFF);
k10.canvas->canvasText(" [AC]", 7, 0xFFFFFF);

k10.canvas->canvasDrawBitmap(127, 53, RED_OFF_SWITCH_WIDTH, RED_OFF_SWITCH_HEIGHT,
lightOn ? greenbutton : Red_off_switch);
k10.canvas->canvasDrawBitmap(127, 102, RED_OFF_SWITCH_WIDTH, RED_OFF_SWITCH_HEIGHT,
fanOn ? greenbutton : Red_off_switch);
k10.canvas->canvasDrawBitmap(127, 147, RED_OFF_SWITCH_WIDTH, RED_OFF_SWITCH_HEIGHT,
acOn ? greenbutton : Red_off_switch);

k10.canvas->canvasLine(10, 191, 230, 191, 0xFFFFFF);

lightIntensity = k10.readALS(); // measure ambient light for backlight control and display on UI
//refresh backlight every 2 seconds
static unsigned long lastBacklightUpdate = 0;
if (millis() - lastBacklightUpdate >= 2000) {
lastBacklightUpdate = millis();
setBacklight(lightIntensity); // also update the backlight duty cycle based on the current ambient light level
}

sensors_event_t humidity_event, temp_event; // local variables to hold sensor readings
aht.getEvent(&humidity_event, &temp_event); // store current humidity and temperature readings
//draw intensity lines on main screen
const int BAR_Y = 200;
drawVerticalBar( 10, BAR_Y, (float)lightIntensity, 0, 400, "Lux", "", 10);
drawVerticalBar( 90, BAR_Y, humidity_event.relative_humidity, 0, 100, "Hum", "%", 10);
drawVerticalBar(170, BAR_Y, temp_event.temperature - 8, 0, 70, "Tmp", "C", 10); // counter the offset in temperature readings, probably due to heat from the device itself.

// draw a line and the alarm status at the bottom of the main page (even when WiFi is down, using cached alarm state)
k10.canvas->canvasLine(10, 257, 230, 257, 0xFFFFFF);

// Display alarm time and status at the bottom of main screen
char alarmHint[24];
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
snprintf(alarmHint, sizeof(alarmHint), " Alarm %02d:%02d %s",
alarmHour, alarmMinute, alarmEnabled ? "[ON]" : "[OFF]");
xSemaphoreGive(AlarmMutex);
k10.canvas->canvasText(alarmHint, 12, alarmEnabled ? 0x00FF88 : 0x888888);
k10.canvas->canvasText("alaminn.com", 10, 305, 0xFFFFFF,
k10.canvas->eCNAndENFont16, 11, true);
}

// DRAW: ALARM PAGE — runs regardless of WiFi status
// ─────────────────────────────────────────────────────────────────────────────
else {
k10.canvas->canvasText("-- ALARM --", 50, 40, 0xFFFFFF,
k10.canvas->eCNAndENFont24, 11, true);
k10.canvas->canvasDrawBitmap(65, 68, ALARMCLOCKk_WIDTH, ALARMCLOCKk_HEIGHT, alarmclockk);

char alarmTimeBuff[10];
xSemaphoreTake(AlarmMutex, portMAX_DELAY);
snprintf(alarmTimeBuff, sizeof(alarmTimeBuff), "%02d:%02d", alarmHour, alarmMinute);
bool aEnabled = alarmEnabled;
xSemaphoreGive(AlarmMutex);

k10.canvas->canvasText(alarmTimeBuff, 88, 240, aEnabled ? 0x00FF88 : 0xFF4444,
k10.canvas->eCNAndENFont24, 8, true);
k10.canvas->canvasText(aEnabled ? "ON" : "OFF", 88, 275,
aEnabled ? 0x00FF88 : 0xFF4444,
k10.canvas->eCNAndENFont24, 8, true);
k10.canvas->canvasLine(10, 227, 230, 227, 0xFFFFFF);
k10.canvas->canvasText("[A] Toggle", 10, 295, 0xFFFFFF, k10.canvas->eCNAndENFont16, 10, false);
k10.canvas->canvasText("Hr [^]", 185, 235, 0xFFFFFF, k10.canvas->eCNAndENFont16, 6, false);
k10.canvas->canvasText("Min [^]", 185, 295, 0xFFFFFF, k10.canvas->eCNAndENFont16, 7, false);
k10.canvas->canvasText("[<] Back", 10, 235, 0xFFFFFF, k10.canvas->eCNAndENFont16, 8, false);
}

k10.canvas->updateCanvas();
vTaskDelay(pdMS_TO_TICKS(100));
}
}


// ─────────────────────────────────────────────────────────────────────────────
// SETUP
// ─────────────────────────────────────────────────────────────────────────────
void setup() {
k10.begin();
Serial.begin(115200);
irsend.begin();

// ── Create ALL mutexes before any task is spawned ─────────────────────
stateMutex = xSemaphoreCreateMutex();
irMutex = xSemaphoreCreateMutex();
AlarmMutex = xSemaphoreCreateMutex();

// ── WiFi ──────────────────────────────────────────────────────────────
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("Connecting to WiFi");
for (int n = 20; n > 0 && WiFi.status() != WL_CONNECTED; n--) {
vTaskDelay(pdMS_TO_TICKS(500));
Serial.print('.');
}
configTime(6 * 3600, 0, "pool.ntp.org", "time.nist.gov");

// ── ASR ───────────────────────────────────────────────────────────────
asr.asrInit(1, EN_MODE, 3000);
while (asr._asrState == 0) {
Serial.print('.');
vTaskDelay(pdMS_TO_TICKS(100));
}
asr.addASRCommand(0+1, const_cast<char*>("Lights"));
asr.addASRCommand(1+1, const_cast<char*>("Fan"));
asr.addASRCommand(2+1, const_cast<char*>("AC"));
asr.addASRCommand(3+1, const_cast<char*>("Show Alarm"));
asr.addASRCommand(4+1, const_cast<char*>("Hide Alarm"));
asr.addASRCommand(5+1, const_cast<char*>("Fan Full Speed"));
asr.addASRCommand(6+1, const_cast<char*>("Fan Low Speed"));


// ── GPIO ──────────────────────────────────────────────────────────────
pinMode(LeftTopButton, INPUT_PULLDOWN);
pinMode(LeftBottomButton, INPUT_PULLDOWN);
pinMode(RightTopButton, INPUT_PULLDOWN);
pinMode(RightBottomButton,INPUT_PULLDOWN);
pinMode(TriacLight, OUTPUT);
pinMode(TriacFan, OUTPUT);
digital_write(TriacLight, LOW);
digital_write(TriacFan, LOW);

// ── Display ───────────────────────────────────────────────────────────
k10.initScreen(screen_dir);
k10.creatCanvas();
k10.canvas->canvasSetLineWidth(1);
k10.setScreenBackground(0x2C4C9C); // draw background once at startup
//setup backlight PWM (display backlight is wired to P1 via a transistor, so we can use PWM to control brightness if desired. For now we just turn it fully on)
ledcSetup(2, 5000, 8); // channel 0, 5 KHz, 8-bit resolution
ledcAttachPin(BackLED, 2); // attach pin to channel 0

// ── AHT sensor ───────────────────────────────────────────────────────
if (!aht.begin()) Serial.println("AHT10/AHT20 not found");

// ── Tasks ─────────────────────────────────────────────────────────────
Serial.print("Free heap before ButtonTasks: "); Serial.println(ESP.getFreeHeap());
xTaskCreatePinnedToCore(ButtonTasks, "ButtonTasks", 1024*8, NULL, 5, NULL, 0);
Serial.print("Free heap before VoiceTasks: "); Serial.println(ESP.getFreeHeap());
xTaskCreatePinnedToCore(VoiceTasks, "VoiceTasks", 1024*3, NULL, 5, NULL, 1);
Serial.print("Free heap before UITasks: "); Serial.println(ESP.getFreeHeap());
xTaskCreatePinnedToCore(UITasks, "UITasks", 1024*8, NULL, 5, NULL, 1);
Serial.print("Free heap before alarm tasks: "); Serial.println(ESP.getFreeHeap());
xTaskCreatePinnedToCore(AlarmTasks, "AlarmTasks", 1024*5, NULL, 5, NULL, 1);
}

// LOOP ( 8kb in size) core 1
// ─────────────────────────────────────────────────────────────────────────────
void loop() {
vTaskDelay(pdMS_TO_TICKS(10));
}

Troubleshooting

testing voice command.gif

I have been using the days regularly for last few days, and I must admit, it is very useful to me. The freedom to modify code that suits my needs really is a game-changer.

The button feels very real-time, thanks to the use of RTOS. I am constantly updating the code to fix bugs and add more features. I am currently working to add webpage connectivity. Keep an eye on my GitHub profile for the updates. Cheers!