/*
* ============================================================================
* SMARTPILL IoT - ESP32 FIRMWARE
* ============================================================================
*
* FLOW:
*
* Scheduled medication time
* ↓
* RED LED blinking + buzzer
* ↓
* User long-presses feedback button
* ↓
* Selector servo rotates to compartment
* ↓
* PUSH servo dispenses pill
* ↓
* GREEN LED confirms pill delivered
* ↓
* Server receives "taken"
*
* TIMEZONE:
* Asia/Kolkata (IST / UTC+05:30)
*
* API:
* https://smartpilliot.pythonanywhere.com
*
* ============================================================================
*/
#include <ArduinoJson.h>
#include <ESP32Servo.h>
#include <HTTPClient.h>
#include <LiquidCrystal_I2C.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <time.h>
// ============================================================================
// CONFIGURATION
// ============================================================================
#define USE_WOKWI 1
#if USE_WOKWI
const char *wifiSsid = "Wokwi-GUEST";
const char *wifiPassword = "";
#else
const char *wifiSsid = "YOUR_WIFI_SSID";
const char *wifiPassword = "YOUR_WIFI_PASSWORD";
#endif
const char *baseUrl = "https://smartpilliot.pythonanywhere.com";
const char *deviceId = "dev123";
const char *apiToken = "secret-esp32-token-123";
const char *firmwareVersion = "2.0.0";
// ============================================================================
// TIMEZONE
// ============================================================================
const long utcOffsetSeconds = 19800;
const int daylightOffsetSeconds = 0;
// ============================================================================
// API TIMERS
// ============================================================================
const unsigned long httpTimeoutMs = 20000;
const unsigned long wifiTimeoutMs = 30000;
const unsigned long wifiRetryDelayMs = 5000;
const unsigned long scheduleSyncIntervalMs = 60000;
const unsigned long heartbeatIntervalMs = 60000;
const unsigned long serverTimeSyncIntervalMs = 300000;
// ============================================================================
// ALARM CONFIGURATION
// ============================================================================
const unsigned long buttonLongPressMs = 1000;
const unsigned long redBlinkIntervalMs = 500;
const unsigned long alarmGracePeriodMs = 60000;
const int maxSchedules = 30;
// ============================================================================
// PIN CONFIGURATION
// ============================================================================
// Wokwi wiring:
// Rotator servo -> GPIO 19
// PUSH servo -> GPIO 18
// Button -> GPIO 5
// Red LED -> GPIO 12
// Green LED -> GPIO 13
// Blue LED -> GPIO 14
// Buzzer -> GPIO 4
// LCD SDA -> GPIO 21
// LCD SCL -> GPIO 22
const int selectorServoPin = 19;
const int openerServoPin = 18;
const int ledRedPin = 12;
const int ledGreenPin = 13;
const int ledBluePin = 14;
const int buzzerPin = 4;
const int buttonPin = 5;
// ============================================================================
// LCD
// ============================================================================
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ============================================================================
// SERVOS
// ============================================================================
Servo selectorServo;
Servo openerServo;
// ============================================================================
// WEEKDAYS
// ============================================================================
const char *weekdays[] = {
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
};
// ============================================================================
// MEDICATION SCHEDULE
// ============================================================================
struct MedicineAlarm {
int id;
String medicineName;
String dosage;
String dayOfWeek;
int hour;
int minute;
int compartment;
bool triggeredToday;
};
// ============================================================================
// GLOBAL STATE
// ============================================================================
MedicineAlarm alarms[maxSchedules];
int alarmCount = 0;
unsigned long lastScheduleSyncMillis = 0;
unsigned long lastHeartbeatMillis = 0;
unsigned long lastServerTimeSyncMillis = 0;
int lastResetYear = -1;
int lastResetYday = -1;
bool timeSynchronized = false;
bool serverTimeVerified = false;
// ============================================================================
// FUNCTION DECLARATIONS
// ============================================================================
void initializeHardware();
bool connectToWiFi();
bool ensureWiFiConnection();
bool synchronizeTime();
bool getCurrentTime(struct tm *timeInfo);
void printCurrentTime();
bool syncSchedule();
bool sendHeartbeat();
bool notifyTriggerStatus(
int compartment,
const String &status
);
bool confirmConsumption(
int compartment,
const String &status
);
int performHttpsGet(
const String &url,
String *response
);
bool parseScheduleResponse(
const String &payload
);
bool validateSchedule(
JsonObject item
);
bool alreadyTriggered(
int id,
const String &dayOfWeek,
int hour,
int minute
);
bool weekdayMatches(
const String &scheduledDay,
int currentWeekday
);
bool parseTimeString(
const String &timeString,
int *hour,
int *minute
);
void resetDailyTriggers(
const struct tm &timeInfo
);
void checkMedicationSchedules(
const struct tm &timeInfo
);
void triggerAlarm(
const MedicineAlarm &alarm
);
bool waitForMedicationConfirmation();
void setStatusLeds(
bool red,
bool green,
bool blue
);
void showLcdMessage(
const String &line1,
const String &line2
);
void printScheduleSummary();
String buildSyncUrl();
String buildHeartbeatUrl();
String buildTriggerUrl(
int compartment,
const String &status
);
String buildConfirmUrl(
int compartment,
const String &status
);
String urlEncode(
const String &value
);
// ============================================================================
// SETUP
// ============================================================================
void setup() {
Serial.begin(115200);
delay(500);
Serial.println();
Serial.println("================================================");
Serial.println(" SMARTPILL IoT ESP32");
Serial.println("================================================");
Serial.println(
"Firmware: " + String(firmwareVersion)
);
Serial.println(
"Device ID: " + String(deviceId)
);
Serial.println(
"Timezone: Asia/Kolkata (IST)"
);
Serial.println("================================================");
initializeHardware();
setStatusLeds(
false,
false,
true
);
showLcdMessage(
"SmartPill IoT",
"Connecting WiFi"
);
if (!connectToWiFi()) {
Serial.println(
"ERROR: Initial WiFi connection failed."
);
showLcdMessage(
"WiFi Failed",
"Retrying..."
);
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println();
Serial.println("WiFi Connected!");
Serial.print("ESP32 IP: ");
Serial.println(
WiFi.localIP()
);
showLcdMessage(
"WiFi Connected",
"Syncing Time"
);
if (synchronizeTime()) {
timeSynchronized = true;
printCurrentTime();
} else {
Serial.println(
"WARNING: NTP synchronization failed."
);
}
// Verify server IST time.
String serverTimeUrl =
String(baseUrl) + "/api/time";
String serverTimeResponse;
int serverTimeCode =
performHttpsGet(
serverTimeUrl,
&serverTimeResponse
);
if (serverTimeCode == 200) {
Serial.println(
"Server IST time API verified."
);
serverTimeVerified = true;
lastServerTimeSyncMillis =
millis();
} else {
Serial.println(
"WARNING: Server time API verification failed."
);
}
}
setStatusLeds(
false,
false,
false
);
showLcdMessage(
"SmartPill IoT",
"Ready"
);
Serial.println();
Serial.println(
"System initialization complete."
);
Serial.println();
}
// ============================================================================
// MAIN LOOP
// ============================================================================
void loop() {
// Maintain WiFi.
if (!ensureWiFiConnection()) {
setStatusLeds(
false,
false,
true
);
delay(1000);
return;
}
// Synchronize ESP32 IST clock.
if (
!timeSynchronized ||
millis() - lastServerTimeSyncMillis >=
serverTimeSyncIntervalMs
) {
if (synchronizeTime()) {
timeSynchronized = true;
lastServerTimeSyncMillis =
millis();
Serial.println(
"IST clock synchronized."
);
printCurrentTime();
}
}
// Synchronize medication schedules.
if (
lastScheduleSyncMillis == 0 ||
millis() - lastScheduleSyncMillis >=
scheduleSyncIntervalMs
) {
setStatusLeds(
false,
false,
true
);
Serial.println();
Serial.println(
"========== SCHEDULE SYNC =========="
);
bool syncSuccessful =
syncSchedule();
if (syncSuccessful) {
Serial.println(
"Schedule synchronization successful."
);
} else {
Serial.println(
"Schedule synchronization failed."
);
Serial.println(
"Existing schedule will be preserved."
);
}
Serial.println(
"=================================="
);
setStatusLeds(
false,
false,
false
);
lastScheduleSyncMillis =
millis();
}
// Heartbeat.
if (
lastHeartbeatMillis == 0 ||
millis() - lastHeartbeatMillis >=
heartbeatIntervalMs
) {
sendHeartbeat();
lastHeartbeatMillis =
millis();
}
// Read current IST time.
struct tm currentTime;
if (!getCurrentTime(¤tTime)) {
Serial.println(
"WARNING: Current IST time unavailable."
);
delay(1000);
return;
}
// Reset daily flags.
resetDailyTriggers(
currentTime
);
// Check medication schedules.
checkMedicationSchedules(
currentTime
);
delay(500);
}
// ============================================================================
// HARDWARE INITIALIZATION
// ============================================================================
void initializeHardware() {
// Initializes LEDs, buzzer, button, servos and LCD.
pinMode(
ledRedPin,
OUTPUT
);
pinMode(
ledGreenPin,
OUTPUT
);
pinMode(
ledBluePin,
OUTPUT
);
pinMode(
buzzerPin,
OUTPUT
);
pinMode(
buttonPin,
INPUT_PULLUP
);
digitalWrite(
ledRedPin,
LOW
);
digitalWrite(
ledGreenPin,
LOW
);
digitalWrite(
ledBluePin,
LOW
);
noTone(
buzzerPin
);
selectorServo.setPeriodHertz(50);
openerServo.setPeriodHertz(50);
selectorServo.attach(
selectorServoPin,
500,
2400
);
openerServo.attach(
openerServoPin,
500,
2400
);
selectorServo.write(0);
openerServo.write(0);
lcd.init();
lcd.backlight();
lcd.clear();
}
// ============================================================================
// WIFI CONNECTION
// ============================================================================
bool connectToWiFi() {
// Connects ESP32 to WiFi.
Serial.println();
Serial.println(
"Connecting to WiFi..."
);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(
true
);
WiFi.persistent(
false
);
WiFi.begin(
wifiSsid,
wifiPassword
);
unsigned long startMillis =
millis();
while (
WiFi.status() != WL_CONNECTED
) {
delay(500);
Serial.print(".");
if (
millis() - startMillis >=
wifiTimeoutMs
) {
Serial.println();
Serial.println(
"WiFi connection timeout."
);
WiFi.disconnect(true);
delay(1000);
return false;
}
}
Serial.println();
Serial.println(
"WiFi connected."
);
Serial.print(
"IP address: "
);
Serial.println(
WiFi.localIP()
);
Serial.print(
"Signal strength: "
);
Serial.print(
WiFi.RSSI()
);
Serial.println(
" dBm"
);
return true;
}
// ============================================================================
// WIFI RECOVERY
// ============================================================================
bool ensureWiFiConnection() {
// Reconnects ESP32 if WiFi is lost.
if (
WiFi.status() ==
WL_CONNECTED
) {
return true;
}
Serial.println();
Serial.println(
"WiFi disconnected."
);
Serial.println(
"Attempting WiFi recovery..."
);
setStatusLeds(
false,
false,
true
);
unsigned long startMillis =
millis();
WiFi.reconnect();
while (
WiFi.status() != WL_CONNECTED &&
millis() - startMillis <
wifiRetryDelayMs
) {
delay(250);
Serial.print(".");
}
Serial.println();
if (
WiFi.status() ==
WL_CONNECTED
) {
Serial.println(
"WiFi connection recovered."
);
Serial.print(
"IP address: "
);
Serial.println(
WiFi.localIP()
);
return true;
}
Serial.println(
"WiFi recovery failed."
);
return false;
}
// ============================================================================
// IST TIME SYNCHRONIZATION
// ============================================================================
bool synchronizeTime() {
// Synchronizes ESP32 clock with NTP using IST.
Serial.println();
Serial.println(
"Synchronizing IST clock..."
);
configTime(
utcOffsetSeconds,
daylightOffsetSeconds,
"pool.ntp.org",
"time.nist.gov",
"time.google.com"
);
struct tm timeInfo;
for (
int attempt = 1;
attempt <= 20;
attempt++
) {
if (
getLocalTime(
&timeInfo,
1000
)
) {
Serial.println(
"NTP synchronization successful."
);
Serial.printf(
"IST: %04d-%02d-%02d %02d:%02d:%02d\n",
timeInfo.tm_year + 1900,
timeInfo.tm_mon + 1,
timeInfo.tm_mday,
timeInfo.tm_hour,
timeInfo.tm_min,
timeInfo.tm_sec
);
return true;
}
Serial.print(
"NTP attempt "
);
Serial.print(
attempt
);
Serial.println(
"/20 failed."
);
delay(500);
}
Serial.println(
"NTP synchronization failed."
);
return false;
}
// ============================================================================
// CURRENT TIME
// ============================================================================
bool getCurrentTime(
struct tm *timeInfo
) {
// Reads current IST time.
if (
timeInfo == nullptr
) {
return false;
}
if (
!getLocalTime(
timeInfo,
1000
)
) {
return false;
}
return true;
}
// ============================================================================
// PRINT CURRENT TIME
// ============================================================================
void printCurrentTime() {
// Prints current IST time.
struct tm timeInfo;
if (
!getCurrentTime(
&timeInfo
)
) {
Serial.println(
"Current time unavailable."
);
return;
}
Serial.printf(
"Current IST: %04d-%02d-%02d %s %02d:%02d:%02d\n",
timeInfo.tm_year + 1900,
timeInfo.tm_mon + 1,
timeInfo.tm_mday,
weekdays[timeInfo.tm_wday],
timeInfo.tm_hour,
timeInfo.tm_min,
timeInfo.tm_sec
);
}
// ============================================================================
// SCHEDULE SYNC
// ============================================================================
bool syncSchedule() {
// Downloads and validates medication schedules.
if (
WiFi.status() != WL_CONNECTED
) {
Serial.println(
"Schedule sync skipped: WiFi unavailable."
);
return false;
}
String url =
buildSyncUrl();
String payload;
Serial.println(
"Schedule API:"
);
Serial.println(
url
);
int responseCode =
performHttpsGet(
url,
&payload
);
if (
responseCode != 200
) {
Serial.print(
"Schedule API failed. HTTP code: "
);
Serial.println(
responseCode
);
return false;
}
if (
payload.length() == 0
) {
Serial.println(
"Schedule API returned empty response."
);
return false;
}
DynamicJsonDocument document(
12288
);
DeserializationError error =
deserializeJson(
document,
payload
);
if (error) {
Serial.print(
"Schedule JSON parse failed: "
);
Serial.println(
error.c_str()
);
return false;
}
String status =
document["status"] | "";
if (
!status.equalsIgnoreCase(
"success"
)
) {
Serial.print(
"Schedule API status is not success: "
);
Serial.println(
status
);
return false;
}
String responseDeviceId =
document["device_id"] | "";
if (
responseDeviceId.length() > 0 &&
responseDeviceId != deviceId
) {
Serial.println(
"ERROR: API returned wrong device ID."
);
return false;
}
JsonArray schedules =
document["schedules"]
.as<JsonArray>();
if (
schedules.isNull()
) {
Serial.println(
"ERROR: schedules array missing."
);
return false;
}
MedicineAlarm newAlarms[
maxSchedules
];
int newAlarmCount = 0;
for (
JsonObject item : schedules
) {
if (
newAlarmCount >= maxSchedules
) {
Serial.println(
"WARNING: Maximum schedule limit reached."
);
break;
}
if (
!validateSchedule(item)
) {
Serial.println(
"WARNING: Invalid schedule skipped."
);
continue;
}
MedicineAlarm alarm;
alarm.id =
item["id"] | 0;
alarm.medicineName =
item["medicine_name"] | "";
alarm.dosage =
item["dosage"] | "";
alarm.dayOfWeek =
item["day_of_week"] | "";
alarm.compartment =
item["compartment"] | 1;
String timeString =
item["time"] | "";
if (
!parseTimeString(
timeString,
&alarm.hour,
&alarm.minute
)
) {
continue;
}
alarm.triggeredToday =
alreadyTriggered(
alarm.id,
alarm.dayOfWeek,
alarm.hour,
alarm.minute
);
newAlarms[
newAlarmCount
] = alarm;
newAlarmCount++;
}
for (
int i = 0;
i < newAlarmCount;
i++
) {
alarms[i] =
newAlarms[i];
}
alarmCount =
newAlarmCount;
Serial.println();
Serial.println(
"Schedule synchronized successfully."
);
Serial.print(
"Total schedules: "
);
Serial.println(
alarmCount
);
printScheduleSummary();
showLcdMessage(
"Schedule Synced",
"Meds: " + String(alarmCount)
);
return true;
}
// ============================================================================
// SCHEDULE VALIDATION
// ============================================================================
bool validateSchedule(
JsonObject item
) {
// Validates a medication schedule.
int id =
item["id"] | 0;
String medicineName =
item["medicine_name"] | "";
String dosage =
item["dosage"] | "";
String dayOfWeek =
item["day_of_week"] | "";
String timeString =
item["time"] | "";
int compartment =
item["compartment"] | 0;
if (id <= 0) {
return false;
}
if (medicineName.length() == 0) {
return false;
}
if (dosage.length() == 0) {
return false;
}
if (dayOfWeek.length() == 0) {
return false;
}
if (
compartment < 1 ||
compartment > 7
) {
return false;
}
int hour;
int minute;
if (
!parseTimeString(
timeString,
&hour,
&minute
)
) {
return false;
}
return true;
}
// ============================================================================
// TIME STRING PARSER
// ============================================================================
bool parseTimeString(
const String &timeString,
int *hour,
int *minute
) {
// Parses HH:MM safely.
if (
hour == nullptr ||
minute == nullptr
) {
return false;
}
if (
timeString.length() != 5
) {
return false;
}
if (
timeString.charAt(2) != ':'
) {
return false;
}
for (
int i = 0;
i < 5;
i++
) {
if (i == 2) {
continue;
}
if (
!isDigit(
timeString.charAt(i)
)
) {
return false;
}
}
int parsedHour =
timeString
.substring(0, 2)
.toInt();
int parsedMinute =
timeString
.substring(3, 5)
.toInt();
if (
parsedHour < 0 ||
parsedHour > 23
) {
return false;
}
if (
parsedMinute < 0 ||
parsedMinute > 59
) {
return false;
}
*hour =
parsedHour;
*minute =
parsedMinute;
return true;
}
// ============================================================================
// DAILY RESET
// ============================================================================
void resetDailyTriggers(
const struct tm &timeInfo
) {
// Resets daily alarm states.
int currentYear =
timeInfo.tm_year;
int currentYday =
timeInfo.tm_yday;
if (
lastResetYear == -1 ||
lastResetYday == -1
) {
lastResetYear =
currentYear;
lastResetYday =
currentYday;
return;
}
if (
currentYear != lastResetYear ||
currentYday != lastResetYday
) {
Serial.println();
Serial.println(
"New IST calendar day detected."
);
for (
int i = 0;
i < alarmCount;
i++
) {
alarms[i].triggeredToday =
false;
}
lastResetYear =
currentYear;
lastResetYday =
currentYday;
}
}
// ============================================================================
// SCHEDULE CHECK
// ============================================================================
void checkMedicationSchedules(
const struct tm &timeInfo
) {
// Starts the red warning when a medication becomes due.
unsigned long currentMinutes =
(unsigned long)timeInfo.tm_hour * 60UL +
timeInfo.tm_min;
for (
int i = 0;
i < alarmCount;
i++
) {
MedicineAlarm &alarm =
alarms[i];
if (
alarm.triggeredToday
) {
continue;
}
if (
!weekdayMatches(
alarm.dayOfWeek,
timeInfo.tm_wday
)
) {
continue;
}
unsigned long scheduledMinutes =
(unsigned long)alarm.hour * 60UL +
alarm.minute;
/*
* Trigger during the scheduled minute.
*
* Also allow the next minute so a temporary
* network/API operation cannot cause the
* medication event to be completely missed.
*/
if (
currentMinutes < scheduledMinutes ||
currentMinutes > scheduledMinutes + 1
) {
continue;
}
Serial.println();
Serial.println(
"=========================================="
);
Serial.println(
"MEDICATION SCHEDULE MATCHED"
);
Serial.print(
"Medicine: "
);
Serial.println(
alarm.medicineName
);
Serial.print(
"Dosage: "
);
Serial.println(
alarm.dosage
);
Serial.print(
"Compartment: "
);
Serial.println(
alarm.compartment
);
Serial.printf(
"Scheduled: %02d:%02d IST\n",
alarm.hour,
alarm.minute
);
Serial.printf(
"Current: %02d:%02d:%02d IST\n",
timeInfo.tm_hour,
timeInfo.tm_min,
timeInfo.tm_sec
);
Serial.println(
"=========================================="
);
/*
* Mark immediately so the same schedule cannot
* start another alarm cycle.
*/
alarm.triggeredToday =
true;
/*
* Important:
* triggerAlarm() now waits for the button FIRST.
* The servos do not move at scheduled time.
*/
triggerAlarm(
alarm
);
}
}
// ============================================================================
// WEEKDAY MATCH
// ============================================================================
bool weekdayMatches(
const String &scheduledDay,
int currentWeekday
) {
// Matches API weekday against ESP32 IST weekday.
if (
currentWeekday < 0 ||
currentWeekday > 6
) {
return false;
}
if (
scheduledDay.length() == 0
) {
return false;
}
return scheduledDay.equalsIgnoreCase(
weekdays[currentWeekday]
);
}
// ============================================================================
// ALREADY TRIGGERED
// ============================================================================
bool alreadyTriggered(
int id,
const String &dayOfWeek,
int hour,
int minute
) {
// Preserves today's trigger state during schedule sync.
for (
int i = 0;
i < alarmCount;
i++
) {
if (
alarms[i].id == id &&
alarms[i].dayOfWeek == dayOfWeek &&
alarms[i].hour == hour &&
alarms[i].minute == minute
) {
return alarms[i].triggeredToday;
}
}
return false;
}
// ============================================================================
// ALARM
// ============================================================================
void triggerAlarm(
const MedicineAlarm &alarm
) {
// Waits for long press, then rotates and dispenses medication.
Serial.println();
Serial.println(
"******** MEDICATION ALARM ********"
);
Serial.print(
"Medicine: "
);
Serial.println(
alarm.medicineName
);
Serial.print(
"Dosage: "
);
Serial.println(
alarm.dosage
);
Serial.print(
"Compartment: "
);
Serial.println(
alarm.compartment
);
/*
* ============================================================
* PHASE 1
*
* Scheduled time reached.
* Do NOT move servos.
* Start RED LED + buzzer.
* ============================================================
*/
showLcdMessage(
"MEDICINE DUE",
"Hold Button"
);
Serial.println(
"Medication time reached."
);
Serial.println(
"RED LED + buzzer active."
);
Serial.println(
"Waiting for long button press..."
);
bool confirmed =
waitForMedicationConfirmation();
/*
* ============================================================
* PHASE 2
*
* Long press detected.
* Stop alarm.
* ============================================================
*/
digitalWrite(
ledRedPin,
LOW
);
noTone(
buzzerPin
);
/*
* ============================================================
* PHASE 3
*
* Rotate selector to correct compartment.
* ============================================================
*/
int compartment =
alarm.compartment;
if (
compartment < 1
) {
compartment = 1;
}
if (
compartment > 7
) {
compartment = 7;
}
int selectorAngle =
(compartment - 1) * 25;
if (
selectorAngle > 180
) {
selectorAngle = 180;
}
Serial.println();
Serial.println(
"Long press confirmed."
);
Serial.print(
"Selecting compartment: "
);
Serial.println(
compartment
);
Serial.print(
"Selector angle: "
);
Serial.println(
selectorAngle
);
showLcdMessage(
"Dispensing...",
"Please Wait"
);
selectorServo.write(
selectorAngle
);
delay(1000);
/*
* ============================================================
* PHASE 4
*
* Open PUSH servo.
* ============================================================
*/
Serial.println(
"Opening medication lid..."
);
openerServo.write(90);
delay(800);
openerServo.write(0);
delay(500);
/*
* Report actual compartment opening.
*/
bool triggerReported =
notifyTriggerStatus(
compartment,
"opened"
);
if (!triggerReported) {
Serial.println(
"WARNING: Trigger status was not confirmed by server."
);
}
/*
* ============================================================
* PHASE 5
*
* Pill delivered.
* Green LED confirms delivery.
* ============================================================
*/
Serial.println();
Serial.println(
"Medication delivered successfully."
);
showLcdMessage(
"Pill Delivered",
"Thank You!"
);
digitalWrite(
ledGreenPin,
HIGH
);
/*
* Save TAKEN status after actual dispensing.
*/
bool confirmationReported =
confirmConsumption(
compartment,
"taken"
);
if (!confirmationReported) {
Serial.println(
"WARNING: Taken status was not confirmed by server."
);
}
delay(3000);
/*
* Turn green LED off.
*/
digitalWrite(
ledGreenPin,
LOW
);
/*
* Return selector to home position.
*/
selectorServo.write(0);
delay(1000);
showLcdMessage(
"SmartPill IoT",
"Ready"
);
Serial.println(
"Medication cycle completed."
);
}
// ============================================================================
// WAIT FOR LONG BUTTON PRESS
// ============================================================================
bool waitForMedicationConfirmation() {
// Blinks red and waits for a one-second button long press.
unsigned long buttonPressStart =
0;
unsigned long lastBlinkMillis =
millis();
bool buttonWasPressed =
false;
bool redState =
false;
while (true) {
unsigned long currentMillis =
millis();
/*
* ------------------------------------------------------------
* RED LED BLINK
* ------------------------------------------------------------
*/
if (
currentMillis -
lastBlinkMillis >=
redBlinkIntervalMs
) {
lastBlinkMillis =
currentMillis;
redState =
!redState;
digitalWrite(
ledRedPin,
redState
? HIGH
: LOW
);
if (redState) {
tone(
buzzerPin,
1000
);
} else {
noTone(
buzzerPin
);
}
}
/*
* ------------------------------------------------------------
* BUTTON STATE
* ------------------------------------------------------------
*
* Button is active LOW because INPUT_PULLUP
* is used and the other side is connected to GND.
*/
bool buttonPressed =
digitalRead(buttonPin) == LOW;
if (buttonPressed) {
/*
* New button press.
*/
if (!buttonWasPressed) {
buttonWasPressed =
true;
buttonPressStart =
currentMillis;
Serial.println(
"Feedback button pressed."
);
}
/*
* Button is being held.
*/
if (
currentMillis -
buttonPressStart >=
buttonLongPressMs
) {
Serial.println(
"Long press confirmed."
);
/*
* Stop red alarm.
*/
digitalWrite(
ledRedPin,
LOW
);
noTone(
buzzerPin
);
/*
* Wait for physical release.
* This prevents the same press from being
* detected again.
*/
while (
digitalRead(buttonPin) == LOW
) {
delay(10);
}
delay(100);
return true;
}
} else {
/*
* Button released before one second.
*/
if (buttonWasPressed) {
Serial.println(
"Button released before long press."
);
buttonWasPressed =
false;
buttonPressStart =
0;
}
}
delay(10);
}
}
// ============================================================================
// HEARTBEAT
// ============================================================================
bool sendHeartbeat() {
// Sends device online status to the API.
if (
WiFi.status() != WL_CONNECTED
) {
Serial.println(
"Heartbeat skipped: WiFi unavailable."
);
return false;
}
String url =
String(baseUrl) +
"/api/esp32/heartbeat?device_id=" +
urlEncode(deviceId) +
"&status=online&firmware_version=" +
urlEncode(firmwareVersion) +
"&token=" +
urlEncode(apiToken);
String response;
int responseCode =
performHttpsGet(
url,
&response
);
if (
responseCode == 200
) {
Serial.println(
"Heartbeat: OK"
);
if (
response.length() > 0
) {
Serial.print(
"Heartbeat response: "
);
Serial.println(
response
);
}
return true;
}
Serial.print(
"Heartbeat failed. HTTP code: "
);
Serial.println(
responseCode
);
return false;
}
// ============================================================================
// TRIGGER STATUS
// ============================================================================
bool notifyTriggerStatus(
int compartment,
const String &status
) {
// Reports medication compartment opening.
if (
WiFi.status() != WL_CONNECTED
) {
Serial.println(
"Trigger notification skipped: WiFi unavailable."
);
return false;
}
String url =
buildTriggerUrl(
compartment,
status
);
String response;
int responseCode =
performHttpsGet(
url,
&response
);
if (
responseCode != 200
) {
Serial.print(
"Trigger notification failed. HTTP code: "
);
Serial.println(
responseCode
);
return false;
}
if (
response.length() > 0
) {
DynamicJsonDocument document(
1024
);
DeserializationError error =
deserializeJson(
document,
response
);
if (!error) {
String responseStatus =
document["status"] | "";
if (
responseStatus.length() > 0 &&
!responseStatus.equalsIgnoreCase(
"success"
)
) {
Serial.println(
"Trigger API returned non-success status."
);
return false;
}
}
}
Serial.println(
"Trigger status reported successfully."
);
return true;
}
// ============================================================================
// CONSUMPTION CONFIRMATION
// ============================================================================
bool confirmConsumption(
int compartment,
const String &status
) {
// Sends medication adherence status.
if (
WiFi.status() != WL_CONNECTED
) {
Serial.println(
"Confirmation skipped: WiFi unavailable."
);
return false;
}
String url =
buildConfirmUrl(
compartment,
status
);
String response;
int responseCode =
performHttpsGet(
url,
&response
);
if (
responseCode != 200
) {
Serial.print(
"Confirmation failed. HTTP code: "
);
Serial.println(
responseCode
);
return false;
}
Serial.print(
"Confirmation response: "
);
Serial.println(
response
);
DynamicJsonDocument document(
1024
);
DeserializationError error =
deserializeJson(
document,
response
);
if (error) {
Serial.print(
"Confirmation JSON parse failed: "
);
Serial.println(
error.c_str()
);
return false;
}
String responseStatus =
document["status"] | "";
if (
!responseStatus.equalsIgnoreCase(
"success"
)
) {
Serial.println(
"Confirmation API did not return success."
);
return false;
}
Serial.println(
"Medication adherence saved successfully."
);
return true;
}
// ============================================================================
// HTTPS GET
// ============================================================================
int performHttpsGet(
const String &url,
String *response
) {
// Performs HTTPS GET with diagnostics.
if (
WiFi.status() != WL_CONNECTED
) {
Serial.println(
"HTTPS ERROR: WiFi disconnected."
);
return -1;
}
Serial.println();
Serial.println(
"========== HTTPS GET =========="
);
Serial.println(
"URL:"
);
Serial.println(
url
);
WiFiClientSecure client;
/*
* PythonAnywhere HTTPS.
*
* Certificate validation is disabled for this
* development/Wokwi implementation.
*/
client.setInsecure();
HTTPClient http;
http.setConnectTimeout(
15000
);
http.setTimeout(
httpTimeoutMs
);
if (
!http.begin(
client,
url
)
) {
Serial.println(
"HTTPS ERROR: http.begin() failed."
);
Serial.println(
"Check DNS, URL and TLS connectivity."
);
Serial.println(
"==============================="
);
return -1;
}
int responseCode =
http.GET();
Serial.print(
"HTTP response code: "
);
Serial.println(
responseCode
);
if (
responseCode > 0
) {
if (
response != nullptr
) {
*response =
http.getString();
Serial.println(
"Response:"
);
Serial.println(
*response
);
}
} else {
Serial.print(
"HTTPS GET error: "
);
Serial.println(
http.errorToString(
responseCode
)
);
}
http.end();
Serial.println(
"==============================="
);
return responseCode;
}
// ============================================================================
// STATUS LEDS
// ============================================================================
void setStatusLeds(
bool red,
bool green,
bool blue
) {
// Controls status LEDs.
digitalWrite(
ledRedPin,
red
? HIGH
: LOW
);
digitalWrite(
ledGreenPin,
green
? HIGH
: LOW
);
digitalWrite(
ledBluePin,
blue
? HIGH
: LOW
);
}
// ============================================================================
// LCD
// ============================================================================
void showLcdMessage(
const String &line1,
const String &line2
) {
// Displays two padded LCD lines.
String firstLine =
line1.substring(
0,
min(
16,
(int)line1.length()
)
);
String secondLine =
line2.substring(
0,
min(
16,
(int)line2.length()
)
);
while (
firstLine.length() < 16
) {
firstLine += ' ';
}
while (
secondLine.length() < 16
) {
secondLine += ' ';
}
lcd.clear();
lcd.setCursor(
0,
0
);
lcd.print(
firstLine
);
lcd.setCursor(
0,
1
);
lcd.print(
secondLine
);
}
// ============================================================================
// SCHEDULE SUMMARY
// ============================================================================
void printScheduleSummary() {
// Prints synchronized schedules.
Serial.println();
Serial.println(
"========== SCHEDULES =========="
);
for (
int i = 0;
i < alarmCount;
i++
) {
Serial.printf(
"#%d | ID:%d | %s | %s | %02d:%02d | C%d | Triggered:%s\n",
i + 1,
alarms[i].id,
alarms[i].dayOfWeek.c_str(),
alarms[i].medicineName.c_str(),
alarms[i].hour,
alarms[i].minute,
alarms[i].compartment,
alarms[i].triggeredToday
? "YES"
: "NO"
);
}
Serial.println(
"==============================="
);
}
// ============================================================================
// API URL BUILDERS
// ============================================================================
String buildSyncUrl() {
// Builds schedule synchronization URL.
return String(baseUrl) +
"/api/esp32/sync?device_id=" +
urlEncode(deviceId) +
"&token=" +
urlEncode(apiToken);
}
String buildHeartbeatUrl() {
// Builds heartbeat URL.
return String(baseUrl) +
"/api/esp32/heartbeat?device_id=" +
urlEncode(deviceId) +
"&status=online&firmware_version=" +
urlEncode(firmwareVersion) +
"&token=" +
urlEncode(apiToken);
}
String buildTriggerUrl(
int compartment,
const String &status
) {
// Builds compartment trigger URL.
return String(baseUrl) +
"/api/esp32/trigger?device_id=" +
urlEncode(deviceId) +
"&compartment=" +
String(compartment) +
"&status=" +
urlEncode(status) +
"&token=" +
urlEncode(apiToken);
}
String buildConfirmUrl(
int compartment,
const String &status
) {
// Builds medication confirmation URL.
return String(baseUrl) +
"/api/esp32/confirm?device_id=" +
urlEncode(deviceId) +
"&compartment=" +
String(compartment) +
"&status=" +
urlEncode(status) +
"&token=" +
urlEncode(apiToken);
}
// ============================================================================
// URL ENCODING
// ============================================================================
String urlEncode(
const String &value
) {
// Encodes URL query parameters.
String encoded = "";
const char *hex =
"0123456789ABCDEF";
for (
unsigned int i = 0;
i < value.length();
i++
) {
char character =
value.charAt(i);
if (
(character >= 'a' &&
character <= 'z') ||
(character >= 'A' &&
character <= 'Z') ||
(character >= '0' &&
character <= '9') ||
character == '-' ||
character == '_' ||
character == '.' ||
character == '~'
) {
encoded +=
character;
} else {
encoded += '%';
encoded +=
hex[
(character >> 4) &
0x0F
];
encoded +=
hex[
character &
0x0F
];
}
}
return encoded;
}Rotator
PUSH
Feedback Pill Taken
Buzz
Time arrived - Blinks till user push feedback button
Confirmation of Pill Taken with Feedback