From f4201b974b92939e22a04761611e758736b5abcc Mon Sep 17 00:00:00 2001 From: Dan Priece Date: Tue, 19 May 2026 09:15:34 -0400 Subject: [PATCH] initial commit --- .DS_Store | Bin 0 -> 8196 bytes ChronoBLE.cpp | 397 +++++++++++++++++++++++++++++++++ ChronoBLE.h | 41 ++++ ChronoDisplay.ino | 179 +++++++++++++++ ChronoProfile.cpp | 12 + ChronoProfile.h | 27 +++ ChronoReading.cpp | 22 ++ ChronoReading.h | 26 +++ ChronoReadingManager.cpp | 173 ++++++++++++++ ChronoReadingManager.h | 49 ++++ ChronoWebServer.cpp | 471 +++++++++++++++++++++++++++++++++++++++ ChronoWebServer.h | 39 ++++ Display.cpp | 278 +++++++++++++++++++++++ Display.h | 49 ++++ FileManager.cpp | 247 ++++++++++++++++++++ FileManager.h | 33 +++ GunProfile.cpp | 10 + GunProfile.h | 30 +++ ProjectileProfile.cpp | 14 ++ ProjectileProfile.h | 31 +++ driver.h | 2 + 21 files changed, 2130 insertions(+) create mode 100644 .DS_Store create mode 100644 ChronoBLE.cpp create mode 100644 ChronoBLE.h create mode 100644 ChronoDisplay.ino create mode 100644 ChronoProfile.cpp create mode 100644 ChronoProfile.h create mode 100644 ChronoReading.cpp create mode 100644 ChronoReading.h create mode 100644 ChronoReadingManager.cpp create mode 100644 ChronoReadingManager.h create mode 100644 ChronoWebServer.cpp create mode 100644 ChronoWebServer.h create mode 100644 Display.cpp create mode 100644 Display.h create mode 100644 FileManager.cpp create mode 100644 FileManager.h create mode 100644 GunProfile.cpp create mode 100644 GunProfile.h create mode 100644 ProjectileProfile.cpp create mode 100644 ProjectileProfile.h create mode 100644 driver.h diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5cf62d8e096e3a0359b2eddaa9f0266bca402660 GIT binary patch literal 8196 zcmeHM-)j>=5T0$LUWh*`Z6z6hW%=z_lB=tB}o*_#*N^0*d86NZR{oSpb^IHo&a^aAqa;3IQ*(%yGOx#AHKI1y&B&@?Kk2OAF!Ezt zu(SKiLYomEz-xnAGOH)q{#Q8i#r|yzmVdc6zNOY=(TLtezd$T8+6kiSWk@hT7e~bW7*yw84! z+JSJb?W4U%W8ryIr6mNNEXSc`IS&2zABGtFn97)n)>EZ%2XgKo0^Ivw^6t8e&A=Z* Co1lpR literal 0 HcmV?d00001 diff --git a/ChronoBLE.cpp b/ChronoBLE.cpp new file mode 100644 index 0000000..ec9b056 --- /dev/null +++ b/ChronoBLE.cpp @@ -0,0 +1,397 @@ +#include "ChronoBLE.h" +#include + +static NimBLEClient* _pClient = nullptr; +static ChronoBLE::ConnectionState currentState; +static const NimBLEAdvertisedDevice* advDevice = nullptr; +static uint32_t scanTimeMs = 5000; +static NimBLEUUID deviceUUID("0000180a-0000-1000-8000-00805f9b34fb"); +static NimBLEUUID serviceUUID("00001623-88EC-688C-644B-3FA706C0BB75"); +static NimBLEUUID speedCharacteristicUUID("00001624-88EC-688C-644B-3FA706C0BB75"); +static NimBLEUUID profileSettingLowUUID("00001626-88EC-688C-644B-3FA706C0BB75"); +static NimBLEUUID profileSettingHighUUID("00001628-88EC-688C-644B-3FA706C0BB75"); +static const char * charUUIDs[] = { + "00001624-88EC-688C-644B-3FA706C0BB75", //notify for speed value? + "00001625-88EC-688C-644B-3FA706C0BB75", //00 + "00001626-88EC-688C-644B-3FA706C0BB75", //power setting low? + "00001627-88EC-688C-644B-3FA706C0BB75", //battery level + "00001628-88EC-688C-644B-3FA706C0BB75", // power setting high? + "00001629-88EC-688C-644B-3FA706C0BB75", //00 + "0000162A-88EC-688C-644B-3FA706C0BB75", + "00002902-0000-1000-8000-00805f9b34fb" +}; + +void (*speedCallback)(int speed); + +//specific to FX chrono settings +uint8_t profile_bytes[2][5] = {{0x32, 0x32, 0x32, 0x32, 0x64},{0x17, 0x1E, 0x2D, 0x43, 0x5A}}; + + +//connection callback +class ClientCallbacks : public NimBLEClientCallbacks { + void onConnect(NimBLEClient* pClient) override { Serial.printf("Connected\n"); } + + void onDisconnect(NimBLEClient* pClient, int reason) override { + Serial.printf("%s Disconnected, reason = %d\n", pClient->getPeerAddress().toString().c_str(), reason); + //NimBLEDevice::getScan()->start(scanTimeMs, false, true); + currentState = ChronoBLE::IDLE; + } +} clientCallbacks; + +//device scan callback +class ScanCallbacks : public NimBLEScanCallbacks { + void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override { + //Serial.printf("Advertised Device found: %s\n", advertisedDevice->toString().c_str()); + if (advertisedDevice->isAdvertisingService(deviceUUID)) { + //Serial.printf("Found device!!!\n"); + /** stop scan before connecting */ + NimBLEDevice::getScan()->stop(); + /** Save the device reference in a global for the client to use*/ + advDevice = advertisedDevice; + } + } + + /** Callback to process the results of the completed scan or restart it */ + void onScanEnd(const NimBLEScanResults& results, int reason) override { + //Serial.printf("Scan Ended, reason: %d, device count: %d; Restarting scan\n", reason, results.getCount()); + NimBLEDevice::getScan()->start(scanTimeMs, false, true); + } +} scanCallbacks; + +//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]; + speed <<= 8; + speed |= ((char*)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); + } + } + +} + + +// Constructor implementation +ChronoBLE::ChronoBLE() { + _setState(IDLE); +} +void ChronoBLE::init() { + NimBLEDevice::init("FX-Chrono-Client"); + NimBLEDevice::setPower(3); /** 3dbm */ + NimBLEScan* pScan = NimBLEDevice::getScan(); + pScan->setScanCallbacks(&scanCallbacks, false); + pScan->setInterval(100); + pScan->setWindow(100); + pScan->setActiveScan(true); +} +void ChronoBLE::_setState(ConnectionState state){ + currentState=state; +} +void ChronoBLE::setSpeedCallback(void (*pFunc)(int speed)){speedCallback=pFunc;} +bool ChronoBLE::isIdle(){if (currentState==IDLE){return true;}return false;} +bool ChronoBLE::isConnected(){ + if (currentState==CONNECTED){ + if (_pClient!= nullptr){ + if (!_pClient->isConnected()){ + _disconnectDevice(); + return false; + } + return true; + } + _disconnectDevice(); + } + return false; +} +bool ChronoBLE::foundDevice(){if (advDevice!=nullptr){return true;}return false;} +void ChronoBLE::startScan(){ + Serial.println("starting scan!!!"); + _setState(SCANNING); + NimBLEDevice::getScan()->start(scanTimeMs, false, true); +} +void ChronoBLE::_disconnectDevice(){ + _setState(IDLE); + advDevice = nullptr; +} +bool ChronoBLE::connectToDevice() { + _setState(CONNECTING); + + /** Check if we have a client we should reuse first **/ + if (NimBLEDevice::getCreatedClientCount()) { + /** + * Special case when we already know this device, we send false as the + * second argument in connect() to prevent refreshing the service database. + * This saves considerable time and power. + */ + _pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress()); + if (_pClient) { + if (!_pClient->connect(advDevice, false)) { + _disconnectDevice(); + return false; + } + } else { + /** + * We don't already have a client that knows this device, + * check for a client that is disconnected that we can use. + */ + _pClient = NimBLEDevice::getDisconnectedClient(); + } + } + + /** No client to reuse? Create a new one. */ + if (!_pClient) { + if (NimBLEDevice::getCreatedClientCount() >= NIMBLE_MAX_CONNECTIONS) { + Serial.printf("Max clients reached - no more connections available\n"); + _disconnectDevice(); + return false; + } + + _pClient = NimBLEDevice::createClient(); + + _pClient->setClientCallbacks(&clientCallbacks, false); + /** + * Set initial connection parameters: + * These settings are safe for 3 clients to connect reliably, can go faster if you have less + * connections. Timeout should be a multiple of the interval, minimum is 100ms. + * Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 150 * 10ms = 1500ms timeout + */ + _pClient->setConnectionParams(12, 12, 0, 150); + + /** Set how long we are willing to wait for the connection to complete (milliseconds), default is 30000. */ + _pClient->setConnectTimeout(5 * 1000); + + if (!_pClient->connect(advDevice)) { + /** Created a client but failed to connect, don't need to keep it as it has no data */ + NimBLEDevice::deleteClient(_pClient); + _disconnectDevice(); + return false; + } + } + + if (!_pClient->isConnected()) { + if (!_pClient->connect(advDevice)) { + _disconnectDevice(); + return false; + } + } + + /** Now we can read/write/subscribe the characteristics of the services we are interested in */ + NimBLERemoteService* pSvc = nullptr; + NimBLERemoteCharacteristic* pChr = nullptr; + NimBLERemoteDescriptor* pDsc = nullptr; + + //set speed profile to default: FAC + /* if (!setSpeedProfile(0x64, 0x5A)){ + Serial.println("unable to set speed profile!!!!"); + return false; + }*/ + + pSvc = _pClient->getService(serviceUUID); + if (pSvc) { + pChr = pSvc->getCharacteristic(profileSettingLowUUID); + if (pChr) { + //if (pChr->canRead()) { + //Serial.printf("%s Value: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + if (pChr->canWrite()) { + if (pChr->writeValue(profile_bytes[0][1])) { + Serial.printf("Wrote new value to: %s\n", pChr->getUUID().toString().c_str()); + } else { + _pClient->disconnect(); + _disconnectDevice(); + return false; + } + //if (pChr->canRead()) { + //Serial.printf("The value of: %s is now: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + } + } + } else { + Serial.printf("unable to set profile low speed setting\n"); + } + + //then, set the high speed profile on the device + if (pSvc) { + pChr = pSvc->getCharacteristic(profileSettingHighUUID); + if (pChr) { + //if (pChr->canRead()) { + //Serial.printf("%s Value: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + if (pChr->canWrite()) { + if (pChr->writeValue(profile_bytes[1][1])) { + Serial.printf("Wrote new value to: %s\n", pChr->getUUID().toString().c_str()); + } else { + _pClient->disconnect(); + _disconnectDevice(); + return false; + } + //if (pChr->canRead()) { + //Serial.printf("The value of: %s is now: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + } + } + } else { + Serial.printf("unable to set profile high speed setting\n"); + } + + //finally, connect to speed characteristic and register for notifications + if (pSvc) { + pChr = pSvc->getCharacteristic(speedCharacteristicUUID); + if (pChr) { + if (pChr->canNotify()) { + if (!pChr->subscribe(true, notifyCB)) { + _pClient->disconnect(); + _disconnectDevice(); + return false; + } + } + else{ + _disconnectDevice(); + return false; + } + } else { + _disconnectDevice(); + return false; + } + } + else { + _disconnectDevice(); + return false; + } + + //device is fully setup and connected + _setState(CONNECTED); + + return true; +} +bool ChronoBLE::changeGunType(int type) { + + if (_pClient == nullptr){return false;} + + if (!_pClient->isConnected()) { + return false; + } + + /** Now we can read/write/subscribe the characteristics of the services we are interested in */ + NimBLERemoteService* pSvc = nullptr; + NimBLERemoteCharacteristic* pChr = nullptr; + NimBLERemoteDescriptor* pDsc = nullptr; + + pSvc = _pClient->getService(serviceUUID); + if (pSvc) { + pChr = pSvc->getCharacteristic(profileSettingLowUUID); + if (pChr) { + if (pChr->canWrite()) { + if (pChr->writeValue(profile_bytes[0][type])) { + Serial.printf("Wrote new value to: %s\n", pChr->getUUID().toString().c_str()); + } else { + return false; + } + //if (pChr->canRead()) { + //Serial.printf("The value of: %s is now: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + } + } + } else { + Serial.printf("unable to set profile low speed setting\n"); + } + + //then, set the high speed profile on the device + if (pSvc) { + pChr = pSvc->getCharacteristic(profileSettingHighUUID); + if (pChr) { + //if (pChr->canRead()) { + //Serial.printf("%s Value: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + if (pChr->canWrite()) { + if (pChr->writeValue(profile_bytes[1][type])) { + Serial.printf("Wrote new value to: %s\n", pChr->getUUID().toString().c_str()); + } else { + return false; + } + //if (pChr->canRead()) { + //Serial.printf("The value of: %s is now: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + } + } + } else { + Serial.printf("unable to set profile high speed setting\n"); + } + + return true; +} +bool ChronoBLE::setSpeedProfile(uint8_t low, uint8_t high){ + //first set the low speed profile on the device + /*NimBLERemoteService* pSvc = nullptr; + NimBLERemoteCharacteristic* pChr = nullptr; + pSvc = pClient->getService(serviceUUID); + if (pSvc) { + pChr = pSvc->getCharacteristic(profileSettingLowUUID); + if (pChr) { + //if (pChr->canRead()) { + //Serial.printf("%s Value: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + if (pChr->canWrite()) { + if (pChr->writeValue(0x32)) { + Serial.printf("Wrote new value to: %s\n", pChr->getUUID().toString().c_str()); + } else { + pClient->disconnect(); + return false; + } + //if (pChr->canRead()) { + //Serial.printf("The value of: %s is now: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + } + } + } else { + Serial.printf("unable to set profile low speed setting\n"); + } + + //then, set the high speed profile on the device + if (pSvc) { + pChr = pSvc->getCharacteristic(profileSettingHighUUID); + if (pChr) { + //if (pChr->canRead()) { + //Serial.printf("%s Value: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + if (pChr->canWrite()) { + if (pChr->writeValue(0x17)) { + Serial.printf("Wrote new value to: %s\n", pChr->getUUID().toString().c_str()); + } else { + pClient->disconnect(); + return false; + } + //if (pChr->canRead()) { + //Serial.printf("The value of: %s is now: %s\n", pChr->getUUID().toString().c_str(), pChr->readValue().c_str()); + //} + } + } + } else { + Serial.printf("unable to set profile high speed setting\n"); + }*/ +} \ No newline at end of file diff --git a/ChronoBLE.h b/ChronoBLE.h new file mode 100644 index 0000000..9034f68 --- /dev/null +++ b/ChronoBLE.h @@ -0,0 +1,41 @@ +#ifndef ChronoBLE_H +#define ChronoBLE_H + +#include +#include +#include + +typedef void (*CallbackFunction)(); + +class ChronoBLE { + public: + enum ConnectionState { + IDLE, + SCANNING, + DEVICE_FOUND, + CONNECTING, + CONNECTED + }; + + // Constructor: Called when a ChronoBLE object is created + ChronoBLE(); + + // Public methods + ConnectionState currentState; + void init(); + void startScan(); + bool isIdle(); + bool isConnected(); + bool foundDevice(); + bool connectToDevice(); + static void setDevice(const NimBLEAdvertisedDevice* device); + static bool changeGunType(int type); + void setSpeedCallback(void (*pFunc)(int speed)); + bool setSpeedProfile(uint8_t low, uint8_t high); + + private: + void _setState(ConnectionState state); + void _disconnectDevice(); +}; + +#endif \ No newline at end of file diff --git a/ChronoDisplay.ino b/ChronoDisplay.ino new file mode 100644 index 0000000..6bc22cc --- /dev/null +++ b/ChronoDisplay.ino @@ -0,0 +1,179 @@ +#include "TFT_eSPI.h" +#include "ChronoBLE.h" +#include "ChronoReading.h" +#include +#include "Display.h" +#include "ChronoReadingManager.h" +#include "ChronoWebServer.h" +#include "FileManager.h" + +//testing mode variables +static bool testMode=false; +static long testModeLastAdd=0; + +//general variables +static char currentTime[64]; + + +//chronograph setup +static bool chronoStateConnected=false; +static ChronoBLE chronoBLE; +static ChronoWebServer webServer; + + + +void speedCallback(int fps){ + addReading(fps); +} +void addReading(int fps){ + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + + //add reading to manager + chronoReadings.addReading(fps); + + //update display + Display::updateEntireDisplay(); + + if (chronoReadings.getShotCount() % 10 == 0){ + // Get filesystem information + unsigned int totalBytes = LittleFS.totalBytes(); + unsigned int usedBytes = LittleFS.usedBytes(); + unsigned int freeBytes = totalBytes - usedBytes; + + Serial.printf("\t(%u) Available Disk / Memory: %u / %u\n", chronoReadings.getShotCount(), freeBytes, ESP.getFreeHeap()); + } + + +} +void toggleTestMode(){ + if (testMode){ + Serial.println("disabling test mode...."); + testMode=false; + } + else { + Serial.println("enabling test mode...."); + testMode=true; + } +} +void setup() +{ + Serial.begin(115200); + Serial.println("starting......"); + + //initialize the file manager + FileManager& files = FileManager::getInstance(); + files.init(); + + //initialize web portal + webServer.init(); + webServer.setTestModeCallback(toggleTestMode); + + //initialize display + Display::init(); + + + //initialize gun and projectile profile + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + chronoReadings.initializeProfiles(); + //Display::updateProfile(chronoReadings.getProfile()); + + + //initialize bluetooth + chronoBLE.init(); + 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 + + 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 + time_t now; + char strftime_buf[64]; + struct tm timeinfo; + + time(&now); + localtime_r(&now, &timeinfo); + strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo); +} +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(); + localtime_r(&now, &timeinfo); + strftime(strftime_buf, sizeof(strftime_buf), "%R", &timeinfo); + + if (strcmp(currentTime, strftime_buf) != 0) { + strcpy(currentTime, strftime_buf); + Display::updateTime(strftime_buf); + Display::refreshDisplay(); + } +} +void checkBLE(){ + if (chronoBLE.isIdle()){ + if (chronoStateConnected){ + chronoStateConnected=false; + Display::showChronographDisconnected(); + Display::refreshDisplay(); + } + chronoBLE.startScan(); + } + else if (chronoBLE.isConnected()){ + //update display + if (!chronoStateConnected){ + chronoStateConnected=true; + Display::showChronographConnected(); + Display::refreshDisplay(); + } + } + else if (chronoBLE.foundDevice()){ + chronoBLE.connectToDevice(); + } +} + +void loop() +{ + //process BLE connection + checkBLE(); + + //process web server + webServer.process(); + + //update time + updateTime(); + + ////////////////////////////////////////////////////// + //run testmode commands if in test mode + if (testMode){ + if (testModeLastAdd==0){Display::showChronographConnected();} + if (millis()>(testModeLastAdd+5000)){ + long randomNumber = random(890, 920); + addReading(randomNumber); + testModeLastAdd=millis(); + } + } + ////////////////////////////////////////////////////// + + //update display + Display::refreshDisplay(); + + delay(2); +} diff --git a/ChronoProfile.cpp b/ChronoProfile.cpp new file mode 100644 index 0000000..80b8e65 --- /dev/null +++ b/ChronoProfile.cpp @@ -0,0 +1,12 @@ +#include "ChronoProfile.h" + +// Constructor implementation +ChronoProfile::ChronoProfile() {} +ChronoProfile::ChronoProfile(GunProfile gunProfile, ProjectileProfile projectileProfile) { + _gunProfile=gunProfile; + _projectileProfile=projectileProfile; +} +GunProfile ChronoProfile::getGunProfile(){return _gunProfile;} +void ChronoProfile::setGunProfile(GunProfile profile){_gunProfile=profile;} +ProjectileProfile ChronoProfile::getProjectileProfile(){return _projectileProfile;} +void ChronoProfile::setProjectileProfile(ProjectileProfile profile){_projectileProfile=profile;} \ No newline at end of file diff --git a/ChronoProfile.h b/ChronoProfile.h new file mode 100644 index 0000000..802a8a8 --- /dev/null +++ b/ChronoProfile.h @@ -0,0 +1,27 @@ +#ifndef ChronoProfile_H +#define ChronoProfile_H + +#include +#include "ProjectileProfile.h" +#include "GunProfile.h" + +class ChronoProfile { + public: + + // Constructor: Called when a ChronoProfile object is created + ChronoProfile(); + ChronoProfile(GunProfile gunProfile, ProjectileProfile projectileProfile); + + // Public methods & variables + GunProfile getGunProfile(); + void setGunProfile(GunProfile profile); + ProjectileProfile getProjectileProfile(); + void setProjectileProfile(ProjectileProfile profile); + + private: + ProjectileProfile _projectileProfile; + GunProfile _gunProfile; + +}; + +#endif \ No newline at end of file diff --git a/ChronoReading.cpp b/ChronoReading.cpp new file mode 100644 index 0000000..8027211 --- /dev/null +++ b/ChronoReading.cpp @@ -0,0 +1,22 @@ +#include "ChronoReading.h" +#include "ChronoProfile.h" + + +// Constructor implementation +ChronoReading::ChronoReading(){} +ChronoReading::ChronoReading(int fps, ChronoProfile profile) { + _fps=fps; + _profile=profile; +} +ChronoProfile ChronoReading::getProfile(){return _profile;} +int ChronoReading::getFPS(){ + return _fps; +} +int ChronoReading::getFPE(){ + float weight=_profile.getProjectileProfile().getWeight(); + return (int)round(_calculateFPE(_fps, weight)); +} +float ChronoReading::_calculateFPE(float fps, float weight){ + //weight in grains + return (fps*fps*weight) / 450240; +} \ No newline at end of file diff --git a/ChronoReading.h b/ChronoReading.h new file mode 100644 index 0000000..de44e2e --- /dev/null +++ b/ChronoReading.h @@ -0,0 +1,26 @@ +#ifndef ChronoReading_H +#define ChronoReading_H + +#include +#include "ChronoProfile.h" + +class ChronoReading { + public: + + // Constructor: Called when a ChronoReading object is created + ChronoReading(); + ChronoReading(int fps, ChronoProfile profile); + + // Public methods + int getFPS(); + int getFPE(); + ChronoProfile getProfile(); + + private: + float _calculateFPE(float fps, float weight); + int _fps; + ChronoProfile _profile; + +}; + +#endif \ No newline at end of file diff --git a/ChronoReadingManager.cpp b/ChronoReadingManager.cpp new file mode 100644 index 0000000..32cadba --- /dev/null +++ b/ChronoReadingManager.cpp @@ -0,0 +1,173 @@ +#include "ChronoReadingManager.h" +#include "FileManager.h" + +// Constructor implementation +ChronoReadingManager::ChronoReadingManager() {} +void ChronoReadingManager::initializeProfiles(){ + //load gun profiles from disk + FileManager& files = FileManager::getInstance(); + //files.formatDisk(); + files.loadGunProfiles(_gunProfiles); + files.loadProjectileProfiles(_projectileProfiles); + + //defaults + GunProfile gp("", GunProfile::PROFILE_TYPE_AIR_GUN_FAC); + ProjectileProfile pp("", ProjectileProfile::PELLET, 0.0, 0.0); + + //set current profile + /*if (_gunProfiles.size()>0){ + gp=_gunProfiles.get(0); + } + if (_projectileProfiles.size()>0){ + pp=_projectileProfiles.get(0); + }*/ + _currentProfile=new ChronoProfile(gp,pp); +} +ChronoReadingManager& ChronoReadingManager::getInstance(){ + static ChronoReadingManager instance; + return instance; +} +LinkedList& ChronoReadingManager::getGunProfiles(){return _gunProfiles;} +void ChronoReadingManager::setGunProfile(int id){ + GunProfile p=_gunProfiles.get(id); + _currentProfile->setGunProfile(p); +} +void ChronoReadingManager::addGunProfile(GunProfile profile, bool writeToDisk){ + if (writeToDisk){ + FileManager& files = FileManager::getInstance(); + files.saveGunProfile(profile); + } + _gunProfiles.add(profile); +} +void ChronoReadingManager::removeGunProfile(int id){ + //remove from disk + GunProfile profile=_gunProfiles.get(id); + FileManager& files = FileManager::getInstance(); + files.removeGunProfile(profile); + + //now remove from local list + for (int i=0; i<_gunProfiles.size();i++){ + GunProfile gp=_gunProfiles.get(i); + if (gp.getName()==profile.getName() && + gp.getPowerLevel()==profile.getPowerLevel()){ + _gunProfiles.remove(i); + break; + } + } +} +LinkedList& ChronoReadingManager::getProjectileProfiles(){return _projectileProfiles;} +void ChronoReadingManager::setProjectileProfile(int id){ + ProjectileProfile pp=_projectileProfiles.get(id); + _currentProfile->setProjectileProfile(pp); +} +void ChronoReadingManager::addProjectileProfile(ProjectileProfile profile, bool writeToDisk){ + if (writeToDisk){ + FileManager& files = FileManager::getInstance(); + files.saveProjectileProfile(profile); + } + _projectileProfiles.add(profile); +} +void ChronoReadingManager::removeProjectileProfile(int id){ + //remove from disk + ProjectileProfile profile=_projectileProfiles.get(id); + FileManager& files = FileManager::getInstance(); + files.removeProjectileProfile(profile); + + //now remove from local list + for (int i=0; i<_projectileProfiles.size();i++){ + ProjectileProfile p=_projectileProfiles.get(i); + if (p.getName()==profile.getName() && + p.getType()==profile.getType() && + p.getCaliber()==profile.getCaliber() && + p.getWeight()==profile.getWeight()){ + _projectileProfiles.remove(i); + break; + } + } +} +int ChronoReadingManager::getShotCount(){return _chronoReadings.size();} +int ChronoReadingManager::getLastFPS(){ + if (_chronoReadings.size()>0){ + return _chronoReadings.get(_chronoReadings.size()-1).getFPS(); + } + else{ + return 0; + } +} +int ChronoReadingManager::getLastFPE(){ + if (_chronoReadings.size()>0){ + return _chronoReadings.get(_chronoReadings.size()-1).getFPE(); + } + else{ + return 0; + } +} +ChronoProfile& ChronoReadingManager::getProfile(){return *_currentProfile;} +LinkedList& ChronoReadingManager::getChronographReadings(){return _chronoReadings;} +void ChronoReadingManager::addReading(int fps){ + ChronoReading reading(fps,*_currentProfile); + _chronoReadings.add(reading); +} +int ChronoReadingManager::getLowFPS() { + int low=0; + for(int i=0;i<_chronoReadings.size();i++){ + if (i==0 || _chronoReadings.get(i).getFPS()high){ + high=_chronoReadings.get(i).getFPS(); + } + } + return high; +} +int ChronoReadingManager::getAvgFPS(){ + int avg=0; + if (_chronoReadings.size()>0){ + int total=0; + for (int i=0;i<_chronoReadings.size();i++){ + total=total+_chronoReadings.get(i).getFPS(); + } + avg=total/_chronoReadings.size(); + } + return avg; +} +float ChronoReadingManager::getStandardDeviation(){ + float sd=0.0; + float total=0.0; + float avg=0.0; + + if (_chronoReadings.size()==0){ + return 0; + } + + //get the average + for (int i = 0; i < _chronoReadings.size(); i++) { // Loop through the array elements + total=total+_chronoReadings.get(i).getFPS(); + } + avg=total/_chronoReadings.size(); + + //get the sum of squared differences + float squaredTotal=0.0; + for (int i = 0; i < _chronoReadings.size(); i++) { // Loop through the array elements + float diff=_chronoReadings.get(i).getFPS()-avg; + squaredTotal=squaredTotal+(diff*diff); + } + float squaredAvg=squaredTotal/_chronoReadings.size(); + sd=sqrt(squaredAvg); + + return sd; +} +int ChronoReadingManager::getSpread(){ + int low=getLowFPS(); + int high=getHighFPS(); + return high-low; +} +void ChronoReadingManager::reset(){ + _chronoReadings.clear(); +} \ No newline at end of file diff --git a/ChronoReadingManager.h b/ChronoReadingManager.h new file mode 100644 index 0000000..2019845 --- /dev/null +++ b/ChronoReadingManager.h @@ -0,0 +1,49 @@ +#ifndef ChronoReadingManager_H +#define ChronoReadingManager_H + +#include +#include "ChronoReading.h" +#include "ChronoProfile.h" +#include "GunProfile.h" +#include "ProjectileProfile.h" +#include + +class ChronoReadingManager { + public: + + // Constructor: Called when a ChronoReadingManager object is created + static ChronoReadingManager& getInstance(); + void initializeProfiles(); + void addReading(int fps); + int getLowFPS(); + int getHighFPS(); + int getAvgFPS(); + float getStandardDeviation(); + int getShotCount(); + int getSpread(); + int getLastFPS(); + int getLastFPE(); + LinkedList& getChronographReadings(); + ChronoProfile& getProfile(); + void addProjectileProfile(ProjectileProfile profile, bool writeToDisk = true); + LinkedList& getProjectileProfiles(); + void setProjectileProfile(int id); + void removeProjectileProfile(int id); + void addGunProfile(GunProfile profile, bool writeToDisk = true); + LinkedList& getGunProfiles(); + void setGunProfile(int id); + void removeGunProfile(int id); + void reset(); + + private: + ChronoReadingManager(); + ChronoReadingManager(const ChronoReadingManager&) = delete; + ChronoReadingManager& operator=(const ChronoReadingManager&) = delete; + LinkedList _chronoReadings; + LinkedList _gunProfiles; + LinkedList _projectileProfiles; + ChronoProfile* _currentProfile; + +}; + +#endif \ No newline at end of file diff --git a/ChronoWebServer.cpp b/ChronoWebServer.cpp new file mode 100644 index 0000000..b1aa0bf --- /dev/null +++ b/ChronoWebServer.cpp @@ -0,0 +1,471 @@ +#include "ChronoWebServer.h" +#include +#include +#include +#include +#include +#include +#include "Display.h" + +const char *ssid = "routeguy"; +const char *password = "wrangleR621!"; +WebServer _server(80); +void (*testModeCallback)(); + +// Constructor implementation +ChronoWebServer::ChronoWebServer() {} +void ChronoWebServer::_handleRoot() { + String temp="\ + \ + FX Chronograph Display Settings\ + \ + \ + \ + \ +

