diff --git a/ChronoBLE.cpp b/ChronoBLE.cpp index ec9b056..8800146 100644 --- a/ChronoBLE.cpp +++ b/ChronoBLE.cpp @@ -1,6 +1,10 @@ #include "ChronoBLE.h" #include +// Flag to signal that a new reading is ready to be processed +volatile bool newReadingAvailable = false; +volatile int lastReadFPS = 0; + static NimBLEClient* _pClient = nullptr; static ChronoBLE::ConnectionState currentState; static const NimBLEAdvertisedDevice* advDevice = nullptr; @@ -61,39 +65,25 @@ class ScanCallbacks : public NimBLEScanCallbacks { //notification callback void notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { uint16_t speed; - char sbuffer[256]; - std::string str = (isNotify == true) ? "Notification" : "Indication"; - str += " from "; - str += pRemoteCharacteristic->getClient()->getPeerAddress().toString(); - str += ": Service = " + pRemoteCharacteristic->getRemoteService()->getUUID().toString(); - str += ", Characteristic = " + pRemoteCharacteristic->getUUID().toString(); - str += ", Value = " + std::string((char*)pData, length); - // Serial.printf("%s\n", str.c_str()); - // Serial.printf("%d\n",pData); - //if(length>0){ - speed = ((char*)pData)[0]; + // Extract speed from data - only what's needed, avoid String allocations + if (length >= 2) { + speed = pData[0]; speed <<= 8; - speed |= ((char*)pData)[1]; + speed |= pData[1]; - if (speed>0){ - float energy; - float fspeed = speed; - /* Draw the speed string */ - //if(units == UNITS_IMPERIAL) { - fspeed *= 0.0475111859; - //sprintf (sbuffer, "%d FPS", int(fspeed)); - Serial.printf("%d FPS\n", int(fspeed)); - //} else { - //fspeed *= 0.014481409; - //sprintf (sbuffer, "%d M/S", int(fspeed)); - //} - //} - if (speedCallback!= nullptr){ - speedCallback(fspeed); + if (speed > 0) { + // Convert to FPS + float fspeed = speed * 0.0475111859; + + // Store the reading for later processing in main loop + // This avoids heap fragmentation and display updates in interrupt context + lastReadFPS = (int)fspeed; + newReadingAvailable = true; + + Serial.printf("%d FPS\n", (int)fspeed); } } - } diff --git a/ChronoBLE.h b/ChronoBLE.h index 9034f68..6a94530 100644 --- a/ChronoBLE.h +++ b/ChronoBLE.h @@ -5,6 +5,10 @@ #include #include +// Flag to signal that a new reading is ready to be processed +extern volatile bool newReadingAvailable; +extern volatile int lastReadFPS; + typedef void (*CallbackFunction)(); class ChronoBLE { diff --git a/ChronoDisplay.ino b/ChronoDisplay.ino index 6bc22cc..22ff5e7 100644 --- a/ChronoDisplay.ino +++ b/ChronoDisplay.ino @@ -1,11 +1,19 @@ #include "TFT_eSPI.h" #include "ChronoBLE.h" +#include "ChronoVersion.h" + +// External declaration for BLE reading flag and value +extern volatile bool newReadingAvailable; +extern volatile int lastReadFPS; #include "ChronoReading.h" #include #include "Display.h" #include "ChronoReadingManager.h" #include "ChronoWebServer.h" #include "FileManager.h" +#include +#include +#include //testing mode variables static bool testMode=false; @@ -64,10 +72,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,41 +94,53 @@ 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 - ESP32 API uses gmtOffset (seconds) and dstOffset (seconds) + // EST = UTC-5, EDT = UTC-4, so gmtOffset = -5*3600 = -18000 + configTime(-18000, 3600, "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; char strftime_buf[64]; struct tm timeinfo; time(&now); - const char* TZ_EST = "EST5EDT,M3.2.0/2,M11.1.0/2"; - setenv("TZ", TZ_EST, 1); - tzset(); + // Use the timezone that was configured via configTime localtime_r(&now, &timeinfo); strftime(strftime_buf, sizeof(strftime_buf), "%R", &timeinfo); @@ -172,6 +195,14 @@ void loop() } ////////////////////////////////////////////////////// + // Check for new BLE readings and process them + // This is done here instead of in the BLE callback to avoid heap fragmentation + // and display updates in interrupt context + if (newReadingAvailable) { + newReadingAvailable = false; + addReading(lastReadFPS); + } + //update display Display::refreshDisplay(); diff --git a/ChronoWebServer.cpp b/ChronoWebServer.cpp index b1aa0bf..7f233ac 100644 --- a/ChronoWebServer.cpp +++ b/ChronoWebServer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "Display.h" const char *ssid = "routeguy"; @@ -73,6 +74,31 @@ void ChronoWebServer::_handleRoot() { .then(data => console.log(data))\ .catch(error => console.error('Error:', error));\ }\ + fetch('/version').then(r=>r.json()).then(v=>{document.getElementById('currentVersion').textContent=v.version}).catch(()=>{});\ + 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]);\ + const xhr = new XMLHttpRequest();\ + xhr.open('POST', '/ota', true);\ + xhr.onload = function () {\ + if (xhr.status >= 200 && xhr.status < 300) {\ + const responseData = JSON.parse(xhr.responseText);\ + console.log('Success:', responseData);\ + window.location.href = '/';\ + } else {\ + console.error('Server error:', xhr.status);\ + }\ + };\ + xhr.onerror = function () {\ + console.error('Network request failed');\ + };\ + xhr.send(formData);\ + }\ document.addEventListener('DOMContentLoaded', function() {\ const rows = document.querySelectorAll('.clickable-row');\ rows.forEach(row => {\ @@ -115,6 +141,14 @@ void ChronoWebServer::_handleRoot() { \ \ \ +
\ +

