Initial version for testing

This commit is contained in:
Magnus
2021-03-26 19:42:58 +01:00
commit 5f04d6e65e
41 changed files with 15774 additions and 0 deletions

99
src/calc.cpp Normal file
View File

@ -0,0 +1,99 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "calc.h"
#include "helper.h"
#include "config.h"
#include "tinyexpr.h"
#include "tempsensor.h"
//#define LOG_LEVEL 5
//
// Calculates gravity according to supplied formula, compatible with iSpindle/Fermentrack formula
//
double calculateGravity( double angle, double temp ) {
const char* formula = myConfig.getGravityFormula();
#if LOG_LEVEL==6
Log.verbose(F("CALC: Calculating gravity for angle %F, temp %F." CR), angle, temp);
Log.verbose(F("CALC: Formula %s." CR), formula);
#endif
if( strlen(formula) == 0 )
return 0.0;
// Store variable names and pointers.
te_variable vars[] = {{"tilt", &angle}, {"temp", &temp}};
int err;
// Compile the expression with variables.
te_expr *expr = te_compile(formula, vars, 2, &err);
if (expr) {
double g = te_eval(expr);
te_free(expr);
#if LOG_LEVEL==6
Log.verbose(F("CALC: Calculated gravity is %F." CR), g);
#endif
return g;
}
Log.error(F("CALC: Failed to parse expression %d." CR), err);
return 0;
}
//
// Do a standard gravity temperature correction. This is a simple way to adjust for differnt worth temperatures
//
double gravityTemperatureCorrection( double gravity, double temp, double calTemp) {
#if LOG_LEVEL==6
Log.verbose(F("CALC: Adjusting gravity based on temperature, gravity %F, temp %F, calTemp %F." CR), gravity, temp, calTemp);
#endif
double tempF = convertCtoF( temp );
double calTempF = convertCtoF(calTemp);
const char* formula = "gravity*((1.00130346-0.000134722124*temp+0.00000204052596*temp^2-0.00000000232820948*temp^3)/(1.00130346-0.000134722124*cal+0.00000204052596*cal^2-0.00000000232820948*cal^3))";
// Store variable names and pointers.
te_variable vars[] = {{"gravity", &gravity}, {"temp", &tempF}, {"cal", &calTempF}};
int err;
// Compile the expression with variables.
te_expr *expr = te_compile(formula, vars, 3, &err);
if (expr) {
double g = te_eval(expr);
te_free(expr);
#if LOG_LEVEL==6
Log.verbose(F("CALC: Corrected gravity is %F." CR), g);
#endif
return g;
}
Log.error(F("CALC: Failed to parse expression %d, no correction has been made." CR), err);
return gravity;
}
// EOF

36
src/calc.h Normal file
View File

@ -0,0 +1,36 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef _CALC_H
#define _CALC_H
// Includes
#include "helper.h"
// Functions
double calculateGravity( double angle, double temp );
double gravityTemperatureCorrection( double gravity, double temp, double calTemp = 20 );
#endif // _CALC_H
// EOF

232
src/config.cpp Normal file
View File

@ -0,0 +1,232 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "config.h"
#include "helper.h"
#include <LittleFS.h>
Config myConfig;
//
// Create the config class with default settings.
//
Config::Config() {
// Assiging default values
sprintf(&id[0], "%6x", (unsigned int) ESP.getChipId() );
sprintf(&mDNS[0], "" WIFI_MDNS "%s", getID() );
setTempFormat('C');
setPushInterval(900); // 15 minutes
setVoltageFactor(1.59); // Conversion factor for battery
setTempSensorAdj(0.0);
setGravityTempAdj(false);
gyroCalibration = { 0, 0, 0, 0, 0 ,0 };
saveNeeded = false;
}
//
// Populate the json document with all configuration parameters (used in both web and saving to file)
//
void Config::createJson(DynamicJsonDocument& doc) {
doc[ CFG_PARAM_MDNS ] = getMDNS();
doc[ CFG_PARAM_ID ] = getID();
doc[ CFG_PARAM_OTA ] = getOtaURL();
doc[ CFG_PARAM_TEMPFORMAT ] = String( getTempFormat() );
doc[ CFG_PARAM_PUSH_BREWFATHER ] = getBrewfatherPushTarget();
doc[ CFG_PARAM_PUSH_HTTP ] = getHttpPushTarget();
doc[ CFG_PARAM_PUSH_INTERVAL ] = getPushInterval();
doc[ CFG_PARAM_VOLTAGEFACTOR ] = getVoltageFactor();
doc[ CFG_PARAM_GRAVITY_FORMULA ] = getGravityFormula();
doc[ CFG_PARAM_TEMP_ADJ ] = getTempSensorAdj();
doc[ CFG_PARAM_GRAVITY_TEMP_ADJ ] = isGravityTempAdj();
JsonObject cal = doc.createNestedObject( CFG_PARAM_GYRO_CALIBRATION );
cal["ax"] = gyroCalibration.ax;
cal["ay"] = gyroCalibration.ay;
cal["az"] = gyroCalibration.az;
cal["gx"] = gyroCalibration.gx;
cal["gy"] = gyroCalibration.gy;
cal["gz"] = gyroCalibration.gz;
}
//
// Save json document to file
//
bool Config::saveFile() {
if( !saveNeeded ) {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Skipping save, not needed." CR));
#endif
return true;
}
#if LOG_LEVEL==6
Log.verbose(F("CFG : Saving configuration to file." CR));
#endif
File configFile = LittleFS.open(CFG_FILENAME, "w");
if (!configFile) {
Log.error(F("CFG : Failed to open file " CFG_FILENAME " for save." CR));
return false;
}
DynamicJsonDocument doc(CFG_JSON_BUFSIZE);
createJson( doc );
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
serializeJson(doc, configFile);
configFile.flush();
configFile.close();
saveNeeded = false;
myConfig.debug();
Log.notice(F("CFG : Configuration saved to " CFG_FILENAME "." CR));
return true;
}
//
// Load config file from disk
//
bool Config::loadFile() {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Loading configuration from file." CR));
#endif
if (!LittleFS.exists(CFG_FILENAME)) {
Log.error(F("CFG : Configuration file does not exist " CFG_FILENAME "." CR));
return false;
}
File configFile = LittleFS.open(CFG_FILENAME, "r");
if (!configFile) {
Log.error(F("CFG : Failed to open " CFG_FILENAME "." CR));
return false;
}
DynamicJsonDocument doc(CFG_JSON_BUFSIZE);
DeserializationError err = deserializeJson(doc, configFile);
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
configFile.close();
if( err ) {
Log.error(F("CFG : Failed to parse " CFG_FILENAME " file, Err: %s, %d." CR), err.c_str(), doc.capacity());
return false;
}
#if LOG_LEVEL==6
Log.verbose(F("CFG : Parsed configuration file." CR));
#endif
if( !doc[ CFG_PARAM_OTA ].isNull() )
setOtaURL( doc[ CFG_PARAM_OTA ] );
if( !doc[ CFG_PARAM_MDNS ].isNull() )
setMDNS( doc[ CFG_PARAM_MDNS ] );
if( !doc[ CFG_PARAM_TEMPFORMAT ].isNull() ) {
String s = doc[ CFG_PARAM_TEMPFORMAT ];
setTempFormat( s.charAt(0) );
}
if( !doc[ CFG_PARAM_PUSH_BREWFATHER ].isNull() )
setBrewfatherPushTarget( doc[ CFG_PARAM_PUSH_BREWFATHER ] );
if( !doc[ CFG_PARAM_PUSH_HTTP ].isNull() )
setHttpPushTarget( doc[ CFG_PARAM_PUSH_HTTP ] );
if( !doc[ CFG_PARAM_PUSH_INTERVAL ].isNull() )
setPushInterval( doc[ CFG_PARAM_PUSH_INTERVAL ].as<int>() );
if( !doc[ CFG_PARAM_VOLTAGEFACTOR ].isNull() )
setVoltageFactor( doc[ CFG_PARAM_VOLTAGEFACTOR ].as<float>() );
if( !doc[ CFG_PARAM_GRAVITY_FORMULA ].isNull() )
setGravityFormula( doc[ CFG_PARAM_GRAVITY_FORMULA ] );
if( !doc[ CFG_PARAM_TEMP_ADJ ].isNull() )
setTempSensorAdj( doc[ CFG_PARAM_TEMP_ADJ ].as<float>() );
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["ax"].isNull() )
gyroCalibration.ax = doc[ CFG_PARAM_GYRO_CALIBRATION ]["ax"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["ay"].isNull() )
gyroCalibration.ay = doc[ CFG_PARAM_GYRO_CALIBRATION ]["ay"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["az"].isNull() )
gyroCalibration.az = doc[ CFG_PARAM_GYRO_CALIBRATION ]["az"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["gx"].isNull() )
gyroCalibration.gx = doc[ CFG_PARAM_GYRO_CALIBRATION ]["gx"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["gy"].isNull() )
gyroCalibration.gy = doc[ CFG_PARAM_GYRO_CALIBRATION ]["gy"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["gz"].isNull() )
gyroCalibration.gz = doc[ CFG_PARAM_GYRO_CALIBRATION ]["gz"];
myConfig.debug();
saveNeeded = false; // Reset save flag
Log.notice(F("CFG : Configuration file " CFG_FILENAME " loaded." CR));
return true;
}
//
// Check if file system can be mounted, if not we format it.
//
void Config::formatFileSystem() {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Formating filesystem." CR));
#endif
LittleFS.format();
}
//
// Check if file system can be mounted, if not we format it.
//
void Config::checkFileSystem() {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Checking if filesystem is valid." CR));
#endif
if (LittleFS.begin()) {
Log.notice(F("CFG : Filesystem mounted." CR));
} else {
Log.error(F("CFG : Unable to mount file system, formatting..." CR));
LittleFS.format();
}
}
//
// Dump the configuration to the serial port
//
void Config::debug() {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Dumping configration " CFG_FILENAME "." CR));
Log.verbose(F("CFG : ID; '%s'." CR), getID());
Log.verbose(F("CFG : mDNS; '%s'." CR), getMDNS() );
Log.verbose(F("CFG : OTA; '%s'." CR), getOtaURL() );
Log.verbose(F("CFG : Temp; %c." CR), getTempFormat() );
Log.verbose(F("CFG : Temp Adj; %F." CR), getTempSensorAdj() );
Log.verbose(F("CFG : VoltageFactor; %F." CR), getVoltageFactor() );
Log.verbose(F("CFG : Gravity formula; '%s'." CR), getGravityFormula() );
Log.verbose(F("CFG : Push brewfather; '%s'." CR), getBrewfatherPushTarget() );
Log.verbose(F("CFG : Push http; '%s'." CR), getHttpPushTarget() );
Log.verbose(F("CFG : Push interval; %d." CR), getPushInterval() );
// Log.verbose(F("CFG : Accel offset\t%d\t%d\t%d" CR), gyroCalibration.ax, gyroCalibration.ay, gyroCalibration.az );
// Log.verbose(F("CFG : Gyro offset \t%d\t%d\t%d" CR), gyroCalibration.gx, gyroCalibration.gy, gyroCalibration.gz );
#endif
}
// EOF

