initial commit

This commit is contained in:
Dan Priece
2026-05-19 09:15:34 -04:00
commit f4201b974b
21 changed files with 2130 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+397
View File
@@ -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
View File
@@ -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
+179
View File
@@ -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);
}
+12
View File
@@ -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;}
+27
View File
@@ -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
+22
View File
@@ -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;
}
+26
View File
@@ -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
+173
View File
@@ -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();
}
+49
View File
@@ -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
+471
View File
@@ -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();
}
+39
View File
@@ -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
View File
@@ -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);
}
+49
View File
@@ -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
View File
@@ -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();
}
+33
View File
@@ -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
+10
View File
@@ -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;}
+30
View File
@@ -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
+14
View File
@@ -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;}
+31
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
#define BOARD_SCREEN_COMBO 502 // 7.5 inch monochrome ePaper Screen UC8179
#define USE_XIAO_EPAPER_DRIVER_BOARD