ChronoDisplay Settings

\ +
\ +

Control Panel

\ + \ + \ + \ + \ +
\ +
\ +

Gun Profiles

\ + \ + "; + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + + String currentGunName=chronoReadings.getProfile().getGunProfile().getName(); + int currentGunPowerLevel=chronoReadings.getProfile().getGunProfile().getPowerLevel(); + LinkedList& gprofiles= chronoReadings.getGunProfiles(); + for(int i=0;i"; + } + else{ + temp +=""; + } + temp +=""; + } + +temp +="
NameType
"+name+""+type+"
\ + \ +
\ +
\ +

Projectile Profiles

\ + \ + "; + String currentProjectileProfileName=chronoReadings.getProfile().getProjectileProfile().getName(); + float currentProjectileProfileWeight=chronoReadings.getProfile().getProjectileProfile().getWeight(); + LinkedList& profiles= chronoReadings.getProjectileProfiles(); + for(int i=0;i"; + } + else{ + temp +=""; + } + temp +=""; + } + + + temp +="
NameTypeCaliberWeight
"+name+""+type+""+cal+""+weight+"
\ + \ +
\ + \ +"; + _server.send(200, "text/html", temp); +} +void ChronoWebServer::_handleGunProfile(){ + String temp="\ + \ + Gun Profile\ + \ + \ + \ + \ +