175
src/config.h Normal file
View File

@ -0,0 +1,175 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef _CONFIG_H
#define _CONFIG_H
// Includes
#include "helper.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#include <stdlib.h>
// defintions
#define CFG_JSON_BUFSIZE 1000
#define CFG_APPNAME "GravityMon " // Name of firmware
#define CFG_FILENAME "/gravitymon.json" // Name of config file
#define WIFI_DEFAULT_SSID "GravityMon" // Name of created SSID
#define WIFI_DEFAULT_PWD "password" // Password for created SSID
#define WIFI_MDNS "gravitymon" // Prefix for MDNS name
#define WIFI_PORTAL_TIMEOUT 120 // Number of seconds until the config portal is closed
// These are used in API + Savefile
#define CFG_PARAM_ID "id"
#define CFG_PARAM_MDNS "mdns" // Device name
#define CFG_PARAM_OTA "ota-url" // Base URL for OTA
#define CFG_PARAM_PUSH_BREWFATHER "brewfather-push" // URL (brewfather format)
#define CFG_PARAM_PUSH_HTTP "http-push" // URL (iSpindle format)
#define CFG_PARAM_PUSH_INTERVAL "push-interval" // Time between push
#define CFG_PARAM_TEMPFORMAT "temp-format" // C or F
#define CFG_PARAM_VOLTAGEFACTOR "voltage-factor" // Factor to calculate the battery voltage
#define CFG_PARAM_GRAVITY_FORMULA "gravity-formula" // Formula for calculating gravity
#define CFG_PARAM_GRAVITY_TEMP_ADJ "gravity-temp-adjustment" // True/False. Adjust gravity for temperature
#define CFG_PARAM_TEMP_ADJ "temp-adjustment-value" // Correction value for temp sensor
#define CFG_PARAM_GYRO_CALIBRATION "gyro-calibration-data" // READ ONLY
// These are used in API's
#define CFG_PARAM_APP_NAME "app-name"
#define CFG_PARAM_APP_VER "app-ver"
#define CFG_PARAM_ANGLE "angle"
#define CFG_PARAM_GRAVITY "gravity"
#define CFG_PARAM_TEMP_C "temp-c"
#define CFG_PARAM_TEMP_F "temp-f"
#define CFG_PARAM_BATTERY "battery"
#define CFG_PARAM_SLEEP_MODE "sleep-mode"
#define CFG_PARAM_RSSI "rssi"
// Used for holding sensordata or sensoroffsets
struct RawGyroData {
int16_t ax; // Raw Acceleration
int16_t ay;
int16_t az;
int16_t gx; // Raw Position
int16_t gy;
int16_t gz;
int16_t temp; // Only for information (temperature of chip)
};
// Main configuration class
class Config {
private:
bool saveNeeded;
// Device configuration
char id[10];
char mDNS[30];
char otaURL[200];
char tempFormat; // C, F
float voltageFactor;
float tempSensorAdj; // This value will be added to the read sensor value
// Push target settings
char brewfatherPushTarget[200];
char httpPushTarget[200];
int pushInterval;
// Gravity and temperature calculations
char gravityFormula[200];
bool gravityTempAdj; // true, false
char gravityFormat; // G, P
// Gyro calibration data
RawGyroData gyroCalibration; // Holds the gyro calibration constants (6 * int16_t)
void debug();
void formatFileSystem();
public:
Config();
const char* getID() { return &id[0]; };
const char* getMDNS() { return &mDNS[0]; }
void setMDNS( const char* s ) { strncpy( &mDNS[0], s, sizeof(mDNS)-1); saveNeeded = true; }
const char* getOtaURL() { return &otaURL[0]; }
void setOtaURL( const char* s ) { strncpy( &otaURL[0], s, sizeof(otaURL)-1); saveNeeded = true; }
bool isOtaActive() { return strlen(&otaURL[0])>0?true:false; }
const char* getBrewfatherPushTarget() { return &brewfatherPushTarget[0]; }
void setBrewfatherPushTarget( const char* s ) { strncpy(&brewfatherPushTarget[0], s, sizeof(brewfatherPushTarget)-1); saveNeeded = true; }
bool isBrewfatherActive() { return strlen(&brewfatherPushTarget[0])>0?true:false; }
const char* getHttpPushTarget() { return &httpPushTarget[0]; }
void setHttpPushTarget( const char* s ) { strncpy(&httpPushTarget[0], s, sizeof(httpPushTarget)-1); saveNeeded = true; }
bool isHttpActive() { return strlen(&httpPushTarget[0])>0?true:false; }
int getPushInterval() { return pushInterval; }
void setPushInterval( int v ) { pushInterval = v; saveNeeded = true; }
void setPushInterval( const char* s ) { pushInterval = atoi(s); saveNeeded = true; }
char getTempFormat() { return tempFormat; }
void setTempFormat( char c ) { tempFormat = c; saveNeeded = true; }
bool isTempC() { return tempFormat=='C'?false:true; };
bool isTempF() { return tempFormat=='F'?false:true; };
float getVoltageFactor() { return voltageFactor; }
void setVoltageFactor( float f ) { voltageFactor = f; saveNeeded = true; }
void setVoltageFactor( const char* s ) { voltageFactor = atof(s); saveNeeded = true; }
float getTempSensorAdj() { return tempSensorAdj; }
void setTempSensorAdj( float f ) { tempSensorAdj = f; saveNeeded = true; }
void setTempSensorAdj( const char* s ) { tempSensorAdj = atof(s); saveNeeded = true; }
const char* getGravityFormula() { return &gravityFormula[0]; }
void setGravityFormula( const char* s ) { strncpy(&gravityFormula[0], s, sizeof(gravityFormula)-1); saveNeeded = true; }
bool isGravityTempAdj() { return gravityTempAdj; }
void setGravityTempAdj( bool b ) { gravityTempAdj = b; saveNeeded = true; }
char getGravityFormat() { return gravityFormat; }
void setGravityFormat( char c ) { gravityFormat = c; saveNeeded = true; }
bool isGravitySG() { return gravityFormat=='G'?false:true; };
bool isGravityPlato() { return gravityFormat=='P'?false:true; };
const RawGyroData& getGyroCalibration() { return gyroCalibration; }
void setGyroCalibration( const RawGyroData &r ) { gyroCalibration = r; saveNeeded = true; }
// IO functions
void createJson(DynamicJsonDocument& doc);
bool saveFile();
bool loadFile();
void checkFileSystem();
bool isSaveNeeded() { return saveNeeded; };
void setSaveNeeded() { saveNeeded = true; };
};
// Global instance created
extern Config myConfig;
#endif // _CONFIG_H
// EOF

