commit f4201b974b92939e22a04761611e758736b5abcc Author: Dan Priece Date: Tue May 19 09:15:34 2026 -0400 initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5cf62d8 Binary files /dev/null and b/.DS_Store differ 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