Merge branch 'ota-updates' into 'main'

Ota updates

See merge request airguns/chronodisplay!2
This commit is contained in:
dan
2026-05-28 18:01:34 +00:00
6 changed files with 169 additions and 57 deletions
+18 -28
View File
@@ -1,6 +1,10 @@
#include "ChronoBLE.h"
#include <NimBLEDevice.h>
// 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);
}
}
}
+4
View File
@@ -5,6 +5,10 @@
#include <NimBLEDevice.h>
#include <GunProfile.h>
// 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 {
+57 -26
View File
@@ -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 <LinkedList.h>
#include "Display.h"
#include "ChronoReadingManager.h"
#include "ChronoWebServer.h"
#include "FileManager.h"
#include <WiFi.h>
#include <time.h>
#include <sys/time.h>
//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();
+83 -1
View File
@@ -5,6 +5,7 @@
#include <FileManager.h>
#include <ChronoProfile.h>
#include <GunProfile.h>
#include <Update.h>
#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() {
<button onclick='format()'>Format</button>\
<button onclick='toggleTestMode()'>Toggle Test Mode</button>\
</div>\
<div style='margin-top:20px; padding: 10px; border: 1px solid #999;'>\
<h3>OTA Firmware Update</h3>\
<p style='font-size: 12px; color: #666;'>Current Version: <strong id='currentVersion'>Unknown</strong></p>\
<input type='file' id='otaFile' accept='.bin,.bin' style='margin-bottom: 10px;'></input>\
<p style='font-size: 12px; color: #666;'>1. Select a .bin firmware file to update over-the-air</p>\
<button onclick='doOta()' style='margin-top: 5px; padding: 8px 16px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;'>OTA Upgrade</button>\
<p style='font-size: 12px; color: #666;'>2. Click OTA Upgrade to install</p>\
</div>\
<div style='margin-top:100px; position:relative;'>\
<h3>Gun Profiles</h3>\
<table style='width:600px; min-height:100px; border-collapse: collapse;'>\
@@ -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);
}
+3
View File
@@ -8,6 +8,7 @@
#include <Update.h>
#include <ChronoBLE.h>
#include <GunProfile.h>
#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();
};
+4 -2
View File
@@ -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