diff --git a/ChronoDisplay.ino b/ChronoDisplay.ino index 6bc22cc..dd3828a 100644 --- a/ChronoDisplay.ino +++ b/ChronoDisplay.ino @@ -6,6 +6,9 @@ #include "ChronoReadingManager.h" #include "ChronoWebServer.h" #include "FileManager.h" +#include +#include +#include //testing mode variables static bool testMode=false; @@ -64,10 +67,13 @@ void setup() FileManager& files = FileManager::getInstance(); files.init(); - //initialize web portal + //initialize web portal (also connects to WiFi) webServer.init(); webServer.setTestModeCallback(toggleTestMode); + //sync time via NTP after WiFi is connected + setDateTime(); + //initialize display Display::init(); @@ -83,32 +89,45 @@ void setup() chronoBLE.setSpeedCallback(speedCallback); } void setDateTime(){ - // Example: Set time to January 1, 2025, 12:00:00 - struct tm t; - t.tm_year = 2025 - 1900; // Year - 1900 - t.tm_mon = 7; // Month (0-11, where 0 = Jan) - t.tm_mday = 4; // Day of the month (1-31) - t.tm_hour = 14; // Hour (0-23) - t.tm_min = 56; // Minute (0-59) - t.tm_sec = 0; // Second (0-59) - t.tm_isdst = -1; // Is DST on? 1 = yes, 0 = no, -1 = unknown + // NTP server configuration + configTime("EST5EDT,M3.2.0/2,M11.1.0/2", "pool.ntp.org", "time.nist.gov"); - time_t epoch_time = mktime(&t); - - struct timeval tv; - tv.tv_sec = epoch_time; - tv.tv_usec = 0; - - settimeofday(&tv, NULL); - - // Verify the set time + // Wait for time to be set time_t now; - char strftime_buf[64]; struct tm timeinfo; + int retry = 0; + const int maxRetry = 20; // 10 seconds max - time(&now); - localtime_r(&now, &timeinfo); - strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo); + while (retry < maxRetry) { + time(&now); + localtime_r(&now, &timeinfo); + + // Check if we have a valid time (year > 2020) + if (timeinfo.tm_year > 120) { // 120 = year 2020 + Serial.println("Time synced via NTP"); + break; + } + Serial.print("Waiting for NTP sync... "); + Serial.println(retry); + delay(500); + retry++; + } + + // If still not synced, fall back to a default date + if (timeinfo.tm_year <= 120) { + Serial.println("NTP sync failed, using fallback date"); + timeinfo.tm_year = 2024 - 1900; + timeinfo.tm_mon = 0; // January + timeinfo.tm_mday = 1; + timeinfo.tm_hour = 0; + timeinfo.tm_min = 0; + timeinfo.tm_sec = 0; + timeinfo.tm_isdst = -1; + + now = mktime(&timeinfo); + struct timeval tv = {.tv_sec = now}; + settimeofday(&tv, NULL); + } } void updateTime(){ time_t now; diff --git a/ChronoWebServer.cpp b/ChronoWebServer.cpp index b1aa0bf..e9ded74 100644 --- a/ChronoWebServer.cpp +++ b/ChronoWebServer.cpp @@ -73,6 +73,26 @@ void ChronoWebServer::_handleRoot() { .then(data => console.log(data))\ .catch(error => console.error('Error:', error));\ }\ + function doOta(){\ + const fileInput = document.getElementById('otaFile');\ + if (!fileInput.files.length) {\ + alert('Please select a firmware file');\ + return;\ + }\ + const formData = new FormData();\ + formData.append('otaFile', fileInput.files[0]);\ + fetch('/ota', {\ + method: 'POST',\ + body: formData\ + })\ + .then(response => response.text())\ + .then(data => {\ + console.log(data);\ + alert('OTA update in progress... Device will restart.');\ + window.location.href = '/';\ + })\ + .catch(error => console.error('Error:', error));\ + }\ document.addEventListener('DOMContentLoaded', function() {\ const rows = document.querySelectorAll('.clickable-row');\ rows.forEach(row => {\ @@ -114,6 +134,12 @@ void ChronoWebServer::_handleRoot() { \ \ \ + \ + \ +
\ +