OTA Firmware Update

\ +

Current Version: Unknown

\ + \ +

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

\ + \ +

2. Click OTA Upgrade to install

\ +
\
\

Gun Profiles

\ \ @@ -430,10 +464,50 @@ 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 handleOtaUpdate() { + size_t fsize = UPDATE_SIZE_UNKNOWN; + if (_server.hasArg("size")) { + fsize = _server.arg("size").toInt(); + } + HTTPUpload &upload = _server.upload(); + if (upload.status == UPLOAD_FILE_START) { + + Serial.printf("Receiving Update: %s, Size: %d\n", upload.filename.c_str(), fsize); + if (!Update.begin(fsize)) { + //otaDone = 0; + //Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + // Update.printError(Serial); + } else { + // otaDone = 100 * Update.progress() / Update.size(); + } + } else if (upload.status == UPLOAD_FILE_END) { + if (Update.end(true)) { + Serial.printf("Update Success: %u bytes\nRebooting...\n", upload.totalSize); + } else { + Serial.printf("%s\n", Update.errorString()); + // otaDone = 0; + } + } +} +void handleOtaUpdateEnd() { + _server.sendHeader("Connection", "close"); + if (Update.hasError()) { + _server.send(502, "text/plain", Update.errorString()); + } else { + _server.sendHeader("Refresh", "10"); + _server.sendHeader("Location", "/"); + _server.send(307); + delay(500); + ESP.restart(); + } +} void ChronoWebServer::init(){ WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); @@ -463,9 +537,17 @@ void ChronoWebServer::init(){ _server.on("/reset", _handleReset); _server.on("/format", _handleFormat); _server.on("/testmode", _handleTestMode); + _server.on("/ota", HTTP_POST, [](){handleOtaUpdateEnd();}, [](){handleOtaUpdate();}); + _server.on("/version", HTTP_GET, _handleVersion); _server.on("/", _handleRoot); _server.begin(); } + void ChronoWebServer::process(){ _server.handleClient(); +} + +void ChronoWebServer::_handleVersion() { + String versionJson = String("{\"version\":\"") + CHRONO_VERSION + "\",\"major\":" + String(CHRONO_VERSION_MAJOR) + ",\"minor\":" + String(CHRONO_VERSION_MINOR) + ",\"patch\":" + String(CHRONO_VERSION_PATCH) + "}"; + _server.send(200, "application/json", versionJson); } \ No newline at end of file diff --git a/ChronoWebServer.h b/ChronoWebServer.h index 3653c7c..3e0ad0b 100644 --- a/ChronoWebServer.h +++ b/ChronoWebServer.h @@ -8,6 +8,7 @@ #include #include #include +#include "ChronoVersion.h" class ChronoWebServer { public: @@ -33,6 +34,8 @@ class ChronoWebServer { static void _handleReset(); static void _handleFormat(); static void _handleTestMode(); + static void _handleOta(); + static void _handleVersion(); }; 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