327
src/gyro.cpp Normal file
View File

@ -0,0 +1,327 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "gyro.h"
#include "helper.h"
GyroSensor myGyro;
#define SENSOR_MOVING_THREASHOLD 500
#define SENSOR_READ_COUNT 50
#define SENSOR_READ_DELAY 3150 // us, empirical, to hold sampling to 200 Hz
//#define GYRO_SHOW_MINMAX // Will calculate the min/max values when doing calibration
//#define GYRO_CALIBRATE_STARTUP // Will calibrate sensor at startup
//
// Initialize the sensor chip.
//
bool GyroSensor::setup() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Setting up hardware." CR));
#endif
Wire.begin(D3, D4);
Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
accelgyro.initialize();
if( !accelgyro.testConnection() ) {
Log.error(F("GYRO: Failed to connect to MPU6050 (gyro)." CR));
sensorConnected = false;
} else {
Log.notice(F("GYRO: Connected to MPU6050 (gyro)." CR));
sensorConnected = true;
// Configure ethe sensor
accelgyro.setTempSensorEnabled(true);
accelgyro.setFullScaleAccelRange(MPU6050_ACCEL_FS_2);
accelgyro.setFullScaleGyroRange(MPU6050_GYRO_FS_250);
accelgyro.setDLPFMode(MPU6050_DLPF_BW_5);
accelgyro.setRate(17);
// For now we run the calibration at start.
#if defined ( GYRO_CALIBRATE_STARTUP )
calibrateSensor();
#endif
// Once we have calibration values stored we just apply them from the config.
calibrationOffset = myConfig.getGyroCalibration();
applyCalibration();
}
return sensorConnected;
}
//
// Set sensor in sleep mode to conserve battery
//
void GyroSensor::enterSleep() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Setting up hardware." CR));
#endif
accelgyro.setSleepEnabled( true );
}
//
// Do a number of reads to get a more stable value.
//
void GyroSensor::readSensor(RawGyroData &raw, const int noIterations, const int delayTime) {
RawGyroDataL average = { 0, 0, 0, 0, 0, 0 };
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Reading sensor with %d iterations %d us delay." CR), noIterations, delayTime );
#endif
// Set some initial values
#if defined( GYRO_SHOW_MINMAX )
RawGyroData min, max;
//accelgyro.getRotation( &min.gx, &min.gy, &min.gz );
accelgyro.getAcceleration( &min.ax, &min.ay, &min.az );
min.temp = accelgyro.getTemperature();
max = min;
#endif
for(int cnt = 0; cnt < noIterations ; cnt ++) {
accelgyro.getRotation( &raw.gx, &raw.gy, &raw.gz );
accelgyro.getAcceleration( &raw.ax, &raw.ay, &raw.az );
raw.temp = accelgyro.getTemperature();
average.ax += raw.ax;
average.ay += raw.ay;
average.az += raw.az;
average.gx += raw.gx;
average.gy += raw.gy;
average.gz += raw.gz;
average.temp += raw.temp;
// Log what the minium value is
#if defined( GYRO_SHOW_MINMAX )
if( raw.ax < min.ax ) min.ax = raw.ax;
if( raw.ay < min.ay ) min.ay = raw.ay;
if( raw.az < min.az ) min.az = raw.az;
if( raw.gx < min.gx ) min.gx = raw.gx;
if( raw.gy < min.gy ) min.gy = raw.gy;
if( raw.gz < min.gz ) min.gz = raw.gz;
if( raw.temp < min.temp ) min.temp = raw.temp;
// Log what the maximum value is
if( raw.ax > max.ax ) max.ax = raw.ax;
if( raw.ay > max.ay ) max.ay = raw.ay;
if( raw.az > max.az ) max.az = raw.az;
if( raw.gx > max.gx ) max.gx = raw.gx;
if( raw.gy > max.gy ) max.gy = raw.gy;
if( raw.gz > max.gz ) max.gz = raw.gz;
if( raw.temp > max.temp ) max.temp = raw.temp;
#endif
delayMicroseconds( delayTime );
}
raw.ax = average.ax/noIterations;
raw.ay = average.ay/noIterations;
raw.az = average.az/noIterations;
raw.gx = average.gx/noIterations;
raw.gy = average.gy/noIterations;
raw.gz = average.gz/noIterations;
raw.temp = average.temp/noIterations;
#if LOG_LEVEL==6
#if defined( GYRO_SHOW_MINMAX )
Log.verbose(F("GYRO: Min \t%d\t%d\t%d\t%d\t%d\t%d\t%d." CR), min.ax, min.ay, min.az, min.gx, min.gy, min.gz, min.temp );
Log.verbose(F("GYRO: Max \t%d\t%d\t%d\t%d\t%d\t%d\t%d." CR), max.ax, max.ay, max.az, max.gx, max.gy, max.gz, max.temp );
#endif
Log.verbose(F("GYRO: Average\t%d\t%d\t%d\t%d\t%d\t%d\t%d." CR), raw.ax, raw.ay, raw.az, raw.gx, raw.gy, raw.gz, raw.temp );
//Log.verbose(F("GYRO: Result \t%d\t%d\t%d\t%d\t%d\t%d." CR), average.ax/noIterations, average.ay/noIterations, average.az/noIterations,
// average.gx/noIterations, average.gy/noIterations, average.gz/noIterations );
#endif
}
//
// Calcuate the angles (tilt)
//
double GyroSensor::calculateAngle(RawGyroData &raw) {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Calculating the angle." CR) );
#endif
// Source: https://www.nxp.com/docs/en/application-note/AN3461.pdf
double v = (acos( raw.ay / sqrt( raw.ax*raw.ax + raw.ay*raw.ay + raw.az*raw.az ) ) *180.0 / PI);
//Log.notice(F("GYRO: angle = %F." CR), v );
//double v = (acos( raw.az / sqrt( raw.ax*raw.ax + raw.ay*raw.ay + raw.az*raw.az ) ) *180.0 / PI);
//Log.notice(F("GYRO: angle = %F." CR), v );
#if LOG_LEVEL==6
Log.verbose(F("GYRO: angle = %F." CR), v );
#endif
return v;
}
//
// Check if the values are high that indicate that the sensor is moving.
//
bool GyroSensor::isSensorMoving(RawGyroData &raw) {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Checking for sensor movement." CR) );
#endif
int x = abs(raw.gx), y = abs(raw.gy), z = abs(raw.gz);
if( x>SENSOR_MOVING_THREASHOLD || y>SENSOR_MOVING_THREASHOLD || z>SENSOR_MOVING_THREASHOLD ) {
Log.notice(F("GYRO: Movement detected (%d)\t%d\t%d\t%d." CR), SENSOR_MOVING_THREASHOLD, x, y, z);
return true;
}
return false;
}
//
// Read the tilt angle from the gyro.
//
bool GyroSensor::read() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Getting new gyro position." CR) );
#endif
RawGyroData raw;
readSensor( raw, SENSOR_READ_COUNT, SENSOR_READ_DELAY );
// If the sensor is unstable we return false to signal we dont have valid value
if( isSensorMoving(raw) ) {
Log.notice(F("GYRO: Sensor is moving." CR) );
validValue = false;
} else {
validValue = true;
angle = calculateAngle( raw );
//Log.notice(F("GYRO: Calculated angle %F" CR), angle );
}
return validValue;
}
//
// Dump the stored calibration values.
//
void GyroSensor::dumpCalibration() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Accel offset\t%d\t%d\t%d" CR), calibrationOffset.ax, calibrationOffset.ay, calibrationOffset.az );
Log.verbose(F("GYRO: Gyro offset \t%d\t%d\t%d" CR), calibrationOffset.gx, calibrationOffset.gy, calibrationOffset.gz );
#endif
}
//
// Update the sensor with out calculated offsets.
//
void GyroSensor::applyCalibration() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Applying calibration offsets to sensor." CR) );
#endif
if( ( calibrationOffset.ax + calibrationOffset.ay + calibrationOffset.az + calibrationOffset.gx + calibrationOffset.gy + calibrationOffset.gz ) == 0 ) {
Log.error(F("GYRO: No valid calibraion values exist, aborting." CR) );
return;
}
accelgyro.setXAccelOffset( calibrationOffset.ax );
accelgyro.setYAccelOffset( calibrationOffset.ay );
accelgyro.setZAccelOffset( calibrationOffset.az );
accelgyro.setXGyroOffset( calibrationOffset.gx );
accelgyro.setYGyroOffset( calibrationOffset.gy );
accelgyro.setZGyroOffset( calibrationOffset.gz );
}
//
// Calculate the offsets for calibration.
//
void GyroSensor::calibrateSensor() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Calibrating sensor" CR) );
#endif
//accelgyro.PrintActiveOffsets();
//Serial.print( CR );
accelgyro.setDLPFMode(MPU6050_DLPF_BW_5);
accelgyro.CalibrateAccel(6); // 6 = 600 readings
accelgyro.CalibrateGyro(6);
accelgyro.PrintActiveOffsets();
Serial.print( CR );
calibrationOffset.ax = accelgyro.getXAccelOffset();
calibrationOffset.ay = accelgyro.getYAccelOffset();
calibrationOffset.az = accelgyro.getZAccelOffset();
calibrationOffset.gx = accelgyro.getXGyroOffset();
calibrationOffset.gy = accelgyro.getYGyroOffset();
calibrationOffset.gz = accelgyro.getZGyroOffset();
// Save the calibrated values
myConfig.setGyroCalibration( calibrationOffset );
myConfig.saveFile();
}
//
// Calibrate the device.
//
void GyroSensor::debug() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Debug - Clock src %d." CR), accelgyro.getClockSource() );
Log.verbose(F("GYRO: Debug - Device ID %d." CR), accelgyro.getDeviceID() );
Log.verbose(F("GYRO: Debug - DHPF Mode %d." CR), accelgyro.getDHPFMode() );
Log.verbose(F("GYRO: Debug - DMP on %s." CR), accelgyro.getDMPEnabled()?"on":"off" );
Log.verbose(F("GYRO: Debug - Acc range %d." CR), accelgyro.getFullScaleAccelRange() );
Log.verbose(F("GYRO: Debug - Gyr range %d." CR), accelgyro.getFullScaleGyroRange() );
Log.verbose(F("GYRO: Debug - Int %s." CR), accelgyro.getIntEnabled()?"on":"off" );
Log.verbose(F("GYRO: Debug - Clock %d." CR), accelgyro.getMasterClockSpeed() );
Log.verbose(F("GYRO: Debug - Rate %d." CR), accelgyro.getRate() );
Log.verbose(F("GYRO: Debug - Gyro range %d." CR), accelgyro.getFullScaleGyroRange() );
// Log.verbose(F("GYRO: Debug - I2C bypass %s." CR), accelgyro.getI2CBypassEnabled()?"on":"off" );
// Log.verbose(F("GYRO: Debug - I2C master %s." CR), accelgyro.getI2CMasterModeEnabled()?"on":"off" );
Log.verbose(F("GYRO: Debug - Acc FactX %d." CR), accelgyro.getAccelXSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Acc FactY %d." CR), accelgyro.getAccelYSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Acc FactZ %d." CR), accelgyro.getAccelZSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Gyr FactX %d." CR), accelgyro.getGyroXSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Gyr FactY %d." CR), accelgyro.getGyroYSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Gyr FactZ %d." CR), accelgyro.getGyroZSelfTestFactoryTrim() );
switch( accelgyro.getFullScaleAccelRange() ) {
case 0:
Log.verbose(F("GYRO: Debug - Accel range +/- 2g." CR));
break;
case 1:
Log.verbose(F("GYRO: Debug - Accel range +/- 4g." CR));
break;
case 2:
Log.verbose(F("GYRO: Debug - Accel range +/- 8g." CR));
break;
case 3:
Log.verbose(F("GYRO: Debug - Accel range +/- 16g." CR));
break;
}
Log.verbose(F("GYRO: Debug - Acc OffX %d\t%d." CR), accelgyro.getXAccelOffset(), calibrationOffset.az );
Log.verbose(F("GYRO: Debug - Acc OffY %d\t%d." CR), accelgyro.getYAccelOffset(), calibrationOffset.ay );
Log.verbose(F("GYRO: Debug - Acc OffZ %d\t%d." CR), accelgyro.getZAccelOffset(), calibrationOffset.az );
Log.verbose(F("GYRO: Debug - Gyr OffX %d\t%d." CR), accelgyro.getXGyroOffset(), calibrationOffset.gx );
Log.verbose(F("GYRO: Debug - Gyr OffY %d\t%d." CR), accelgyro.getYGyroOffset(), calibrationOffset.gy );
Log.verbose(F("GYRO: Debug - Gyr OffZ %d\t%d." CR), accelgyro.getZGyroOffset(), calibrationOffset.gz );
#endif
}
// EOF