OTA Firmware Update

\ + \ +

Select a .bin firmware file to update over-the-air

\
\
\

Gun Profiles

\ @@ -430,10 +456,73 @@ void ChronoWebServer::setTestModeCallback(void (*pFunc)()){ testModeCallback=pFunc; } void ChronoWebServer::_handleTestMode(){ - Serial.println("toggling test mode...."); + Serial.println("toggling test mode...."); testModeCallback(); _server.send(200, "text/html", "ok"); } +void ChronoWebServer::_handleOta(){ + HTTPMethod method = _server.method(); + + if (method == HTTP_GET) { + // Serve OTA update page + String temp = "\ + \ + OTA Firmware Update\ + \ + \ + \ +

OTA Firmware Update

\ +

Select a .bin firmware file to update over-the-air.

\ +
\ +
\ +

Firmware File

\ + \ +
\ +
\ + \ +
\ +
\ + \ + "; + _server.send(200, "text/html", temp); + } + else if (method == HTTP_POST) { + // Handle firmware upload + HTTPUpload& upload = _server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + Serial.println("OTA: Starting update..."); + // Initialize update with the size of the uploaded file + if (!Update.begin(upload.totalSize)) { + Serial.println("OTA: Error: Not enough space"); + _server.send(500, "text/plain", "Not enough space"); + return; + } + } + else if (upload.status == UPLOAD_FILE_WRITE) { + // Write received firmware to flash + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + Serial.println("OTA: Error: Write failed"); + _server.send(500, "text/plain", "Write failed"); + return; + } + } + else if (upload.status == UPLOAD_FILE_END) { + // End of upload + if (Update.end(true)) { + Serial.printf("OTA: Update complete: %u bytes\n", upload.totalSize); + _server.send(200, "text/html", "

OTA Update Successful!
Restarting...

"); + delay(1000); + ESP.restart(); + } else { + Serial.println("OTA: Error: Update end failed"); + _server.send(500, "text/plain", "Update failed"); + } + } + } +} void ChronoWebServer::init(){ WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); @@ -463,6 +552,7 @@ void ChronoWebServer::init(){ _server.on("/reset", _handleReset); _server.on("/format", _handleFormat); _server.on("/testmode", _handleTestMode); + _server.on("/ota", _handleOta); _server.on("/", _handleRoot); _server.begin(); } diff --git a/ChronoWebServer.h b/ChronoWebServer.h index 3653c7c..3243375 100644 --- a/ChronoWebServer.h +++ b/ChronoWebServer.h @@ -33,6 +33,7 @@ class ChronoWebServer { static void _handleReset(); static void _handleFormat(); static void _handleTestMode(); + static void _handleOta(); }; diff --git a/README.md b/README.md index 96c2951..0b8fdb4 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,8 @@ Connect to the device's WiFi network (or existing network — SSID and password | `/reset` | GET | Clear all readings (shot count, stats) | | `/format` | GET | Format LittleFS — erase all stored profiles | | `/testmode` | GET | Toggle test mode (simulated readings every 5s) | +| `/ota` | GET | OTA firmware update page | +| `/ota` | POST | Upload and apply firmware update (.bin file) | ### Gun Profile Types @@ -200,8 +202,8 @@ Initial development. The project compiles and runs with the following functional - ✅ CSV export of readings - ✅ Persistent profile storage - ✅ Test mode for display debugging -- ⏳ OTA firmware updates (Update.h included but not implemented) -- ⏳ Time synchronization via NTP (currently hardcoded) +- ✅ OTA firmware updates (via web UI at /ota) +- ✅ Time synchronization via NTP ## License