Add Gun Profile

\ +
\ +

Name

\ + \ +

Type

\ + \ +
\ +
\ + \ + "; + _server.send(200, "text/html", temp); +} +void ChronoWebServer::_handleAddGunProfile(){ + if (_server.method() == HTTP_POST){ + String name="Default"; + GunProfile::GunProfileType type=GunProfile::PROFILE_TYPE_AIR_GUN_FAC; + float caliber=0.0; + float weight=0.0; + if (_server.hasArg("name")){name=_server.arg("name");} + if (_server.hasArg("type")){ + String st=_server.arg("type"); + if (strcmp(st.c_str(),"0")==0){type=GunProfile::PROFILE_TYPE_BOW_AIRSOFT;} + else if (strcmp(st.c_str(),"1")==0){type=GunProfile::PROFILE_TYPE_CO2_PISTOL;} + else if (strcmp(st.c_str(),"2")==0){type=GunProfile::PROFILE_TYPE_AIR_PISTOL;} + else if (strcmp(st.c_str(),"3")==0){type=GunProfile::PROFILE_TYPE_AIR_GUN_UK;} + else if (strcmp(st.c_str(),"4")==0){type=GunProfile::PROFILE_TYPE_AIR_GUN_FAC;} + } + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + GunProfile p(name,type); + chronoReadings.addGunProfile(p); + } + + _server.send(200, "text/html", "ok"); +} +void ChronoWebServer::_handleSetGunProfile(){ + if (_server.method() == HTTP_POST){ + if (_server.hasArg("id")){ + int id=_server.arg("id").toInt(); + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + chronoReadings.setGunProfile(id); + + //update profile with chronograph + int type=chronoReadings.getProfile().getGunProfile().getPowerLevel(); + ChronoBLE::changeGunType(type); + + //update display + Display::updateProfile(chronoReadings.getProfile()); + Display::refreshDisplay(); + } + } + _server.send(200, "text/html", "ok"); +} +void ChronoWebServer::_handleRemoveGunProfile(){ + if (_server.method() == HTTP_POST){ + if (_server.hasArg("id")){ + int id=_server.arg("id").toInt(); + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + chronoReadings.removeGunProfile(id); + } + } + _server.send(200, "text/html", "ok"); +} +void ChronoWebServer::_handleProjectileProfile(){ + String temp="\ + \ + Projectile Profile\ + \ + \ + \ + \ +