79
src/gyro.h Normal file
View File

@ -0,0 +1,79 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef _GYRO_H
#define _GYRO_H
#define I2CDEV_IMPLEMENTATION I2CDEV_ARDUINO_WIRE
//#define I2CDEV_IMPLEMENTATION I2CDEV_BUILTIN_SBWIRE
// Includes
#include <arduino.h>
#include "MPU6050.h"
#include "config.h"
// Classes
struct RawGyroDataL { // Used for average multiple readings
long ax; // Raw Acceleration
long ay;
long az;
long gx; // Raw Position
long gy;
long gz;
long temp; // Only for information (temperature of chip)
};
class GyroSensor {
private:
MPU6050 accelgyro;
bool sensorConnected = false;
bool validValue = false;
double angle = 0;
RawGyroData calibrationOffset;
void debug();
void applyCalibration();
void dumpCalibration();
void readSensor(RawGyroData &raw, const int noIterations = 100, const int delayTime = 1);
bool isSensorMoving(RawGyroData &raw);
double calculateAngle(RawGyroData &raw);
public:
bool setup();
bool read();
void calibrateSensor();
double getAngle() { return angle; };
bool isConnected() { return sensorConnected; };
bool hasValue() { return validValue; };
void enterSleep();
};
// Global instance created
extern GyroSensor myGyro;
#endif // _GYRO_H
// EOF

110
src/helper.cpp Normal file
View File

@ -0,0 +1,110 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "helper.h"
#include "config.h"
SerialDebug mySerial;
BatteryVoltage myBatteryVoltage;
//
// Enter deep sleep for the defined duration (Argument is seconds)
//
void deepSleep(int t) {
#if LOG_LEVEL==6
Log.verbose(F("HELP: Entering sleep mode for %ds." CR), t );
#endif
uint64_t wake = t * 1000000;
ESP.deepSleep( wake );
}
//
// Print the build options used
//
void printBuildOptions() {
Log.notice( F("Build options: %s LOGLEVEL %d "
#ifdef ACTIVATE_PUSH
"PUSH "
#endif
#ifdef SKIP_SLEEPMODE
"SKIP_SLEEP "
#endif
#ifdef ACTIVATE_OTA
"OTA "
#endif
CR), CFG_APPVER, LOG_LEVEL );
}
//
// Configure serial debug output
//
SerialDebug::SerialDebug(const long serialSpeed) {
// Start serial with auto-detected rate (default to defined BAUD)
Serial.flush();
Serial.begin(serialSpeed);
getLog()->begin(LOG_LEVEL, &Serial, true);
getLog()->setPrefix(printTimestamp);
getLog()->notice(F("SDBG: Serial logging started at %l." CR), serialSpeed);
}
//
// Print the timestamp (ms since start of device)
//
void printTimestamp(Print* _logOutput) {
char c[12];
sprintf(c, "%10lu ", millis());
_logOutput->print(c);
}
//
// Read and calculate the battery voltage
//
void BatteryVoltage::read() {
// The analog pin can only handle 3.3V maximum voltage so we need to reduce the voltage (from max 5V)
float factor = myConfig.getVoltageFactor(); // Default value is 1.63
int v = analogRead( A0 );
batteryLevel = ((3.3/1023)*v)*factor;
#if LOG_LEVEL==6
Log.verbose(F("BATT: Reading voltage level. Factor=%F Value=%d, Voltage=%F." CR), factor, v, batteryLevel );
#endif
}
//
// Convert float to formatted string with n decimals. Buffer should be at least 10 chars.
//
char* convertFloatToString( float f, char *buffer, int dec ) {
dtostrf(f, 6, dec, buffer);
return buffer;
}
//
// Reduce precision to n decimals
//
float reduceFloatPrecision( float f, int dec ) {
char buffer[5];
dtostrf(f, 6, dec, &buffer[0]);
return atof(&buffer[0]);
}
// EOF

