added support for ota and ntp
This commit is contained in:
+40
-21
@@ -6,6 +6,9 @@
|
|||||||
#include "ChronoReadingManager.h"
|
#include "ChronoReadingManager.h"
|
||||||
#include "ChronoWebServer.h"
|
#include "ChronoWebServer.h"
|
||||||
#include "FileManager.h"
|
#include "FileManager.h"
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
//testing mode variables
|
//testing mode variables
|
||||||
static bool testMode=false;
|
static bool testMode=false;
|
||||||
@@ -64,10 +67,13 @@ void setup()
|
|||||||
FileManager& files = FileManager::getInstance();
|
FileManager& files = FileManager::getInstance();
|
||||||
files.init();
|
files.init();
|
||||||
|
|
||||||
//initialize web portal
|
//initialize web portal (also connects to WiFi)
|
||||||
webServer.init();
|
webServer.init();
|
||||||
webServer.setTestModeCallback(toggleTestMode);
|
webServer.setTestModeCallback(toggleTestMode);
|
||||||
|
|
||||||
|
//sync time via NTP after WiFi is connected
|
||||||
|
setDateTime();
|
||||||
|
|
||||||
//initialize display
|
//initialize display
|
||||||
Display::init();
|
Display::init();
|
||||||
|
|
||||||
@@ -83,32 +89,45 @@ void setup()
|
|||||||
chronoBLE.setSpeedCallback(speedCallback);
|
chronoBLE.setSpeedCallback(speedCallback);
|
||||||
}
|
}
|
||||||
void setDateTime(){
|
void setDateTime(){
|
||||||
// Example: Set time to January 1, 2025, 12:00:00
|
// NTP server configuration
|
||||||
struct tm t;
|
configTime("EST5EDT,M3.2.0/2,M11.1.0/2", "pool.ntp.org", "time.nist.gov");
|
||||||
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
|
|
||||||
|
|
||||||
time_t epoch_time = mktime(&t);
|
// Wait for time to be set
|
||||||
|
|
||||||
struct timeval tv;
|
|
||||||
tv.tv_sec = epoch_time;
|
|
||||||
tv.tv_usec = 0;
|
|
||||||
|
|
||||||
settimeofday(&tv, NULL);
|
|
||||||
|
|
||||||
// Verify the set time
|
|
||||||
time_t now;
|
time_t now;
|
||||||
char strftime_buf[64];
|
|
||||||
struct tm timeinfo;
|
struct tm timeinfo;
|
||||||
|
int retry = 0;
|
||||||
|
const int maxRetry = 20; // 10 seconds max
|
||||||
|
|
||||||
|
while (retry < maxRetry) {
|
||||||
time(&now);
|
time(&now);
|
||||||
localtime_r(&now, &timeinfo);
|
localtime_r(&now, &timeinfo);
|
||||||
strftime(strftime_buf, sizeof(strftime_buf), "%c", &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(){
|
void updateTime(){
|
||||||
time_t now;
|
time_t now;
|
||||||
|
|||||||
@@ -73,6 +73,26 @@ void ChronoWebServer::_handleRoot() {
|
|||||||
.then(data => console.log(data))\
|
.then(data => console.log(data))\
|
||||||
.catch(error => console.error('Error:', error));\
|
.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() {\
|
document.addEventListener('DOMContentLoaded', function() {\
|
||||||
const rows = document.querySelectorAll('.clickable-row');\
|
const rows = document.querySelectorAll('.clickable-row');\
|
||||||
rows.forEach(row => {\
|
rows.forEach(row => {\
|
||||||
@@ -114,6 +134,12 @@ void ChronoWebServer::_handleRoot() {
|
|||||||
<button onclick='reset()'>Reset</button>\
|
<button onclick='reset()'>Reset</button>\
|
||||||
<button onclick='format()'>Format</button>\
|
<button onclick='format()'>Format</button>\
|
||||||
<button onclick='toggleTestMode()'>Toggle Test Mode</button>\
|
<button onclick='toggleTestMode()'>Toggle Test Mode</button>\
|
||||||
|
<button onclick='doOta()'>OTA Update</button>\
|
||||||
|
</div>\
|
||||||
|
<div style='margin-top:20px; padding: 10px; border: 1px solid #999;'>\
|
||||||
|
<h3>OTA Firmware Update</h3>\
|
||||||
|
<input type='file' id='otaFile' accept='.bin,.bin'></input>\
|
||||||
|
<p style='font-size: 12px; color: #666;'>Select a .bin firmware file to update over-the-air</p>\
|
||||||
</div>\
|
</div>\
|
||||||
<div style='margin-top:100px; position:relative;'>\
|
<div style='margin-top:100px; position:relative;'>\
|
||||||
<h3>Gun Profiles</h3>\
|
<h3>Gun Profiles</h3>\
|
||||||
@@ -434,6 +460,69 @@ void ChronoWebServer::_handleTestMode(){
|
|||||||
testModeCallback();
|
testModeCallback();
|
||||||
_server.send(200, "text/html", "ok");
|
_server.send(200, "text/html", "ok");
|
||||||
}
|
}
|
||||||
|
void ChronoWebServer::_handleOta(){
|
||||||
|
HTTPMethod method = _server.method();
|
||||||
|
|
||||||
|
if (method == HTTP_GET) {
|
||||||
|
// Serve OTA update page
|
||||||
|
String temp = "<html>\
|
||||||
|
<head>\
|
||||||
|
<title>OTA Firmware Update</title>\
|
||||||
|
<style>\
|
||||||
|
body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
|
||||||
|
</style>\
|
||||||
|
</head>\
|
||||||
|
<body>\
|
||||||
|
<h1>OTA Firmware Update</h1>\
|
||||||
|
<p>Select a .bin firmware file to update over-the-air.</p>\
|
||||||
|
<form method='POST' action='/ota' enctype='multipart/form-data'>\
|
||||||
|
<div>\
|
||||||
|
<h3>Firmware File</h3>\
|
||||||
|
<input type='file' name='otaFile' accept='.bin'></input>\
|
||||||
|
</div>\
|
||||||
|
<div style='margin-top: 20px;'>\
|
||||||
|
<button type='submit'>Upload</button>\
|
||||||
|
</div>\
|
||||||
|
</form>\
|
||||||
|
</body>\
|
||||||
|
</html>";
|
||||||
|
_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", "<h1>OTA Update Successful!<br>Restarting...</h1>");
|
||||||
|
delay(1000);
|
||||||
|
ESP.restart();
|
||||||
|
} else {
|
||||||
|
Serial.println("OTA: Error: Update end failed");
|
||||||
|
_server.send(500, "text/plain", "Update failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
void ChronoWebServer::init(){
|
void ChronoWebServer::init(){
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.begin(ssid, password);
|
WiFi.begin(ssid, password);
|
||||||
@@ -463,6 +552,7 @@ void ChronoWebServer::init(){
|
|||||||
_server.on("/reset", _handleReset);
|
_server.on("/reset", _handleReset);
|
||||||
_server.on("/format", _handleFormat);
|
_server.on("/format", _handleFormat);
|
||||||
_server.on("/testmode", _handleTestMode);
|
_server.on("/testmode", _handleTestMode);
|
||||||
|
_server.on("/ota", _handleOta);
|
||||||
_server.on("/", _handleRoot);
|
_server.on("/", _handleRoot);
|
||||||
_server.begin();
|
_server.begin();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class ChronoWebServer {
|
|||||||
static void _handleReset();
|
static void _handleReset();
|
||||||
static void _handleFormat();
|
static void _handleFormat();
|
||||||
static void _handleTestMode();
|
static void _handleTestMode();
|
||||||
|
static void _handleOta();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) |
|
| `/reset` | GET | Clear all readings (shot count, stats) |
|
||||||
| `/format` | GET | Format LittleFS — erase all stored profiles |
|
| `/format` | GET | Format LittleFS — erase all stored profiles |
|
||||||
| `/testmode` | GET | Toggle test mode (simulated readings every 5s) |
|
| `/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
|
### Gun Profile Types
|
||||||
|
|
||||||
@@ -200,8 +202,8 @@ Initial development. The project compiles and runs with the following functional
|
|||||||
- ✅ CSV export of readings
|
- ✅ CSV export of readings
|
||||||
- ✅ Persistent profile storage
|
- ✅ Persistent profile storage
|
||||||
- ✅ Test mode for display debugging
|
- ✅ Test mode for display debugging
|
||||||
- ⏳ OTA firmware updates (Update.h included but not implemented)
|
- ✅ OTA firmware updates (via web UI at /ota)
|
||||||
- ⏳ Time synchronization via NTP (currently hardcoded)
|
- ✅ Time synchronization via NTP
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user