Add Projectile Profile

\ +
\ +

Name

\ + \ +

Type

\ + \ +

Caliber

\ + \ +

Weight

\ + \ +
\ +
\ + \ + "; + _server.send(200, "text/html", temp); +} +void ChronoWebServer::_handleSetProjectileProfile(){ + if (_server.method() == HTTP_POST){ + if (_server.hasArg("id")){ + int id=_server.arg("id").toInt(); + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + chronoReadings.setProjectileProfile(id); + Display::updateProfile(chronoReadings.getProfile()); + Display::refreshDisplay(); + } + } + _server.send(200, "text/html", "ok"); +} +void ChronoWebServer::_handleAddProjectileProfile(){ + String message = "Got request...\n\n"; + message += "URI: "; + message += _server.uri(); + message += "\nMethod: "; + message += (_server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += _server.args(); + message += "\n"; + + for (uint8_t i = 0; i < _server.args(); i++) { + message += " " + _server.argName(i) + ": " + _server.arg(i) + "\n"; + } + Serial.println(message); + + if (_server.method() == HTTP_POST){ + String name=""; + ProjectileProfile::ProjectileType type=ProjectileProfile::PELLET; + float caliber=0.0; + float weight=0.0; + if (_server.hasArg("name")){name=_server.arg("name");} + if (_server.hasArg("type")){ + String st=_server.arg("type"); + if (strcmp(st.c_str(),"slug")==0){ + type=ProjectileProfile::SLUG; + } + } + if (_server.hasArg("cal")){caliber=_server.arg("cal").toFloat();} + if (_server.hasArg("weight")){weight=_server.arg("weight").toFloat();} + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + ProjectileProfile pp(name,type,caliber,weight); + chronoReadings.addProjectileProfile(pp); + } + + _server.send(200, "text/html", "ok"); +} +void ChronoWebServer::_handleRemoveProjectileProfile(){ + if (_server.method() == HTTP_POST){ + if (_server.hasArg("id")){ + int id=_server.arg("id").toInt(); + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + chronoReadings.removeProjectileProfile(id); + } + } + _server.send(200, "text/html", "ok"); +} +void ChronoWebServer::_handleExport(){ + + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + LinkedList& readings=chronoReadings.getChronographReadings(); + int count=readings.size(); + Serial.print("exporting "); + Serial.print(count); + Serial.println(" readings to csv..."); + + //return csv data + String csvData = "Shot #,FPS,FPE,Gun,Power,Projectile,Type,Caliber,Weight\n"; + for (int i=0; i +#include +#include +#include +#include +#include +#include + +class ChronoWebServer { + public: + + // Constructor: Called when a ChronoWebServer object is created + ChronoWebServer(); + + void init(); + void process(); + void setTestModeCallback(void (*pFunc)()); + + private: + static void _handleRoot(); + static void _handleGunProfile(); + static void _handleProjectileProfile(); + static void _handleAddProjectileProfile(); + static void _handleSetProjectileProfile(); + static void _handleRemoveProjectileProfile(); + static void _handleAddGunProfile(); + static void _handleSetGunProfile(); + static void _handleRemoveGunProfile(); + static void _handleExport(); + static void _handleReset(); + static void _handleFormat(); + static void _handleTestMode(); + +}; + +#endif \ No newline at end of file diff --git a/Display.cpp b/Display.cpp new file mode 100644 index 0000000..b475ae4 --- /dev/null +++ b/Display.cpp @@ -0,0 +1,278 @@ +#include "Display.h" + +// Constructor implementation +EPaper Display::_epaper; +Display::Display() {} +void Display::init(){ + _epaper.begin(); + _epaper.fillScreen(TFT_WHITE); + _epaper.setTextColor(TFT_BLACK, TFT_WHITE); // Adding a background colour erases previous text automatically + + _drawScreenLayout(); + refreshDisplay(); +} +void Display::refreshDisplay(){ + _epaper.update(); + //_epaper.sleep(); +} +void Display::_drawScreenLayout(){ + _drawFPS(); + _drawFPE(); + showChronographDisconnected(); + _drawShotCount(); + _drawAverageFPS(); + _drawLowFPS(); + _drawHighFPS(); + _drawSpread(); + _drawStandardDeviation(); + _drawProfile(); + _drawLastShots(); +} +void Display::_drawFPE(){ + _epaper.setTextSize(2); + _epaper.drawString("FPE: ",20,240); +} +void Display::updateFPE(int fpe){ + _epaper.fillRect(65,230,100,35,TFT_WHITE); + char buffer[10]; + itoa(fpe, buffer, 10); + _epaper.setTextSize(2); + _epaper.drawString(buffer,70,240); +} +void Display::_drawFPS(){ + _epaper.drawRoundRect(10, 10, 300, 260, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("Latest Shot:", 15, 15); +} +void Display::updateFPS(int fps){ + if (fps>0){ + _epaper.fillRoundRect(20, 70, 280, 110, 15, TFT_WHITE); + _epaper.setTextSize(20); + char floatCharArray[10]; + dtostrf(fps, 3, 0, floatCharArray); + _epaper.drawString(floatCharArray, 100, 100); + _epaper.setTextSize(2); + _epaper.drawString("fps", 145, 155); + } + else{ + _epaper.fillRoundRect(20, 70, 280, 110, 15, TFT_WHITE); + _epaper.setTextSize(20); + _epaper.drawString(" -", 100, 100); + _epaper.setTextSize(2); + _epaper.drawString("fps", 145, 155); + } +} +void Display::_drawAverageFPS(){ + _epaper.drawRoundRect(450, 10, 120, 80, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("Avg FPS:",465,15); + _epaper.setTextSize(4); + _epaper.drawString("-",475,45); +} +void Display::updateAverageFPS(int fps){ + _epaper.fillRoundRect(460, 35, 100, 45, 10, TFT_WHITE); + _epaper.setTextSize(4); + if (fps>0){ + char buffer[10]; + itoa(fps, buffer, 10); + _epaper.drawString(buffer,475,45); + } + else{ + _epaper.drawString("-",475,45); + } +} +void Display::_drawLowFPS(){ + _epaper.drawRoundRect(450, 100, 120, 80, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("Low FPS:",465,105); + _epaper.setTextSize(4); + _epaper.drawString("-",475,135); +} +void Display::updateLowFPS(int fps){ + _epaper.fillRoundRect(460, 125, 100, 45, 10, TFT_WHITE); + _epaper.setTextSize(4); + if (fps>0){ + char buffer[10]; + itoa(fps, buffer, 10); + _epaper.drawString(buffer,475,135); + } + else{ + _epaper.drawString("-",475,135); + } +} +void Display::_drawHighFPS(){ + _epaper.drawRoundRect(450, 190, 120, 80, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("High FPS:",460,195); + _epaper.setTextSize(4); + _epaper.drawString("-",475,225); +} +void Display::updateHighFPS(int fps){ + _epaper.fillRoundRect(460, 215, 100, 45, 10, TFT_WHITE); + _epaper.setTextSize(2); + _epaper.drawString("High FPS:",460,195); + _epaper.setTextSize(4); + if (fps>0){ + char buffer[10]; + itoa(fps, buffer, 10); + _epaper.drawString(buffer,475,225); + } + else{ + _epaper.drawString("-",475,225); + } +} +void Display::_drawProfile(){ + _epaper.drawRoundRect(580, 40, 210, 230, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("Profile:", 585, 45); +} +void Display::updateProfile(ChronoProfile& profile){ + String gunName=profile.getGunProfile().getName(); + int powerLevel=profile.getGunProfile().getPowerLevel() + 1; + _epaper.fillRect(590, 70, 280, 120, TFT_WHITE); + _epaper.drawRoundRect(580, 40, 210, 230, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString(gunName, 595, 75); + _epaper.drawCircle(770, 56, 12, TFT_BLACK); + char buffer[10]; + itoa(powerLevel, buffer, 10); + _epaper.drawString(buffer, 766, 49); + + String projectileName=profile.getProjectileProfile().getName(); + String projectileType=profile.getProjectileProfile().getType(); + float projectileWeight=profile.getProjectileProfile().getWeight(); + float projectileCaliber=profile.getProjectileProfile().getCaliber(); + _epaper.drawString(projectileName, 595, 105); + char buffer3[10]; + dtostrf(projectileCaliber, 0, 2, buffer3); + char buffer4[10]; + strncpy(buffer4, buffer3 + 1, 4); + _epaper.drawString(buffer4, 595, 130); + _epaper.drawString(projectileType, 595, 155); + char buffer2[10]; + dtostrf(projectileWeight, 1, 2, buffer2); + char out[20]; + strcpy(out,buffer2); + strcat(out,"g"); + _epaper.drawString(out, 595, 180); +} +void Display::_drawShotCount(){ + _epaper.drawRoundRect(320, 10, 120, 80, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("Count:",325,15); + _epaper.setTextSize(4); + _epaper.drawString("-",335,45); +} +void Display::updateShotCount(int count){ + _epaper.fillRoundRect(330, 35, 100, 45, 10, TFT_WHITE); + char buffer[10]; + itoa(count, buffer, 10); + _epaper.setTextSize(4); + _epaper.drawString(buffer,335,45); +} +void Display::_drawSpread(){ + _epaper.drawRoundRect(320, 100, 120, 80, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("Spread:",325,105); + _epaper.setTextSize(4); + _epaper.drawString("-",335,135); +} +void Display::updateSpread(int spread){ + _epaper.fillRoundRect(330, 125, 100, 45, 10, TFT_WHITE); + _epaper.setTextSize(2); + _epaper.drawString("Spread:",325,105); + _epaper.setTextSize(4); + if (spread>=0){ + char buffer[10]; + itoa(spread, buffer, 10); + _epaper.drawString(buffer,335,135); + } + else{ + _epaper.drawString("-",335,135); + } +} +void Display::_drawStandardDeviation(){ + _epaper.drawRoundRect(320, 190, 120, 80, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("SD:",325,195); + _epaper.setTextSize(4); + _epaper.drawString("-",335,225); +} +void Display::updateStandardDeviation(float sd){ + _epaper.fillRoundRect(330, 215, 100, 45, 10, TFT_WHITE); + if (sd==0){ + _epaper.setTextSize(4); + _epaper.drawString("-",335,225); + } + else{ + _epaper.setTextSize(3); + char buffer[10]; + dtostrf(sd, 1, 2, buffer); + _epaper.drawString(buffer,335,225); + } +} +void Display::_drawLastShots(){ + _epaper.drawRoundRect(10, 280, 780, 190, 10, TFT_BLACK); + _epaper.setTextSize(2); + _epaper.drawString("Last Shots:",15,285); +} +void Display::updateLastShots(LinkedList &chronoReadings){ + _epaper.fillRoundRect(20, 305, 760, 155, 10, TFT_WHITE); + const int max=32; + _epaper.setTextSize(2); + int chronographReadingCount=chronoReadings.size(); + if (chronographReadingCount>0){ + int x=25; + int y=310; + for (int i=0;i=max){break;} + int c=chronographReadingCount-i; + int fps=chronoReadings.get(chronographReadingCount-i-1).getFPS(); + int fpe=chronoReadings.get(chronographReadingCount-i-1).getFPE(); + char charArray[50]; + sprintf(charArray, "%d) %d / %d", c, fps, fpe); + if (i%8 == 0 && i>0){x=x+190; y=310;} + _epaper.drawString(charArray, x, y); + y=y+20; + } + } +} +void Display::showChronographConnected(){ + _epaper.fillRoundRect(20, 70, 280, 110, 15, TFT_WHITE); + //_epaper.update(); + _epaper.setTextSize(20); + _epaper.drawString(" -", 100, 100); + _epaper.setTextSize(2); + _epaper.drawString("fps", 145, 155); +} +void Display::showChronographDisconnected(){ + _epaper.setTextSize(3); + _epaper.fillRoundRect(20, 70, 280, 110, 15, TFT_BLACK); + //_epaper.update(); + _epaper.setTextColor(TFT_WHITE, TFT_BLACK); + _epaper.drawString("Searching", 80, 80); + _epaper.drawString("for", 135, 110); + _epaper.drawString("chronograph...", 35, 140); + _epaper.setTextColor(TFT_BLACK, TFT_WHITE); +} +void Display::updateTime(String time){ + _epaper.drawRect(698, 8, 102, 35, TFT_WHITE); //reset to get rid of ghosting + _epaper.update(); + _epaper.setTextSize(3); + _epaper.drawString(time,700,10); +} +void Display::updateEntireDisplay(){ + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + + //update display + updateFPS(chronoReadings.getLastFPS()); + updateFPE(chronoReadings.getLastFPE()); + updateShotCount(chronoReadings.getShotCount()); + updateAverageFPS(chronoReadings.getAvgFPS()); + updateLowFPS(chronoReadings.getLowFPS()); + updateHighFPS(chronoReadings.getHighFPS()); + updateSpread(chronoReadings.getSpread()); + updateStandardDeviation(chronoReadings.getStandardDeviation()); + LinkedList &readings=chronoReadings.getChronographReadings(); + updateLastShots(readings); +} \ No newline at end of file diff --git a/Display.h b/Display.h new file mode 100644 index 0000000..265241c --- /dev/null +++ b/Display.h @@ -0,0 +1,49 @@ +#ifndef Display_H +#define Display_H + +#include +#include +#include +#include +#include "TFT_eSPI.h" + +class Display { + public: + + // Constructor: Called when a Display object is created + Display(); + + static void init(); + static void updateShotCount(int count); + static void updateFPE(int fpe); + static void updateFPS(int fps); + static void updateAverageFPS(int fps); + static void updateLowFPS(int fps); + static void updateHighFPS(int fps); + static void updateProfile(ChronoProfile& profile); + static void updateLastShots(LinkedList &chronoReadings); + static void updateSpread(int spread); + static void updateStandardDeviation(float sd); + static void updateTime(String time); + static void showChronographConnected(); + static void showChronographDisconnected(); + static void refreshDisplay(); + static void updateEntireDisplay(); + + private: + static EPaper _epaper; + static void _drawScreenLayout(); + static void _drawShotCount(); + static void _drawFPE(); + static void _drawFPS(); + static void _drawAverageFPS(); + static void _drawLowFPS(); + static void _drawHighFPS(); + static void _drawProfile(); + static void _drawLastShots(); + static void _drawSpread(); + static void _drawStandardDeviation(); + +}; + +#endif \ No newline at end of file diff --git a/FileManager.cpp b/FileManager.cpp new file mode 100644 index 0000000..1118022 --- /dev/null +++ b/FileManager.cpp @@ -0,0 +1,247 @@ +#include "FileManager.h" +#include "ChronoReadingManager.h" + +const char* _gunProfileFolder="/gunprofile/"; +const char* _projectileProfileFolder="/projectileprofile/"; +const int maxProfileCount=128; +FileManager::FileManager() { +} +FileManager& FileManager::getInstance(){ + static FileManager instance; + return instance; +} +void FileManager::init(){ + if (!LittleFS.begin(true)) { // true to format if not formatted + Serial.println("LittleFS Mount Failed"); + return; + } +} +void FileManager::saveGunProfile(GunProfile& profile){ + String filename=_gunProfileFolder; + filename += String(millis()); + Serial.print("filename: "); + Serial.println(filename); + File file = LittleFS.open(filename, FILE_WRITE); + if (!file) { + Serial.println("Failed to open gun profile file for writing"); + return; + } + + // Write the object's data directly as bytes + file.write((uint8_t*)&profile, sizeof(GunProfile)); + file.close(); + Serial.println("GunProfile Object written to file successfully."); +} +void FileManager::removeGunProfile(GunProfile& profile){ + File directory = LittleFS.open(_gunProfileFolder, FILE_READ); + if (!directory){ + Serial.println("failed to open gun profile folder...."); + if (!directory.isDirectory()) { + return; + } + } + if (!directory.isDirectory()) { + Serial.println(" - not a directory, error"); + return; + } + directory.rewindDirectory(); + + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + Serial.println("parsing gun profile directory...."); + File file = directory.openNextFile(); + String fs=""; + bool found=false; + GunProfile gunProfile; + while (file) { + fs=_gunProfileFolder; + fs += file.name(); + + file.read((byte *)&gunProfile, sizeof(gunProfile)); + file.close(); + + if (gunProfile.getName()==profile.getName() && + gunProfile.getPowerLevel()==profile.getPowerLevel()){ + Serial.print("removing: "); + Serial.println(fs); + LittleFS.remove(fs); + break; + } + + + file = directory.openNextFile(); + if (file.size()==0){break;} + } + directory.close(); + Serial.println("done removing gun profile!"); +} +void FileManager::removeAllGunProfiles(){ + File directory = LittleFS.open(_gunProfileFolder, FILE_READ); + if (!directory){ + Serial.println("failed to open gun profile folder...."); + if (!directory.isDirectory()) { + return; + } + } + if (!directory.isDirectory()) { + Serial.println(" - not a directory, error"); + return; + } + directory.rewindDirectory(); + + Serial.println("parsing gun profile directory...."); + File file = directory.openNextFile(); + const char* fs; + while (file) { + fs=file.path(); + file.close(); + file = directory.openNextFile(); + Serial.print("removing: "); + Serial.print(fs); + bool rm=LittleFS.remove(fs); + Serial.print(" ---> "); + Serial.println(rm); + } + directory.close(); + Serial.println("done removing all gun profiles!"); +} +void FileManager::loadGunProfiles(LinkedList& profiles){ + //File file = LittleFS.open(filename, FILE_READ); + File directory = LittleFS.open(_gunProfileFolder, FILE_READ); + if (!directory){ + Serial.println("failed to open gun profile folder...."); + if (!directory.isDirectory()) { + Serial.println(" - not a directory, generating directory for first time"); + LittleFS.mkdir(_gunProfileFolder); + return; + } + } + + if (!directory.isDirectory()) { + Serial.println(" - not a directory, error"); + return; + } + directory.rewindDirectory(); + + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + Serial.println("parsing gun profile directory...."); + File file = directory.openNextFile(); + GunProfile gunProfile; + while (file) { + String fs=_gunProfileFolder; + fs += file.name(); + file.read((byte *)&gunProfile, sizeof(gunProfile)); + chronoReadings.addGunProfile(gunProfile, false); + Serial.print("added gun profile: "); + Serial.println(file.name()); + + Serial.print("Free Heap Memory: "); + Serial.print(ESP.getFreeHeap()); + Serial.println(" bytes"); + + file.close(); + file = directory.openNextFile(); + if (file.size()==0){break;} + } + + directory.close(); + Serial.println("done loading gun profiles!"); +} +void FileManager::saveProjectileProfile(ProjectileProfile& profile){ + String filename=_projectileProfileFolder; + filename += String(millis()); + File file = LittleFS.open(filename, FILE_WRITE); + if (!file) { + Serial.println("Failed to open projectile profile file for writing"); + return; + } + + // Write the object's data directly as bytes + file.write((uint8_t*)&profile, sizeof(ProjectileProfile)); + file.close(); + Serial.println("ProjectileProfile Object written to file successfully."); +} +void FileManager::removeProjectileProfile(ProjectileProfile& profile){ + File directory = LittleFS.open(_projectileProfileFolder, FILE_READ); + if (!directory){ + Serial.println("failed to open projectile profile folder...."); + if (!directory.isDirectory()) { + return; + } + } + if (!directory.isDirectory()) { + Serial.println(" - not a directory, error"); + return; + } + directory.rewindDirectory(); + + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + Serial.println("parsing projectile profile directory...."); + File file = directory.openNextFile(); + String fs=""; + bool found=false; + while (file) { + fs=_projectileProfileFolder; + fs += file.name(); + + ProjectileProfile projectileProfile; + file.read((byte *)&projectileProfile, sizeof(projectileProfile)); + file.close(); + + if (projectileProfile.getName()==profile.getName() && + projectileProfile.getCaliber()==profile.getCaliber() && + projectileProfile.getWeight()==profile.getWeight() && + projectileProfile.getType()==profile.getType()){ + Serial.print("removing: "); + Serial.println(fs); + LittleFS.remove(fs); + break; + } + + + file = directory.openNextFile(); + if (file.size()==0){break;} + } + directory.close(); + Serial.println("done removing projectile profile!"); +} +void FileManager::loadProjectileProfiles(LinkedList& profiles){ + //File file = LittleFS.open(filename, FILE_READ); + File directory = LittleFS.open(_projectileProfileFolder, FILE_READ); + if (!directory){ + Serial.println("failed to open projectile profile folder...."); + if (!directory.isDirectory()) { + Serial.println(" - not a directory, generating directory for first time"); + LittleFS.mkdir(_projectileProfileFolder); + return; + } + } + + if (!directory.isDirectory()) { + Serial.println(" - not a directory, error"); + return; + } + directory.rewindDirectory(); + + ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance(); + Serial.println("parsing projectile profile directory...."); + File file = directory.openNextFile(); + while (file) { + String fs=_projectileProfileFolder; + fs += file.name(); + ProjectileProfile projectileProfile; + file.read((byte *)&projectileProfile, sizeof(projectileProfile)); + chronoReadings.addProjectileProfile(projectileProfile, false); + Serial.print("added projectile profile: "); + Serial.println(file.name()); + + file.close(); + file = directory.openNextFile(); + if (file.size()==0){break;} + } + + directory.close(); + Serial.println("done loading projectile profiles!"); +} +void FileManager::formatDisk(){ + LittleFS.format(); +} \ No newline at end of file diff --git a/FileManager.h b/FileManager.h new file mode 100644 index 0000000..0a490a2 --- /dev/null +++ b/FileManager.h @@ -0,0 +1,33 @@ +#ifndef FileManager_H +#define FileManager_H + +#include +#include "FS.h" +#include +#include +#include +#include + +class FileManager { + public: + static FileManager& getInstance(); + + void init(); + void formatDisk(); + void saveGunProfile(GunProfile& profile); + void removeGunProfile(GunProfile& profile); + void removeAllGunProfiles(); + void loadGunProfiles(LinkedList& profiles); + void saveProjectileProfile(ProjectileProfile& profile); + void removeProjectileProfile(ProjectileProfile& profile); + void loadProjectileProfiles(LinkedList& profiles); + + + private: + FileManager(); + FileManager(const FileManager&) = delete; + FileManager& operator=(const FileManager&) = delete; + +}; + +#endif \ No newline at end of file diff --git a/GunProfile.cpp b/GunProfile.cpp new file mode 100644 index 0000000..54221af --- /dev/null +++ b/GunProfile.cpp @@ -0,0 +1,10 @@ +#include "GunProfile.h" + +// Constructor implementation +GunProfile::GunProfile() {} +GunProfile::GunProfile(String name, int type) { + strcpy(_name, name.c_str()); + _type=type; +} +String GunProfile::getName(){return String(_name);} +int GunProfile::getPowerLevel(){return _type;} \ No newline at end of file diff --git a/GunProfile.h b/GunProfile.h new file mode 100644 index 0000000..86b3f5c --- /dev/null +++ b/GunProfile.h @@ -0,0 +1,30 @@ +#ifndef GunProfile_H +#define GunProfile_H + +#include + +class GunProfile { + public: + + enum GunProfileType{ + PROFILE_TYPE_BOW_AIRSOFT, + PROFILE_TYPE_CO2_PISTOL, + PROFILE_TYPE_AIR_PISTOL, + PROFILE_TYPE_AIR_GUN_UK, + PROFILE_TYPE_AIR_GUN_FAC + }; + + // Constructor: Called when a GunProfile object is created + GunProfile(); + GunProfile(String name, int type); + String getName(); + int getPowerLevel(); + + private: + char _name[50]; + int _type; + + +}; + +#endif \ No newline at end of file diff --git a/ProjectileProfile.cpp b/ProjectileProfile.cpp new file mode 100644 index 0000000..66e5e15 --- /dev/null +++ b/ProjectileProfile.cpp @@ -0,0 +1,14 @@ +#include "ProjectileProfile.h" + +// Constructor implementation +ProjectileProfile::ProjectileProfile() {} +ProjectileProfile::ProjectileProfile(String name, ProjectileType type, float caliber, float weight) { + strcpy(_name, name.c_str()); + _type=type; + _caliber=caliber; + _weight=weight; +} +String ProjectileProfile::getName(){return String(_name);} +float ProjectileProfile::getWeight(){return _weight;} +String ProjectileProfile::getType(){if (_type==PELLET){return "Pellet";}else{return "Slug";}} +float ProjectileProfile::getCaliber(){return _caliber;} \ No newline at end of file diff --git a/ProjectileProfile.h b/ProjectileProfile.h new file mode 100644 index 0000000..64cf2a3 --- /dev/null +++ b/ProjectileProfile.h @@ -0,0 +1,31 @@ +#ifndef ProjectileProfile_H +#define ProjectileProfile_H + +#include + +class ProjectileProfile { + public: + enum ProjectileType{ + PELLET, + SLUG + }; + + // Constructor: Called when a ProjectileProfile object is created + ProjectileProfile(); + ProjectileProfile(String name, ProjectileType type, float caliber, float weight); + + String getName(); + float getWeight(); + String getType(); + float getCaliber(); + + private: + char _name[50]; + float _caliber; + float _weight; + ProjectileType _type; + + +}; + +#endif \ No newline at end of file diff --git a/driver.h b/driver.h new file mode 100644 index 0000000..fdb0753 --- /dev/null +++ b/driver.h @@ -0,0 +1,2 @@ +#define BOARD_SCREEN_COMBO 502 // 7.5 inch monochrome ePaper Screen (UC8179) +#define USE_XIAO_EPAPER_DRIVER_BOARD \ No newline at end of file