66
src/helper.h Normal file
View File

@ -0,0 +1,66 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef _HELPER_H
#define _HELPER_H
// Includes
#include <ArduinoLog.h>
// Sleep mode
void deepSleep(int t);
// Show build options
void printBuildOptions();
// Float to String
char* convertFloatToString( float f, char* buf, int dec = 2);
float reduceFloatPrecision( float f, int dec = 2 );
// Logging via serial
void printTimestamp(Print* _logOutput);
void printNewline(Print* _logOutput);
// Classes
class SerialDebug {
public:
SerialDebug(const long serialSpeed = 115200L);
static Logging* getLog() { return &Log; };
};
class BatteryVoltage {
private:
float batteryLevel;
public:
void read();
float getVoltage() { return batteryLevel; };
};
// Global instance created
extern SerialDebug mySerial;
extern BatteryVoltage myBatteryVoltage;
#endif // _HELPER_H
// EOF

214
src/main.cpp Normal file
View File

@ -0,0 +1,214 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "helper.h"
#include "gyro.h"
#include "config.h"
#include "wifi.h"
#include "webserver.h"
#include "calc.h"
#include "tempsensor.h"
#include "pushtarget.h"
#include <LittleFS.h>
// Settings for double reset detector.
#define ESP_MRD_USE_LITTLEFS true
#define ESP_MRD_USE_SPIFFS false
#define ESP_MRD_USE_EEPROM false
#define MRD_TIMES 3
#define MRD_TIMEOUT 10
#define MRD_ADDRESS 0
#define MULTIRESETDETECTOR_DEBUG true
#include <ESP_MultiResetDetector.h>
MultiResetDetector *mrd;
// Define constats for this program
#if LOG_LEVEL==6
const int interval = 1000; // ms, time to wait between changes to output
bool sleepModeAlwaysSkip = true; // Web interface can override normal behaviour
#else
const int interval = 100; // ms, time to wait between changes to output
bool sleepModeAlwaysSkip = false; // Web interface can override normal behaviour
#endif
unsigned long lastMillis = 0;
unsigned long startMillis;
bool sleepModeActive = false;
//
// Check if we should be in sleep mode
//
void checkSleepMode( float angle, float volt ) {
#if defined( SKIP_SLEEPMODE )
sleepModeActive = false;
Log.verbose(F("MAIN: Skipping sleep mode (SKIP_SLEEPMODE is defined)." CR) );
return;
#endif
const RawGyroData &g = myConfig.getGyroCalibration();
// Will not enter sleep mode if: no calibration data
if( g.ax==0 && g.ay==0 && g.az==0 && g.gx==0 && g.gy==0 && g.gz==0 ) {
Log.notice(F("MAIN: Missing calibration data, so forcing webserver to be active." CR) );
sleepModeAlwaysSkip = true;
}
if( sleepModeAlwaysSkip ) {
Log.notice(F("MAIN: Sleep mode disabled from web interface." CR) );
sleepModeActive = false;
return;
}
// Will not enter sleep mode if: charger is connected
sleepModeActive = volt<4.15 ? true : false;
// sleep mode active when flat
//sleepModeActive = ( angle<85 && angle>5 ) ? true : false;
#if LOG_LEVEL==6
Log.verbose(F("MAIN: Deep sleep mode %s (angle=%F volt=%F)." CR), sleepModeActive ? "true":"false", angle, volt );
#endif
}
//
// Setup
//
void setup() {
startMillis = millis();
mrd = new MultiResetDetector(MRD_TIMEOUT, MRD_ADDRESS);
bool dt = mrd->detectMultiReset();
#if LOG_LEVEL==6
Log.verbose(F("Main: Reset reason %s." CR), ESP.getResetInfo().c_str() );
#endif
// Main startup
Log.notice(F("Main: Started setup for %s." CR), String( ESP.getChipId(), HEX).c_str() );
printBuildOptions();
Log.notice(F("Main: Loading configuration." CR));
myConfig.checkFileSystem();
myConfig.loadFile();
// Setup watchdog
ESP.wdtDisable();
ESP.wdtEnable( interval*2 );
myTempSensor.setup();
// Setup Gyro
if( !myGyro.setup() )
Log.error(F("Main: Failed to initialize the gyro." CR));
if( dt )
Log.notice(F("Main: Detected doubletap on reset." CR));
Log.notice(F("Main: Connecting to wifi." CR));
myWifi.connect( dt );
Log.notice(F("Main: WIFI connected." CR));
myGyro.read();
myBatteryVoltage.read();
checkSleepMode( myGyro.getAngle(), myBatteryVoltage.getVoltage() );
if( myWifi.isConnected() ) {
Log.notice(F("Main: Connected to wifi ip=%s." CR), myWifi.getIPAddress().c_str() );
if( !sleepModeActive )
if( myWebServer.setupWebServer() )
Log.notice(F("Main: Webserver is running." CR) );
}
#if defined( ACTIVATE_OTA )
if( !sleepModeActive && myWifi.isConnected() && myWifi.checkFirmwareVersion() ) {
myWifi.updateFirmware();
}
#endif
}
//
// Main loops
//
void loop() {
mrd->loop();
if( sleepModeActive || abs(millis() - lastMillis) > interval ) {
float angle = 90;
float volt = myBatteryVoltage.getVoltage();
#if LOG_LEVEL==6
Log.verbose(F("Main: Entering main loop." CR) );
#endif
// If we dont get any readings we just skip this and try again the next interval.
if( myGyro.hasValue() ) {
angle = myGyro.getAngle();
float temp = myTempSensor.getValueCelcius(); // The code is build around using C for temp.
float gravity = calculateGravity( angle, temp );
#if LOG_LEVEL==6
Log.verbose(F("Main: Sensor values gyro=%F, temp=%F, gravity=%F." CR), angle, temp, gravity );
#endif
if( myConfig.isGravityTempAdj() ) {
gravity = gravityTemperatureCorrection( gravity, temp); // Use default correction temperature of 20C
#if LOG_LEVEL==6
Log.verbose(F("Main: Temp adjusted gravity=%F." CR), gravity );
#endif
}
Log.notice(F("Main: Gyro angle=%F, temp=%F, gravity=%F, batt=%F." CR), angle, temp, gravity, volt );
#if defined( ACTIVATE_PUSH )
unsigned long runTime = millis() - startMillis;
myPushTarget.send( angle, gravity, temp, runTime/1000, sleepModeActive ); // Force the transmission if we are going to sleep
#endif
} else {
Log.error(F("Main: No gyro value." CR) );
}
if( sleepModeActive ) {
unsigned long runTime = millis() - startMillis;
// Enter sleep mode...
Log.notice(F("MAIN: Entering deep sleep, run time %l s." CR), runTime/1000 );
LittleFS.end();
myGyro.enterSleep();
mrd->stop();
delay(100);
deepSleep( myConfig.getPushInterval() );
}
#if LOG_LEVEL==6
Log.verbose(F("Main: Sleep mode not active." CR) );
#endif
// Do these checks if we are running in normal mode (not sleep mode)
checkSleepMode( angle, volt );
myGyro.read();
myBatteryVoltage.read();
lastMillis = millis();
#if LOG_LEVEL==6
Log.verbose(F("Main: Heap %d kb FreeSketch %d kb." CR), ESP.getFreeHeap()/1024, ESP.getFreeSketchSpace()/1024 );
Log.verbose(F("Main: HeapFrag %d %%." CR), ESP.getHeapFragmentation() );
#endif
}
myWebServer.loop();
}
// EOF

170
src/pushtarget.cpp Normal file
View File

