initial commit
This commit is contained in:
+397
@@ -0,0 +1,397 @@
|
||||
#include "ChronoBLE.h"
|
||||
#include <NimBLEDevice.h>
|
||||
|
||||
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");
|
||||
}*/
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
#ifndef ChronoBLE_H
|
||||
#define ChronoBLE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include <GunProfile.h>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "TFT_eSPI.h"
|
||||
#include "ChronoBLE.h"
|
||||
#include "ChronoReading.h"
|
||||
#include <LinkedList.h>
|
||||
#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);
|
||||
}
|
||||
@@ -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;}
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef ChronoProfile_H
|
||||
#define ChronoProfile_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef ChronoReading_H
|
||||
#define ChronoReading_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#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
|
||||
@@ -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<GunProfile>& 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<ProjectileProfile>& 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<ChronoReading>& 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()<low){
|
||||
low=_chronoReadings.get(i).getFPS();
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
int ChronoReadingManager::getHighFPS() {
|
||||
int high=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();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#ifndef ChronoReadingManager_H
|
||||
#define ChronoReadingManager_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "ChronoReading.h"
|
||||
#include "ChronoProfile.h"
|
||||
#include "GunProfile.h"
|
||||
#include "ProjectileProfile.h"
|
||||
#include <LinkedList.h>
|
||||
|
||||
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<ChronoReading>& getChronographReadings();
|
||||
ChronoProfile& getProfile();
|
||||
void addProjectileProfile(ProjectileProfile profile, bool writeToDisk = true);
|
||||
LinkedList<ProjectileProfile>& getProjectileProfiles();
|
||||
void setProjectileProfile(int id);
|
||||
void removeProjectileProfile(int id);
|
||||
void addGunProfile(GunProfile profile, bool writeToDisk = true);
|
||||
LinkedList<GunProfile>& getGunProfiles();
|
||||
void setGunProfile(int id);
|
||||
void removeGunProfile(int id);
|
||||
void reset();
|
||||
|
||||
private:
|
||||
ChronoReadingManager();
|
||||
ChronoReadingManager(const ChronoReadingManager&) = delete;
|
||||
ChronoReadingManager& operator=(const ChronoReadingManager&) = delete;
|
||||
LinkedList<ChronoReading> _chronoReadings;
|
||||
LinkedList<GunProfile> _gunProfiles;
|
||||
LinkedList<ProjectileProfile> _projectileProfiles;
|
||||
ChronoProfile* _currentProfile;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,471 @@
|
||||
#include "ChronoWebServer.h"
|
||||
#include <WebServer.h>
|
||||
#include <ProjectileProfile.h>
|
||||
#include <ChronoReadingManager.h>
|
||||
#include <FileManager.h>
|
||||
#include <ChronoProfile.h>
|
||||
#include <GunProfile.h>
|
||||
#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="<html>\
|
||||
<head>\
|
||||
<title>FX Chronograph Display Settings</title>\
|
||||
<style>\
|
||||
body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
|
||||
.clickable-row {cursor: pointer;}\
|
||||
.clickable-row:hover {background-color: #f0f0f0;}\
|
||||
</style>\
|
||||
</head>\
|
||||
<body>\
|
||||
<script>\
|
||||
function addGunProfile() {window.location.href = '/gunprofile';}\
|
||||
function addProjectileProfile() {window.location.href = '/projectileprofile';}\
|
||||
function setProfile(type,id){\
|
||||
const formData = new URLSearchParams();\
|
||||
formData.append('id', id);\
|
||||
var url='/projectileprofile/set';\
|
||||
if (type=='gun'){url='/gunprofile/set';}\
|
||||
fetch(url, {\
|
||||
method: 'POST',\
|
||||
body: formData\
|
||||
})\
|
||||
.then(response => response.text())\
|
||||
.then(data => {console.log(data); window.location.href = '/';})\
|
||||
.catch(error => console.error('Error:', error));\
|
||||
}\
|
||||
function exportData(){\
|
||||
const link = document.createElement('a');\
|
||||
link.href = '/export';\
|
||||
link.setAttribute('download', 'data.csv');\
|
||||
document.body.appendChild(link);\
|
||||
link.click();\
|
||||
document.body.removeChild(link);\
|
||||
}\
|
||||
function reset(){\
|
||||
fetch('/reset', {\
|
||||
method: 'GET',\
|
||||
})\
|
||||
.then(response => response.text())\
|
||||
.then(data => console.log(data))\
|
||||
.catch(error => console.error('Error:', error));\
|
||||
}\
|
||||
function format(){\
|
||||
fetch('/format', {\
|
||||
method: 'GET',\
|
||||
})\
|
||||
.then(response => response.text())\
|
||||
.then(data => console.log(data))\
|
||||
.catch(error => console.error('Error:', error));\
|
||||
}\
|
||||
function toggleTestMode(){\
|
||||
fetch('/testmode', {\
|
||||
method: 'GET',\
|
||||
})\
|
||||
.then(response => response.text())\
|
||||
.then(data => console.log(data))\
|
||||
.catch(error => console.error('Error:', error));\
|
||||
}\
|
||||
document.addEventListener('DOMContentLoaded', function() {\
|
||||
const rows = document.querySelectorAll('.clickable-row');\
|
||||
rows.forEach(row => {\
|
||||
row.addEventListener('click', function() {\
|
||||
const type = this.dataset.type;\
|
||||
const id = this.dataset.id;\
|
||||
if (id) {\
|
||||
setProfile(type,id);\
|
||||
}\
|
||||
});\
|
||||
});\
|
||||
const removeButtons = document.querySelectorAll('.remove-button');\
|
||||
removeButtons.forEach(button => {\
|
||||
button.addEventListener('click', (event) => {\
|
||||
event.stopPropagation();\
|
||||
const type = event.target.dataset.type;\
|
||||
const id = event.target.dataset.id;\
|
||||
var url='/projectileprofile/remove';\
|
||||
if (type === 'gun') {\
|
||||
url='/gunprofile/remove';\
|
||||
}\
|
||||
const formData = new URLSearchParams();\
|
||||
formData.append('id', id);\
|
||||
fetch(url, {\
|
||||
method: 'POST',\
|
||||
body: formData\
|
||||
})\
|
||||
.then(response => response.text())\
|
||||
.then(data => {console.log(data); window.location.href = '/';})\
|
||||
.catch(error => console.error('Error:', error));\
|
||||
});\
|
||||
});\
|
||||
});\
|
||||
</script>\
|
||||
<h1>ChronoDisplay Settings</h1>\
|
||||
<div style='margin-top:50px; position:relative;'>\
|
||||
<h3>Control Panel</h3>\
|
||||
<button onclick='exportData()'>Export</button>\
|
||||
<button onclick='reset()'>Reset</button>\
|
||||
<button onclick='format()'>Format</button>\
|
||||
<button onclick='toggleTestMode()'>Toggle Test Mode</button>\
|
||||
</div>\
|
||||
<div style='margin-top:100px; position:relative;'>\
|
||||
<h3>Gun Profiles</h3>\
|
||||
<table style='width:600px; min-height:100px; border-collapse: collapse;'>\
|
||||
<tr style='border-bottom:1px solid black;'><th>Name</th><th>Type</th><th></th></tr>";
|
||||
ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
|
||||
|
||||
String currentGunName=chronoReadings.getProfile().getGunProfile().getName();
|
||||
int currentGunPowerLevel=chronoReadings.getProfile().getGunProfile().getPowerLevel();
|
||||
LinkedList<GunProfile>& gprofiles= chronoReadings.getGunProfiles();
|
||||
for(int i=0;i<gprofiles.size();i++){
|
||||
String name=gprofiles.get(i).getName();
|
||||
String type="";
|
||||
int powerlevel=gprofiles.get(i).getPowerLevel();
|
||||
if (powerlevel==0){type="Bow or Airsoft";}
|
||||
else if (powerlevel==1){type="CO2 Pistol";}
|
||||
else if (powerlevel==2){type="Air Pistol";}
|
||||
else if (powerlevel==3){type="Air Gun UK";}
|
||||
else if (powerlevel==4){type="FAC";}
|
||||
if (strcmp(name.c_str(),currentGunName.c_str())==0 && currentGunPowerLevel==powerlevel){
|
||||
temp +="<tr class='clickable-row' data-type='gun' data-id='"+String(i)+"' style='background-color:#b0b0b0;'>";
|
||||
}
|
||||
else{
|
||||
temp +="<tr class='clickable-row' data-type='gun' data-id='"+String(i)+"'>";
|
||||
}
|
||||
temp +="<td>"+name+"</td><td>"+type+"</td><td><button class='remove-button' data-type='gun' data-id='"+String(i)+"'>Remove</button></td></tr>";
|
||||
}
|
||||
|
||||
temp +="</table>\
|
||||
<button onclick='addGunProfile()' style='position:absolute; top:10px; left:575px;'>Add</button>\
|
||||
</div>\
|
||||
<div style='margin-top:100px; position:relative;'>\
|
||||
<h3>Projectile Profiles</h3>\
|
||||
<table style='width:600px; min-height:100px; border-collapse: collapse;'>\
|
||||
<tr style='border-bottom:1px solid black;'><th>Name</th><th>Type</th><th>Caliber</th><th>Weight</th><th></th></tr>";
|
||||
String currentProjectileProfileName=chronoReadings.getProfile().getProjectileProfile().getName();
|
||||
float currentProjectileProfileWeight=chronoReadings.getProfile().getProjectileProfile().getWeight();
|
||||
LinkedList<ProjectileProfile>& profiles= chronoReadings.getProjectileProfiles();
|
||||
for(int i=0;i<profiles.size();i++){
|
||||
String name=profiles.get(i).getName();
|
||||
String type=profiles.get(i).getType();
|
||||
String cal=String(profiles.get(i).getCaliber());
|
||||
float fweight=profiles.get(i).getWeight();
|
||||
String weight=String(fweight);
|
||||
if (strcmp(name.c_str(),currentProjectileProfileName.c_str())==0 && fweight==currentProjectileProfileWeight){
|
||||
temp +="<tr class='clickable-row' data-type='projectile' data-id='"+String(i)+"' style='background-color:#b0b0b0;'>";
|
||||
}
|
||||
else{
|
||||
temp +="<tr class='clickable-row' data-type='projectile' data-id='"+String(i)+"'>";
|
||||
}
|
||||
temp +="<td>"+name+"</td><td>"+type+"</td><td>"+cal+"</td><td>"+weight+"</td><td><button class='remove-button' data-type='projectile' data-id='"+String(i)+"'>Remove</button></td></tr>";
|
||||
}
|
||||
|
||||
|
||||
temp +="</table>\
|
||||
<button onclick='addProjectileProfile()' style='position:absolute; top:10px; left:575px;'>Add</button>\
|
||||
</div>\
|
||||
</body>\
|
||||
</html>";
|
||||
_server.send(200, "text/html", temp);
|
||||
}
|
||||
void ChronoWebServer::_handleGunProfile(){
|
||||
String temp="<html>\
|
||||
<head>\
|
||||
<title>Gun Profile</title>\
|
||||
<style>\
|
||||
body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
|
||||
</style>\
|
||||
</head>\
|
||||
<body>\
|
||||
<script>\
|
||||
function addProfile() {\
|
||||
var name=document.getElementById('name').value;\
|
||||
var type=document.getElementById('type').value;\
|
||||
const formData = new URLSearchParams();\
|
||||
formData.append('name', name);\
|
||||
formData.append('type', type);\
|
||||
fetch('/gunprofile/add', {\
|
||||
method: 'POST',\
|
||||
body: formData\
|
||||
})\
|
||||
.then(response => response.text())\
|
||||
.then(data => {console.log(data); window.location.href = '/';})\
|
||||
.catch(error => console.error('Error:', error));\
|
||||
}\
|
||||
</script>\
|
||||
<h1>Add Gun Profile</h1>\
|
||||
<div>\
|
||||
<h3>Name</h3>\
|
||||
<input id='name' style='width:200px;'></input>\
|
||||
<h3>Type</h3>\
|
||||
<select id='type'>\
|
||||
<option value='0'>Bow or Airsoft</option>\
|
||||
<option value='1'>CO2 Pistol</option>\
|
||||
<option value='2'>Air Pistol</option>\
|
||||
<option value='3'>Air Gun UK</option>\
|
||||
<option value='4'>FAC</option>\
|
||||
</select>\
|
||||
<div style='margin-top:50px;'><button onclick='addProfile()'>Add Profile</button></div>\
|
||||
</div>\
|
||||
</body>\
|
||||
</html>";
|
||||
_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="<html>\
|
||||
<head>\
|
||||
<title>Projectile Profile</title>\
|
||||
<style>\
|
||||
body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
|
||||
</style>\
|
||||
</head>\
|
||||
<body>\
|
||||
<script>\
|
||||
function addProfile() {\
|
||||
var name=document.getElementById('name').value;\
|
||||
var type=document.getElementById('type').value;\
|
||||
var cal=document.getElementById('caliber').value;\
|
||||
var weight=document.getElementById('weight').value;\
|
||||
const formData = new URLSearchParams();\
|
||||
formData.append('name', name);\
|
||||
formData.append('type', type);\
|
||||
formData.append('cal', cal);\
|
||||
formData.append('weight', weight);\
|
||||
fetch('/projectileprofile/add', {\
|
||||
method: 'POST',\
|
||||
body: formData\
|
||||
})\
|
||||
.then(response => response.text())\
|
||||
.then(data => {console.log(data); window.location.href = '/';})\
|
||||
.catch(error => console.error('Error:', error));\
|
||||
}\
|
||||
</script>\
|
||||
<h1>Add Projectile Profile</h1>\
|
||||
<div>\
|
||||
<h3>Name</h3>\
|
||||
<input id='name' style='width:200px;'></input>\
|
||||
<h3>Type</h3>\
|
||||
<select id='type'>\
|
||||
<option value='pellet'>Pellet</option>\
|
||||
<option value='slug'>Slug</option>\
|
||||
</select>\
|
||||
<h3>Caliber</h3>\
|
||||
<input id='caliber' style='width:50px;'></input>\
|
||||
<h3>Weight</h3>\
|
||||
<input id='weight' style='width:50px;'></input>\
|
||||
<div style='margin-top:50px;'><button onclick='addProfile()'>Add Profile</button></div>\
|
||||
</div>\
|
||||
</body>\
|
||||
</html>";
|
||||
_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<ChronoReading>& 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<count;i++){
|
||||
ChronoReading r=readings.get(i);
|
||||
String shotNum=String(i+1);
|
||||
String fps=String(r.getFPS());
|
||||
String fpe=String(r.getFPE());
|
||||
String gun=r.getProfile().getGunProfile().getName();
|
||||
String power=String(r.getProfile().getGunProfile().getPowerLevel());
|
||||
String projectile=r.getProfile().getProjectileProfile().getName();
|
||||
String ptype=r.getProfile().getProjectileProfile().getType();
|
||||
String cal=String(r.getProfile().getProjectileProfile().getCaliber());
|
||||
String weight=String(r.getProfile().getProjectileProfile().getWeight());
|
||||
csvData += shotNum + ",";
|
||||
csvData += fps + ",";
|
||||
csvData += fpe + ",";
|
||||
csvData += gun + ",";
|
||||
csvData += power + ",";
|
||||
csvData += projectile + ",";
|
||||
csvData += ptype + ",";
|
||||
csvData += cal + ",";
|
||||
csvData += weight + "\n";
|
||||
}
|
||||
|
||||
_server.sendHeader("Content-Disposition", "attachment; filename=data.csv");
|
||||
_server.send(200, "text/csv", csvData);
|
||||
|
||||
|
||||
//_server.send(200, "text/html", "ok");
|
||||
}
|
||||
void ChronoWebServer::_handleReset(){
|
||||
ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
|
||||
chronoReadings.reset();
|
||||
Display::updateEntireDisplay();
|
||||
Display::refreshDisplay();
|
||||
_server.send(200, "text/html", "ok");
|
||||
}
|
||||
void ChronoWebServer::_handleFormat(){
|
||||
Serial.println("formating disk, erasing all data...");
|
||||
FileManager& files = FileManager::getInstance();
|
||||
files.formatDisk();
|
||||
_server.send(200, "text/html", "ok");
|
||||
}
|
||||
void ChronoWebServer::setTestModeCallback(void (*pFunc)()){
|
||||
testModeCallback=pFunc;
|
||||
}
|
||||
void ChronoWebServer::_handleTestMode(){
|
||||
Serial.println("toggling test mode....");
|
||||
testModeCallback();
|
||||
_server.send(200, "text/html", "ok");
|
||||
}
|
||||
void ChronoWebServer::init(){
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
Serial.println("");
|
||||
Serial.print("Connected to ");
|
||||
Serial.println(ssid);
|
||||
Serial.print("IP address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
|
||||
if (MDNS.begin("esp32")) {
|
||||
Serial.println("MDNS responder started");
|
||||
}
|
||||
|
||||
_server.on("/gunprofile/add", _handleAddGunProfile);
|
||||
_server.on("/gunprofile/set", _handleSetGunProfile);
|
||||
_server.on("/gunprofile/remove", _handleRemoveGunProfile);
|
||||
_server.on("/gunprofile", _handleGunProfile);
|
||||
_server.on("/projectileprofile/set", _handleSetProjectileProfile);
|
||||
_server.on("/projectileprofile/add", _handleAddProjectileProfile);
|
||||
_server.on("/projectileprofile/remove", _handleRemoveProjectileProfile);
|
||||
_server.on("/projectileprofile", _handleProjectileProfile);
|
||||
_server.on("/export", _handleExport);
|
||||
_server.on("/reset", _handleReset);
|
||||
_server.on("/format", _handleFormat);
|
||||
_server.on("/testmode", _handleTestMode);
|
||||
_server.on("/", _handleRoot);
|
||||
_server.begin();
|
||||
}
|
||||
void ChronoWebServer::process(){
|
||||
_server.handleClient();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef ChronoWebServer_H
|
||||
#define ChronoWebServer_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <NetworkClient.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <Update.h>
|
||||
#include <ChronoBLE.h>
|
||||
#include <GunProfile.h>
|
||||
|
||||
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
|
||||
+278
@@ -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<ChronoReading> &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<chronographReadingCount;i++){
|
||||
if (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<ChronoReading> &readings=chronoReadings.getChronographReadings();
|
||||
updateLastShots(readings);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#ifndef Display_H
|
||||
#define Display_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <LinkedList.h>
|
||||
#include <ChronoReading.h>
|
||||
#include <ChronoReadingManager.h>
|
||||
#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<ChronoReading> &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
|
||||
+247
@@ -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<GunProfile>& 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<ProjectileProfile>& 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();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef FileManager_H
|
||||
#define FileManager_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "FS.h"
|
||||
#include <LittleFS.h>
|
||||
#include <GunProfile.h>
|
||||
#include <ProjectileProfile.h>
|
||||
#include <LinkedList.h>
|
||||
|
||||
class FileManager {
|
||||
public:
|
||||
static FileManager& getInstance();
|
||||
|
||||
void init();
|
||||
void formatDisk();
|
||||
void saveGunProfile(GunProfile& profile);
|
||||
void removeGunProfile(GunProfile& profile);
|
||||
void removeAllGunProfiles();
|
||||
void loadGunProfiles(LinkedList<GunProfile>& profiles);
|
||||
void saveProjectileProfile(ProjectileProfile& profile);
|
||||
void removeProjectileProfile(ProjectileProfile& profile);
|
||||
void loadProjectileProfiles(LinkedList<ProjectileProfile>& profiles);
|
||||
|
||||
|
||||
private:
|
||||
FileManager();
|
||||
FileManager(const FileManager&) = delete;
|
||||
FileManager& operator=(const FileManager&) = delete;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -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;}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef GunProfile_H
|
||||
#define GunProfile_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
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
|
||||
@@ -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;}
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef ProjectileProfile_H
|
||||
#define ProjectileProfile_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user