#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <time.h>
// Display configuration
// Display configuration
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3c
//Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Button pins - Updated for XIAO ESP32-C3
#define START_STOP_BUTTON 8 // GPIO9 (D8)
#define PROJECT_BUTTON 9 // GPIO10 (D9)
#define DISPLAY_TOGGLE_BUTTON 20 // GPIO2 (D7)
// RGB LED pins - Updated for XIAO ESP32-C3 PWM capable pins
#define LED_RED_PIN 3 // GPIO3 (D1) - PWM capable
#define LED_GREEN_PIN 4 // GPIO4 (D2) - PWM capable
#define LED_BLUE_PIN 5 // GPIO5 (D3) - PWM capable
#define BATTERY_PIN A0
// WiFi credentials
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// Google Sheets configuration
const char* googleScriptURL = "";
// Time tracking variables
bool isTracking = false;
unsigned long startTime = 0;
unsigned long elapsedTime = 0;
unsigned long lastDisplayUpdate = 0;
String currentProject = "NASA";
int currentProjectIndex = 0;
String sessionStartTimestamp = "";
// Power management and display variables
bool displayOn = true;
bool sleepMode = false;
unsigned long lastButtonPress = 0;
unsigned long lastActivity = 0;
const unsigned long SLEEP_TIMEOUT = 300000; // 5 minutes
const unsigned long DISPLAY_UPDATE_INTERVAL = 1000; // 1 second
// Project names and colors
String projects[] = {"NASA", "Whisper Aero"};
int projectColors[][3] = {
{0, 0, 126}, // NASA - Blue
{126, 0, 126} // Whisper Aero - Magenta
};
int numProjects = 2;
// Button debouncing - Enhanced for ESP32-C3
const unsigned long debounceDelay = 250;
bool lastButtonStates[3] = {HIGH, HIGH, HIGH}; // Track previous button states
// Battery monitoring variables
float batteryVoltage = 0.0;
int batteryPercentage = 0;
unsigned long lastBatteryCheck = 0;
const unsigned long BATTERY_CHECK_INTERVAL = 30000; // Check every 30 seconds
// Time sync
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -18000; // EST (-5 hours)
const int daylightOffset_sec = 3600;
void setup() {
Serial.begin(115200);
delay(250); // Give serial time to initialize
Serial.println("Starting XIAO ESP32-C3 Time Tracker...");
display.begin(SCREEN_ADDRESS,true);
display.display();
// Initialize I2C for display with specific pins for XIAO ESP32-C3
//Wire.begin(); // Use default I2C pins (SDA=GPIO6/D4, SCL=GPIO7/D5)
// Show startup screen
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(0, 0);
display.print("Time Tracker");
display.setCursor(0, 16);
display.print("Starting...");
display.display();
delay(2000);
// Initialize buttons with explicit pull-up
pinMode(START_STOP_BUTTON, INPUT_PULLUP);
pinMode(PROJECT_BUTTON, INPUT_PULLUP);
pinMode(DISPLAY_TOGGLE_BUTTON, INPUT_PULLUP);
// Small delay to let pins settle
delay(100);
// Read initial button states
lastButtonStates[0] = digitalRead(START_STOP_BUTTON);
lastButtonStates[1] = digitalRead(PROJECT_BUTTON);
lastButtonStates[2] = digitalRead(DISPLAY_TOGGLE_BUTTON);
Serial.println("Button pins initialized:");
Serial.println("START_STOP (GPIO9/D9): " + String(START_STOP_BUTTON) + " = " + String(lastButtonStates[0]));
Serial.println("PROJECT (GPIO10/D10): " + String(PROJECT_BUTTON) + " = " + String(lastButtonStates[1]));
Serial.println("DISPLAY (GPIO2/D0): " + String(DISPLAY_TOGGLE_BUTTON) + " = " + String(lastButtonStates[2]));
// Initialize RGB LED pins
pinMode(LED_RED_PIN, OUTPUT);
pinMode(LED_GREEN_PIN, OUTPUT);
pinMode(LED_BLUE_PIN, OUTPUT);
// Initialize battery monitoring
pinMode(BATTERY_PIN, INPUT);
// Set initial LED color (red for not tracking)
setLEDColor(126, 0, 0);
// Initial battery reading
updateBatteryStatus();
// Initialize activity tracking
lastActivity = millis();
lastButtonPress = millis();
// Connect to WiFi
WiFi.begin(ssid, password);
// Update display during WiFi connection
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(0, 0);
display.print("Connecting WiFi...");
display.display();
int wifiAttempts = 0;
while (WiFi.status() != WL_CONNECTED && wifiAttempts < 20) {
delay(500);
Serial.print(".");
wifiAttempts++;
// Update display with progress
display.setCursor(0, 16);
display.print("Attempt: ");
display.print(wifiAttempts);
display.display();
}
if (WiFi.status() == WL_CONNECTED) {
// Serial.println("\nWiFi connected!");
Serial.println("IP address: " + WiFi.localIP().toString());
// Initialize time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
} else {
Serial.println("\nWiFi connection failed!");
}
// Initial display update
updateDisplay();
Serial.println("Setup complete!");
}
void loop() {
// Handle button presses
handleButtons();
// Check for sleep mode
checkSleepMode();
// Update battery status periodically
if (millis() - lastBatteryCheck > BATTERY_CHECK_INTERVAL) {
updateBatteryStatus();
lastBatteryCheck = millis();
}
// Update display every second (if display is on and not in sleep mode)
if (displayOn && !sleepMode && millis() - lastDisplayUpdate > DISPLAY_UPDATE_INTERVAL) {
updateDisplay();
lastDisplayUpdate = millis();
}
delay(50);
}
void handleButtons() {
// Read current button states
bool startStopState = digitalRead(START_STOP_BUTTON);
bool projectState = digitalRead(PROJECT_BUTTON);
bool displayState = digitalRead(DISPLAY_TOGGLE_BUTTON);
// Check for button press (HIGH to LOW transition) with debouncing
if (millis() - lastButtonPress > debounceDelay) {
// Start/Stop button
if (lastButtonStates[0] == HIGH && startStopState == LOW) {
Serial.println("START/STOP button pressed");
toggleTracking();
updateActivity();
lastButtonPress = millis();
}
// Project button (only when not tracking)
if (!isTracking && lastButtonStates[1] == HIGH && projectState == LOW) {
Serial.println("PROJECT button pressed");
cycleProject();
updateActivity();
lastButtonPress = millis();
}
// Display toggle button
if (lastButtonStates[2] == HIGH && displayState == LOW) {
Serial.println("DISPLAY TOGGLE button pressed");
toggleDisplay();
updateActivity();
lastButtonPress = millis();
}
}
// Update button states in correct order
lastButtonStates[0] = startStopState; // START_STOP_BUTTON
lastButtonStates[1] = projectState; // PROJECT_BUTTON
lastButtonStates[2] = displayState; // DISPLAY_TOGGLE_BUTTON
}
void toggleTracking() {
if (!isTracking) {
// Start tracking
startTime = millis();
sessionStartTimestamp = getCurrentTimestamp();
isTracking = true;
setLEDColor(0, 126, 0); // Green for tracking
Serial.println("Started tracking: " + currentProject + " at " + sessionStartTimestamp);
// Wake up display if in sleep mode
wakeUp();
} else {
// Stop tracking
elapsedTime += millis() - startTime;
isTracking = false;
setLEDColor(126, 0, 0); // Red for not tracking
Serial.println("Stopped tracking. Total: " + formatTime(elapsedTime));
// Log the session
logSession();
elapsedTime = 0; // Reset for next session
}
}
void cycleProject() {
currentProjectIndex = (currentProjectIndex + 1) % numProjects;
currentProject = projects[currentProjectIndex];
Serial.println("Selected project: " + currentProject);
// Flash project color briefly
setLEDColor(projectColors[currentProjectIndex][0],
projectColors[currentProjectIndex][1],
projectColors[currentProjectIndex][2]);
delay(500);
// Back to red (not tracking)
if (!isTracking) {
setLEDColor(126, 0, 0);
}
}
void updateDisplay() {
// Don't update display if it's off or in sleep mode
if (!displayOn || sleepMode) {
return;
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
// Battery status (top right)
display.setCursor(84, 48);
display.print("BAT:");
display.print(batteryPercentage);
display.print("%");
// Project name
display.setCursor(0, 0);
display.print("Project: ");
display.println(currentProject);
// Status
display.setCursor(0, 16);
if (isTracking) {
display.print("TRACKING");
unsigned long currentElapsed = elapsedTime + (millis() - startTime);
display.setCursor(0, 32);
display.print("Time: ");
display.println(formatTime(currentElapsed));
} else {
display.print("STOPPED");
if (elapsedTime > 0) {
display.setCursor(0, 32);
display.print("Last: ");
display.println(formatTime(elapsedTime));
}
}
// WiFi status
display.setCursor(0, 48);
if (WiFi.status() == WL_CONNECTED) {
display.print("WiFi: Good");
} else {
display.print("WiFi: Bad");
}
display.display();
}
void setLEDColor(int red, int green, int blue) {
// For common anode RGB LED, invert the PWM values
// 255 = LED off, 0 = LED full brightness
analogWrite(LED_RED_PIN, 255 - red);
analogWrite(LED_GREEN_PIN, 255 - green);
analogWrite(LED_BLUE_PIN, 255 - blue);
}
void toggleDisplay() {
displayOn = !displayOn;
if (displayOn) {
wakeUp();
Serial.println("Display turned ON");
} else {
display.clearDisplay();
display.display();
Serial.println("Display turned OFF");
}
}
void checkSleepMode() {
if (isTracking) {
return;
}
if (!sleepMode && millis() - lastActivity > SLEEP_TIMEOUT) {
enterSleepMode();
}
}
void enterSleepMode() {
sleepMode = true;
displayOn = false;
display.clearDisplay();
display.display();
setLEDColor(10, 0, 0); // Dim red
Serial.println("Entering sleep mode");
}
void wakeUp() {
if (sleepMode || !displayOn) {
sleepMode = false;
displayOn = true;
if (isTracking) {
setLEDColor(0, 126, 0); // Green
} else {
setLEDColor(126, 0, 0); // Red
}
updateDisplay();
Serial.println("Waking up from sleep mode");
}
updateActivity();
}
void updateActivity() {
lastActivity = millis();
if (sleepMode) {
wakeUp();
}
}
String formatTime(unsigned long milliseconds) {
unsigned long seconds = milliseconds / 1000;
unsigned long minutes = seconds / 60;
unsigned long hours = minutes / 60;
seconds %= 60;
minutes %= 60;
String timeStr = "";
if (hours > 0) {
timeStr += String(hours) + "h ";
}
if (minutes > 0 || hours > 0) {
timeStr += String(minutes) + "m ";
}
timeStr += String(seconds) + "s";
return timeStr;
}
void logSession() {
if (elapsedTime < 1000) {
Serial.println("Session too short to log");
return;
}
syncToSpreadsheet();
}
String getCurrentTimestamp() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return String(millis()); // Fallback
}
char buffer[32];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
String timestamp = String(buffer);
Serial.println("Generated timestamp: " + timestamp);
return timestamp;
}
void syncToSpreadsheet() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected, cannot sync");
return;
}
if (elapsedTime == 0) {
Serial.println("No data to sync");
return;
}
HTTPClient http;
http.begin(googleScriptURL);
http.addHeader("Content-Type", "application/json");
http.setTimeout(15000);
String endTimestamp = getCurrentTimestamp();
if (sessionStartTimestamp.length() == 0 || sessionStartTimestamp == "Time not set") {
Serial.println("Warning: sessionStartTimestamp is empty, calculating approximate start time");
unsigned long startMillis = millis() - elapsedTime;
sessionStartTimestamp = "Approx: " + String(startMillis);
}
StaticJsonDocument<400> doc;
doc["project"] = currentProject;
doc["duration"] = (unsigned long)(elapsedTime / 1000);
doc["startTime"] = sessionStartTimestamp;
doc["endTime"] = endTimestamp;
String jsonString;
serializeJson(doc, jsonString);
int httpResponseCode = http.POST(jsonString);
if (httpResponseCode > 0) {
String response = http.getString();
StaticJsonDocument<200> responseDoc;
DeserializationError error = deserializeJson(responseDoc, response);
if (!error) {
const char* status = responseDoc["status"];
const char* message = responseDoc["message"];
Serial.println("Status: " + String(status));
Serial.println("Message: " + String(message));
}
// Show sync status on display briefly
display.clearDisplay();
display.setCursor(0, 8);
display.print("Sync Code: ");
display.println(httpResponseCode);
display.setCursor(0, 24);
display.print("Status: ");
if (httpResponseCode >= 200 && httpResponseCode < 400) {
display.println("SUCCESS");
Serial.println("Sync successful!");
} else {
display.println("FAILED");
Serial.println("Sync failed with code: " + String(httpResponseCode));
}
display.display();
delay(3000);
} else {
Serial.println("HTTP request failed: " + String(httpResponseCode));
display.clearDisplay();
display.setCursor(0, 16);
display.print("HTTP Error: ");
display.println(httpResponseCode);
display.display();
delay(3000);
}
http.end();
}
void updateBatteryStatus() {
// Read ADC value
//int adcValue = analogRead(BATTERY_PIN);
// Convert ADC reading to voltage
// XIAO ESP32-C3 has built-in voltage divider, so we multiply by 2
//batteryVoltage = ((float)adcValue / ADC_RESOLUTION) * (VREF / 1000.0) * VOLTAGE_DIVIDER;
uint32_t batteryVoltage = 0;
for(int i = 0; i < 16; i++) {
batteryVoltage = batteryVoltage + analogReadMilliVolts(A0); // ADC with correction
}
float batteryVoltagef = 2 * batteryVoltage / 16 / 1000.0; // attenuation ratio 1/2, mV --> V
Serial.println(batteryVoltagef, 3);
//delay(1000);
// Convert voltage to percentage (typical Li-Po battery curve)
// 4.2V = 100%, 3.7V = 50%, 3.0V = 0%
if (batteryVoltagef >= 4.2) {
batteryPercentage = 100;
} else if (batteryVoltage >= 3.7) {
// Linear interpolation between 3.7V (50%) and 4.2V (100%)
batteryPercentage = (int)(50 + ((batteryVoltagef - 3.7) / 0.5) * 50);
} else if (batteryVoltagef >= 3.0) {
// Linear interpolation between 3.0V (0%) and 3.7V (50%)
batteryPercentage = (int)((batteryVoltagef - 3.0) / 0.7 * 50);
} else {
batteryPercentage = 0;
}
// Clamp percentage between 0 and 100
batteryPercentage = constrain(batteryPercentage, 0, 100);
// Debug output
Serial.println("Voltage: " + String(batteryVoltagef, 2) + "V" + ", Percentage: " + String(batteryPercentage) + "%");
}