@ -0,0 +1,170 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "pushtarget.h"
#include "config.h"
#if defined( ACTIVATE_PUSH )
PushTarget myPushTarget;
//
// Send the pressure value
//
void PushTarget::send(float angle, float gravity, float temp, float runTime, bool force ) {
unsigned long timePassed = abs( millis() - ms );
unsigned long interval = myConfig.getPushInterval()*1000;
if( ( timePassed < interval ) && !force) {
#if LOG_LEVEL==6
Log.verbose(F("PUSH: Timer has not expired %l vs %l." CR), timePassed, interval );
#endif
return;
}
#if LOG_LEVEL==6
Log.verbose(F("PUSH: Sending data." CR) );
#endif
ms = millis();
if( myConfig.isBrewfatherActive() )
sendBrewfather( angle, gravity, temp );
if( myConfig.isHttpActive() )
sendHttp( angle, gravity, temp, runTime );
}
//
// Send data to brewfather
//
void PushTarget::sendBrewfather(float angle, float gravity, float temp ) {
Log.notice(F("PUSH: Sending values to brewfather angle=%F, gravity=%F, temp=%F." CR), angle, gravity, temp );
DynamicJsonDocument doc(300);
//
// {
// "name": "YourDeviceName", // Required field, this will be the ID in Brewfather
// "temp": 20.32,
// "aux_temp": 15.61, // Fridge Temp
// "ext_temp": 6.51, // Room Temp
// "temp_unit": "C", // C, F, K
// "gravity": 1.042,
// "gravity_unit": "G", // G, P
// "pressure": 10,
// "pressure_unit": "PSI", // PSI, BAR, KPA
// "ph": 4.12,
// "bpm": 123, // Bubbles Per Minute
// "comment": "Hello World",
// "beer": "Pale Ale"
// "battery": 4.98
// }
//
doc["name"] = myConfig.getMDNS();
doc["temp"] = reduceFloatPrecision( temp, 1);
//doc["aux_temp"] = 0;
//doc["ext_temp"] = 0;
doc["temp_unit"] = String( myConfig.getTempFormat() );
//doc["pressure"] = ;
//doc["pressure_unit"] = ;
doc["battery"] = reduceFloatPrecision( myBatteryVoltage.getVoltage(), 2 );
doc["gravity"] = reduceFloatPrecision( gravity, 4 );
doc["gravity_unit"] = myConfig.isGravitySG()?"G":"P";
//doc["ph"] = 0;
//doc["bpm"] = 0;
//doc["comment"] = "";
//doc["beer"] = "";
WiFiClient client;
HTTPClient http;
String serverPath = myConfig.getBrewfatherPushTarget();
// Your Domain name with URL path or IP address with path
http.begin( client, serverPath);
String json;
serializeJson(doc, json);
#if LOG_LEVEL==6
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), json.c_str());
#endif
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json") );
int httpResponseCode = http.POST(json);
if (httpResponseCode==200) {
Log.notice(F("PUSH: HTTP Response code %d" CR), httpResponseCode);
} else {
Log.error(F("PUSH: HTTP Response code %d" CR), httpResponseCode);
}
http.end();
}
//
// Send data to http target
//
void PushTarget::sendHttp(float angle, float gravity, float temp, float runTime ) {
Log.notice(F("PUSH: Sending values to http angle=%F, gravity=%F, temp=%F." CR), angle, gravity, temp );
DynamicJsonDocument doc(256);
doc["name"] = myConfig.getMDNS();
doc["temp"] = reduceFloatPrecision( temp, 1 );
doc["temp-unit"] = String( myConfig.getTempFormat() );
doc["gravity"] = reduceFloatPrecision( gravity, 4 );
doc["angle"] = reduceFloatPrecision( angle, 2);
doc["battery"] = reduceFloatPrecision( myBatteryVoltage.getVoltage(), 2 );
doc["rssi"] = WiFi.RSSI();
// Some debug information
doc["run-time"] = reduceFloatPrecision( runTime, 2 );
WiFiClient client;
HTTPClient http;
String serverPath = myConfig.getHttpPushTarget();
// Your Domain name with URL path or IP address with path
http.begin( client, serverPath);
String json;
serializeJson(doc, json);
#if LOG_LEVEL==6
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), json.c_str());
#endif
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json") );
int httpResponseCode = http.POST(json);
if (httpResponseCode==200) {
Log.notice(F("PUSH: HTTP Response code %d" CR), httpResponseCode);
} else {
Log.error(F("PUSH: HTTP Response code %d" CR), httpResponseCode);
}
http.end();
}
#endif // ACTIVATE_PUSH
// EOF

52
src/pushtarget.h Normal file
View File

@ -0,0 +1,52 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef _PUSHTARGET_H
#define _PUSHTARGET_H
// Includes
#include "helper.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
// Classes
class PushTarget {
private:
unsigned long ms; // Used to check that we do not post to often
void sendBrewfather(float angle, float gravity, float temp );
void sendHttp(float angle, float gravity, float temp, float runTime );
public:
PushTarget() { ms = millis(); }
void send(float angle, float gravity, float temp, float runTime, bool force = false );
};
extern PushTarget myPushTarget;
#endif // _PUSHTARGET_H
// EOF

41
src/resources.cpp Normal file
View File

@ -0,0 +1,41 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <incbin.h>
#if defined( EMBED_HTML )
/*INCBIN(IndexHtm, "data/index.htm" );
INCBIN(DeviceHtm, "data/device.htm" );
INCBIN(ConfigHtm, "data/config.htm" );
INCBIN(AboutHtm, "data/about.htm" );*/
// Using minify to reduce memory usage. Reducing RAM memory usage with about 7%
INCBIN(IndexHtm, "data/index.min.htm" );
INCBIN(DeviceHtm, "data/device.min.htm" );
INCBIN(ConfigHtm, "data/config.min.htm" );
INCBIN(AboutHtm, "data/about.min.htm" );
#endif
// EOF

108
src/tempsensor.cpp Normal file
View File

@ -0,0 +1,108 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "tempsensor.h"
#include "helper.h"
#include "config.h"
#include <onewire.h>
#include <DallasTemperature.h>
#include <Wire.h>
//
// Conversion between C and F
//
float convertCtoF( float t ) {
return (t * 1.8 ) + 32.0;
}
OneWire myOneWire(D6);
DallasTemperature mySensors(&myOneWire);
TempSensor myTempSensor;
#define TEMPERATURE_PRECISION 9
//
// Setup temp sensors
//
void TempSensor::setup() {
#if defined( SIMULATE_TEMP )
hasSensors = true;
return;
#endif
if( mySensors.getDeviceCount() )
return;
#if LOG_LEVEL==6
Log.verbose(F("TSEN: Looking for temp sensors." CR));
#endif
mySensors.begin();
if( mySensors.getDeviceCount() ) {
Log.notice(F("TSEN: Found %d sensors." CR), mySensors.getDeviceCount());
mySensors.setResolution(TEMPERATURE_PRECISION);
}
float t = myConfig.getTempSensorAdj();
// Set the temp sensor adjustment values
if( myConfig.isTempC() ) {
tempSensorAdjF = t * 1.8; // Convert the adjustment value to C
tempSensorAdjC = t;
} else {
tempSensorAdjF = t;
tempSensorAdjC = t * 0.556; // Convert the adjustent value to F
}
#if LOG_LEVEL==6
Log.verbose(F("TSEN: Adjustment values for temp sensor %F C, %F F." CR), tempSensorAdjC, tempSensorAdjF );
#endif
}
//
// Retrieving value from sensor
//
float TempSensor::getValue() {
float c = 0;
#if defined( SIMULATE_TEMP )
return 21;
#endif
// Read the sensors
mySensors.requestTemperatures();
if( mySensors.getDeviceCount() >= 1) {
c = mySensors.getTempCByIndex(0);
#if LOG_LEVEL==6
Log.verbose(F("TSEN: Reciving temp value for sensor %F C." CR), c);
#endif
hasSensor = true;
}
return c;
}
// EOF

50
src/tempsensor.h Normal file
View File

@ -0,0 +1,50 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef _TEMPSENSOR_H
#define _TEMPSENSOR_H
// definitions
float convertCtoF( float t );
// classes
class TempSensor {
private:
bool hasSensor = false;
float tempSensorAdjF = 0;
float tempSensorAdjC = 0;
float getValue();
public:
void setup();
bool isSensorAttached() { return hasSensor; };
float getValueCelcius() { return getValue() + tempSensorAdjC; }
float getValueFarenheight() { return convertCtoF(getValue()) + tempSensorAdjF; };
};
// Global instance created
extern TempSensor myTempSensor;
#endif // _TEMPSENSOR_H
// EOF

382
src/webserver.cpp Normal file
View File

@ -0,0 +1,382 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
//#define DEBUG_ESP_HTTP_SERVER
#include "webserver.h"
#include "config.h"
#include "helper.h"
#include "gyro.h"
#include "calc.h"
#include "tempsensor.h"
#include <ArduinoJson.h>
#include <incbin.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <LittleFS.h>
// Binary resouces
#if defined( EMBED_HTML )
INCBIN_EXTERN(IndexHtm);
INCBIN_EXTERN(DeviceHtm);
INCBIN_EXTERN(ConfigHtm);
INCBIN_EXTERN(AboutHtm);
#endif
WebServer myWebServer;
ESP8266WebServer server(80);
extern bool sleepModeActive;
extern bool sleepModeAlwaysSkip;
//
// Callback from webServer when / has been accessed.
//
void webHandleDevice() {
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config." CR));
#endif
DynamicJsonDocument doc(100);
doc[ CFG_PARAM_ID ] = myConfig.getID();
doc[ CFG_PARAM_APP_NAME ] = CFG_APPNAME;
doc[ CFG_PARAM_APP_VER ] = CFG_APPVER;
doc[ CFG_PARAM_MDNS ] = myConfig.getMDNS();
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
String out;
serializeJson(doc, out);
server.send(200, "application/json", out.c_str() );
}
//
// Callback from webServer when / has been accessed.
//
void webHandleConfig() {
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config." CR));
#endif
DynamicJsonDocument doc(CFG_JSON_BUFSIZE);
myConfig.createJson( doc );
double angle = myGyro.getAngle();
double temp = myTempSensor.getValueCelcius();
double gravity = calculateGravity( angle, temp );
doc[ CFG_PARAM_ANGLE ] = reduceFloatPrecision( angle);
doc[ CFG_PARAM_GRAVITY ] = reduceFloatPrecision( gravityTemperatureCorrection( gravity, temp ), 4);
doc[ CFG_PARAM_BATTERY ] = reduceFloatPrecision( myBatteryVoltage.getVoltage());
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
String out;
serializeJson(doc, out);
server.send(200, "application/json", out.c_str() );
}
//
// Callback from webServer when / has been accessed.
//
void webHandleCalibrate() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/calibrate." CR));
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
myGyro.calibrateSensor();
server.send(200, "text/plain", "Device calibrated" );
}
//
// Callback from webServer when / has been accessed.
//
void webHandleFactoryReset() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/factory." CR));
#endif
if( !id.compareTo( myConfig.getID() ) ) {
server.send(200, "text/plain", "Doing reset...");
LittleFS.remove(CFG_FILENAME);
LittleFS.end();
delay(500);
ESP.reset();
} else {
server.send(400, "text/plain", "Unknown ID.");
}
}
//
// Callback from webServer when / has been accessed.
//
void webHandleStatus() {
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/status." CR));
#endif
DynamicJsonDocument doc(256);
double angle = myGyro.getAngle();
double temp = myTempSensor.getValueCelcius();
double gravity = calculateGravity( angle, temp );
doc[ CFG_PARAM_ID ] = myConfig.getID();
doc[ CFG_PARAM_ANGLE ] = reduceFloatPrecision( angle);
doc[ CFG_PARAM_GRAVITY ] = reduceFloatPrecision( gravityTemperatureCorrection( gravity, temp ), 4);
doc[ CFG_PARAM_TEMP_C ] = reduceFloatPrecision( temp, 1);
doc[ CFG_PARAM_TEMP_F ] = reduceFloatPrecision( myTempSensor.getValueFarenheight(), 1);
doc[ CFG_PARAM_BATTERY ] = reduceFloatPrecision( myBatteryVoltage.getVoltage());
doc[ CFG_PARAM_TEMPFORMAT ] = String( myConfig.getTempFormat() );
doc[ CFG_PARAM_SLEEP_MODE ] = sleepModeAlwaysSkip;
doc[ CFG_PARAM_RSSI ] = WiFi.RSSI();
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
String out;
serializeJson(doc, out);
server.send(200, "application/json", out.c_str() );
}
//
// Callback from webServer when / has been accessed.
//
void webHandleClearWIFI() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/clearwifi." CR));
#endif
if( !id.compareTo( myConfig.getID() ) ) {
server.send(200, "text/plain", "Clearing WIFI credentials and doing reset...");
delay(1000);
WiFi.disconnect(); // Clear credentials
ESP.reset();
} else {
server.send(400, "text/plain", "Unknown ID.");
}
}
//
// Used to force the device to never sleep.
//
void webHandleStatusSleepmode() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/status/sleepmode." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : sleep-mode=%s." CR), server.arg( CFG_PARAM_SLEEP_MODE ).c_str() );
#endif
if( server.arg( CFG_PARAM_SLEEP_MODE ).equalsIgnoreCase( "true" ) )
sleepModeAlwaysSkip = true;
else
sleepModeAlwaysSkip = false;
server.send(200, "text/plain", "Sleep mode updated" );
}
//
// Update device settings.
//
void webHandleConfigDevice() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config/device." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : mdns=%s, temp-format=%s." CR), server.arg( CFG_PARAM_MDNS ).c_str(), server.arg( CFG_PARAM_TEMPFORMAT ).c_str() );
#endif
myConfig.setMDNS( server.arg( CFG_PARAM_MDNS ).c_str() );
myConfig.setTempFormat( server.arg( CFG_PARAM_TEMPFORMAT ).charAt(0) );
myConfig.saveFile();
server.sendHeader("Location", "/config.htm#collapseOne", true);
server.send(302, "text/plain", "Device config updated" );
}
//
// Update push settings.
//
void webHandleConfigPush() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config/push." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : http=%s, bf=%s interval=%s." CR), server.arg( CFG_PARAM_PUSH_HTTP ).c_str(), server.arg( CFG_PARAM_PUSH_BREWFATHER ).c_str(), server.arg( CFG_PARAM_PUSH_INTERVAL ).c_str() );
#endif
myConfig.setHttpPushTarget( server.arg( CFG_PARAM_PUSH_HTTP ).c_str() );
myConfig.setBrewfatherPushTarget( server.arg( CFG_PARAM_PUSH_BREWFATHER ).c_str() );
myConfig.setPushInterval( server.arg( CFG_PARAM_PUSH_INTERVAL ).c_str() );
myConfig.saveFile();
server.sendHeader("Location", "/config.htm#collapseTwo", true);
server.send(302, "text/plain", "Push config updated" );
}
//
// Update gravity settings.
//
void webHandleConfigGravity() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config/gravity." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : formula=%s, temp-corr=%s." CR), server.arg( CFG_PARAM_GRAVITY_FORMULA ).c_str(), server.arg( CFG_PARAM_GRAVITY_TEMP_ADJ ).c_str() );
#endif
myConfig.setGravityFormula( server.arg( CFG_PARAM_GRAVITY_FORMULA ).c_str() );
myConfig.setGravityTempAdj( server.arg( CFG_PARAM_GRAVITY_TEMP_ADJ ).equalsIgnoreCase( "on" ) ? true:false);
myConfig.saveFile();
server.sendHeader("Location", "/config.htm#collapseThree", true);
server.send(302, "text/plain", "Gravity config updated" );
}
//
// Update hardware settings.
//
void webHandleConfigHardware() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config/hardware." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : vf=%s, tempadj=%s, ota=%s." CR), server.arg( CFG_PARAM_VOLTAGEFACTOR ).c_str(), server.arg( CFG_PARAM_TEMP_ADJ ).c_str(), server.arg( CFG_PARAM_OTA ).c_str() );
#endif
myConfig.setVoltageFactor( server.arg( CFG_PARAM_VOLTAGEFACTOR ).toFloat() );
myConfig.setTempSensorAdj( server.arg( CFG_PARAM_TEMP_ADJ ).toFloat() );
myConfig.setOtaURL( server.arg( CFG_PARAM_OTA ).c_str() );
myConfig.saveFile();
server.sendHeader("Location", "/config.htm#collapseFour", true);
server.send(302, "text/plain", "Hardware config updated" );
}
//
// Setup the Web Server callbacks and start it
//
bool WebServer::setupWebServer() {
#if LOG_LEVEL==6
Log.verbose(F("WEB : Setting up web server." CR));
#endif
Log.notice(F("WEB : Web server setup started." CR));
MDNS.begin( myConfig.getMDNS() );
MDNS.addService("http", "tcp", 80);
// Static content
#if defined( EMBED_HTML )
server.on("/",[]() {
server.send_P(200, "text/html", (const char*) gIndexHtmData, gIndexHtmSize );
} );
server.on("/index.htm",[]() {
server.send_P(200, "text/html", (const char*) gIndexHtmData, gIndexHtmSize );
} );
server.on("/device.htm",[]() {
server.send_P(200, "text/html", (const char*) gDeviceHtmData, gDeviceHtmSize );
} );
server.on("/config.htm",[]() {
server.send_P(200, "text/html", (const char*) gConfigHtmData, gConfigHtmSize );
} );
server.on("/about.htm",[]() {
server.send_P(200, "text/html", (const char*) gAboutHtmData, gAboutHtmSize );
} );
#else
// Show files in the filessytem at startup
FSInfo fs;
LittleFS.info(fs);
Log.notice( F("File system: Total=%d, Used=%d." CR), fs.totalBytes, fs.usedBytes );
Dir dir = LittleFS.openDir("/");
while( dir.next() ) {
Log.notice( F("File: %s, %d bytes" CR), dir.fileName().c_str(), dir.fileSize() );
}
server.serveStatic("/", LittleFS, "/index.htm" );
server.serveStatic("/index.htm", LittleFS, "/index.htm" );
server.serveStatic("/device.htm", LittleFS, "/device.htm" );
server.serveStatic("/config.htm", LittleFS, "/config.htm" );
server.serveStatic("/about.htm", LittleFS, "/about.htm" );
#endif
// Dynamic content
server.on("/api/config", webHandleConfig); // Get config.json
server.on("/api/device", webHandleDevice); // Get device.json
server.on("/api/calibrate", webHandleCalibrate); // Run calibration routine (param id)
server.on("/api/factory", webHandleFactoryReset); // Reset the device
server.on("/api/status", webHandleStatus); // Get the status.json
server.on("/api/clearwifi", webHandleClearWIFI); // Clear wifi settings
server.on("/api/status/sleepmode", webHandleStatusSleepmode);
server.on("/api/config/device", webHandleConfigDevice);
server.on("/api/config/push", webHandleConfigPush);
server.on("/api/config/gravity", webHandleConfigGravity);
server.on("/api/config/hardware", webHandleConfigHardware);
server.onNotFound( []() {
Log.error(F("WEB : URL not found %s received." CR), server.uri().c_str());
server.send(404, "text/plain", F("URL not found") );
} );
server.begin();
Log.notice(F("WEB : Web server started." CR));
return true;
}
//
// called from main loop
//
void WebServer::loop() {
// Dont put serial debug output in this call
server.handleClient();
MDNS.update();
}
// EOF

41
src/webserver.h Normal file
View File

@ -0,0 +1,41 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef _WEBSERVER_H
#define _WEBSERVER_H
// Include
// classes
class WebServer {
public:
bool setupWebServer();
void loop();
};
// Global instance created
extern WebServer myWebServer;
#endif // _WEBSERVER_H
// EOF

203
src/wifi.cpp Normal file
View File

@ -0,0 +1,203 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "wifi.h"
#include "config.h"
#include "helper.h"
#include "gyro.h"
#include "calc.h"
#include "tempsensor.h"
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ArduinoJson.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include <LittleFS.h>
#include <incbin.h>
Wifi myWifi;
WiFiManager myWifiManager;
// TODO: ADD MDNS setting to WIFI portal.....
// TODO: Download html files during OTA update to reduce image size.
//
// Connect to last known access point or create one if connection is not working.
//
bool Wifi::connect( bool showPortal ) {
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Connecting to WIFI via connection manager (portal=%s)." CR), showPortal?"true":"false");
myWifiManager.setDebugOutput(true);
#else
myWifiManager.setDebugOutput(false);
#endif
unsigned long startMillis = millis();
myWifiManager.setConfigPortalTimeout( WIFI_PORTAL_TIMEOUT );
if( showPortal ) {
Log.notice(F("WIFI: Starting wifi portal." CR));
connectedFlag = myWifiManager.startConfigPortal( WIFI_DEFAULT_SSID, WIFI_DEFAULT_PWD );
}
else
connectedFlag = myWifiManager.autoConnect( WIFI_DEFAULT_SSID, WIFI_DEFAULT_PWD );
Log.notice( F("WIFI: Connect time %d s" CR), abs(millis() - startMillis)/1000);
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Connect returned %s." CR), connectedFlag?"True":"False" );
#endif
return connectedFlag;
}
//
// This will erase the stored credentials and forcing the WIFI manager to AP mode.
//
bool Wifi::disconnect() {
Log.notice(F("WIFI: Erasing stored WIFI credentials." CR));
// Erase WIFI credentials
return WiFi.disconnect();
}
#if defined( ACTIVATE_OTA )
//
//
//
bool Wifi::updateFirmware() {
if( !newFirmware ) {
Log.notice(F("WIFI: No newer version exist, skipping update." CR));
return false;
}
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Updating firmware." CR));
#endif
WiFiClient client;
String serverPath = myConfig.getOtaURL();
serverPath += "firmware.bin";
HTTPUpdateResult ret = ESPhttpUpdate.update(client, serverPath);
switch(ret) {
case HTTP_UPDATE_FAILED:
Log.error(F("WIFI: Updating failed %d, %s." CR), ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_NO_UPDATES:
break;
case HTTP_UPDATE_OK:
Log.notice("WIFI: Updated succesfull, rebooting." );
ESP.reset();
break;
}
return false;
}
//
// Check what firmware version is available over OTA
//
bool Wifi::checkFirmwareVersion() {
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Checking if new version exist." CR));
#endif
WiFiClient client;
HTTPClient http;
String serverPath = myConfig.getOtaURL();
serverPath += "version.json";
// Your Domain name with URL path or IP address with path
http.begin( client, serverPath);
// Send HTTP GET request
int httpResponseCode = http.GET();
if (httpResponseCode==200) {
Log.notice(F("WIFI: HTTP Response code %d" CR), httpResponseCode);
String payload = http.getString();
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Payload %s." CR), payload.c_str());
#endif
DynamicJsonDocument ver(256);
DeserializationError err = deserializeJson(ver, payload);
if( err ) {
Log.error(F("WIFI: Failed to parse json" CR));
} else {
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Project %s version %s." CR), ver["project"].as<char*>(), ver["version"].as<char*>());
#endif
int newVer[3];
int curVer[3];
if( parseFirmwareVersionString( newVer, (const char*) ver["version"] ) ) {
if( parseFirmwareVersionString( curVer, CFG_APPVER) ) {
#if LOG_LEVEL==6
Log.verbose(F("OTA : Checking New=%d.%d.%d Cur=%d.%d.%d" CR), newVer[0], newVer[1], newVer[2], curVer[0], curVer[1], curVer[2] );
#endif
// Compare major version
if( newVer[0] > curVer[0] )
newFirmware = true;
// Compare minor version
if( newVer[0] == curVer[0] && newVer[1] > curVer[1] )
newFirmware = true;
// Compare patch version
if( newVer[0] == curVer[0] && newVer[1] == curVer[1] && newVer[2] > curVer[2] )
newFirmware = true;
}
}
}
} else {
Log.error(F("WIFI: HTTP Response code %d" CR), httpResponseCode);
}
http.end();
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Found new version %s." CR), newFirmware?"true":"false");
#endif
return newFirmware;
}
//
// Parse a version string in the format M.m.p (eg. 1.2.10)
//
bool Wifi::parseFirmwareVersionString( int (&num)[3], const char *version ) {
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Parsing version number %s." CR), version);
#endif
char temp[80];
char *s;
char *p = &temp[0];
int i = 0;
strcpy( &temp[0], version );
// TODO: Do some error checking on the string, lenght etc.
while ((s = strtok_r(p, ".", &p)) != NULL) {
num[i++] = atoi( s );
}
return true;
}
#endif // ACTIVATE_OTA
// EOF

57
src/wifi.h Normal file
View File

@ -0,0 +1,57 @@
/*
MIT License
Copyright (c) 2021 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef _WIFI_H
#define _WIFI_H
// Include
#include <WiFiManager.h>
// classes
class Wifi {
private:
// WIFI
bool connectedFlag = false;
// OTA
bool newFirmware = false;
bool parseFirmwareVersionString( int (&num)[3], const char *version );
public:
// WIFI
bool connect( bool showPortal = false );
bool disconnect();
bool isConnected() { return connectedFlag; };
String getIPAddress() { return WiFi.localIP().toString(); };
// OTA
bool updateFirmware();
bool checkFirmwareVersion();
};
// Global instance created
extern Wifi myWifi;
#endif // _WIFI_H
// EOF