//Wheelieometer //Use an accelerometer to show instantaneous angle, max angle and time >15 degrees //V.3 With averaging of measured angle //V.4 With yellow LED and weather station, commenting improved //V.5 Real time clock and rev counter (to measure speed and distance) support //V.6 Improved displays, added energy and power //V.7 Minor display improvements //V.8 Config settings for minimum angle, angle averaging and tilt //V.9 Added parameter save and load and large wheelie time and diagnostic display pages //V.10 Improved data load and save interface //V.11 Changed EEPROM.write to EEPROM.update //V.12 Enhancements to save and load //V.14 Autosave wear levelling, bugfix wheel sensor //V.15 Added display brightness option //V.16 Added option to not set clock during setup //V.18 Changed minimum brightness from 0 to 1a //V.19 Bug Fixes for save / load after reset //V.20 Count vertical angles < -90 as positive for wheelie detection //Remember to update SWVersion below! //Mount the board with the pins at front of bike parallel with bars and components uppermost //Note the reset button grounds the Arduino reset pin //I2C interface for accelerometer, display and weather station is on pins A4 (SDA) and A5 (SCL) on Arduino Nano, varies between Arduino model //Libraries used by the sketch #include //Read and write to non-volatile memory #include //Weather station library from https://github.com/Seeed-Studio/Grove_BME280 #include //I2C Interface functions #include //OLED Display text-only library - uses minimal memory resources https://github.com/greiman/SSD1306Ascii #include //OLED Display text-only library - extensions to support I2C interface #include //Real time clock library https://github.com/dtu-mekatronik/HCRTC //Constants used in code #define SWVersion 20 //Software Version //I2C Device Addresses #define I2COLedAdd 0x3C //I2C address for OLED display - may vary with manufacturer #define I2CAccAdd 0x53 //I2C address for accelerometer - may vary with manufacturer #define I2CDS1307Add 0x68 //I2C address for the real time clock - may vary with manufacturer //Accelerometer memory addresses #define GRangeAdd 0x31 //g range address of accelerometer - may vary with manufacturer #define GRangeSet 0x01 //g range value for +/-4g - may vary with manufacturer #define PowerSaveAdd 0x2D //Power save mode address of accelerometer - may vary with manufacturer #define PowerSaveMode 0x08 //Power save value for measurement mode - may vary with manufacturer #define XStartAdd 0x32 //X data start address for accelerometer - may vary with manufacturer //Hardware pins for buttons, LEDs and revolution sensor #define PinDisplayButton 5 //Display select button on pin D5 #define PinSaveButton 4 //Save/Load button on pin D4 #define PinLEDG 6 //Green LED anode on pin D6 - time exceeds previous maximum #define PinLEDY 7 //Yelllow LED anode on pin D7 - > MinAngle degrees in measurement mode #define PinRevSensor 8 //Reed switch on wheel to detect revolutions #define PinRevSensorPresent 9 //Switch contact on rev sensor socket, contact closed if plug not inserted //Timing #define DisplayUDInt 250 //Display update interval in in milliseconds #define SpeedAveragingTime 10000 //Speed measurement averaging time in in milliseconds #define MaxDisplayButtonInterval 500 //Maximum button press interval (ms), stops multiple triggering of button command actions due to fast running of code. 500ms is suitable, short enough not to be noticeable by user, but long enough to debounce #define MaxSaveButtonInterval 500 //Maximum button press interval (ms) #define MinRevSensorTime 100 //Minimum interval between rev sensor contact closures, for debounce to avoid multiple counts per rev. No minimum length of contact closure, could be very fast. 100ms limits maximum detectable speed to about 57 km/h (36mph) with a 20" wheel or 77km/h (48mph) with a 27" wheel #define SetupTimeout 5000 //Timeout to accept value in setup mode if no button pressed; half the value is used for confirmation messages #define AutoSaveIdleTime 60000 //Time (ms) stationary after which auto save occurs if values have changed; also mimimum time between subsequent saves. Reduces EEPROM wear. Note: always "stationary" if no wheel sensor, so minimuim time between saves important. //Wheelie parameters #define MinWheelieTime 500 //Minimum duration of new wheelie required before wiping previous value, in milliseconds //Display Modes #define WheelieDisplayModeAT 0 //Wheelie display - Angle and time #define WheelieDisplayModeTime 1 //Wheelie display - Big Time Display #define SpeedDisplayMode 2 //Big Speedometer display #define DistanceDisplayMode 3 //Big Distance display #define RideDisplayMode 4 //Ride stats display #define SpeedStatsDisplayMode 5 //Speed statistics display #define EnergyDisplayMode 6 //Calorie Count display #define PowerDisplayMode 7 //Metabolic Rate display #define ClockDisplayMode 8 //Clock Display #define WeatherDisplayMode 9 //Weather display #define DiagnosticDisplayMode 10 //Diagnostics display #define MaxDisplayMode 10 //Maximum available display mode, i.e. the maximum of those defined above //Wheel Size #define MinWheelSize 12 //Minimum wheel diameter in halves of an inch #define MaxWheelSize 60 //Maximum wheel diameter in halves of an inch #define WheelSizeEEPROMAddress 0 //Memory address where wheel size stored //Units of measurement #define SpeedKMH 0 //Speed in km/h #define SpeedMPH 1 //Speed in mph #define SpeedMS 2 //Speed in m/s #define SpeedUnitsMax 2 //Maximum value of above #define SpeedUnitsEEPROMAddress 1 //Memory address where speed units stored #define SpeedNamesLen 5 //Length of speed unit names, including null termination character #define DistNamesShortLen 3 //Length of short distance unit names, including null termination character #define DistNamesLongLen 3 //Length of long distance unit names, including null termination character #define DistMetric 0 //Distance in metric units #define DistImperial 1 //Distance in imperial units #define DistUnitsMax 1 //Maximum value of above #define DistShort 0 //Use short measurement for distance (e.g. metres) #define DistLong 1 //Use long measurement for distance (e.g. kilometres) #define TimeShort 0 //Use short measurement for distance (minutes) #define TimeLong 1 //Use long measurement for distance (hours) #define DistUnitsEEPROMAddress 2 //Memory address where distance units stored #define Energykcal 0 //Energy in kcal #define EnergyJoule 1 //Energy in Joules #define EnergyUnitsMax 1 //Number of maximum energy unit #define EnergyUnitsEEPROMAddress 3 //Memory address where energy unit stored #define EnergyNamesLen 5 //Length of energy unit names, including null termination character #define PowerWatt 0 //Power in Watt #define Powerkcalhr 1 //Power in kcal/hr #define PowerUnitsMax 1 //Number of maximum energy unit #define PowerUnitsEEPROMAddress 4 //Memory address where energy unit stored #define PowerNamesLen 7 //Length of power unit names, including null termination character #define MinRiderWeight 10 //Minimum rider weight kg #define MaxRiderWeight 200 //Maximum rider weight kg #define RiderWeightEEPROMAddress 5 //Memory address of rider weight #define MinBikeWeight 1 //Minimum bike weight kg #define MaxBikeWeight 100 //Maximum bike weight kg #define BikeWeightEEPROMAddress 6 //Memory address of bike weight #define MinAngleMin 5 //Minimum minimum angle threshold #define MinAngleMax 45 //Minimum minimum angle threshold #define MinAngleEEPROMAddress 7 //Memory address of minimum angle #define MinAngleAveTime 5 //Minimum angle avergaing time in hundredths of a second (multiply by 10 for ms) #define MaxAngleAveTime 100 //Minimum angle avergaing time in hundredths of a second (multiply by 10 for ms) #define AngleAveTimeEEPROMAddress 8 //Memory address of angle averaging time in hundredths of a second (multiply by 10 for ms) #define TiltMeasEEPROMAddress 9 //Memory address of tilt mode measurement (1 for on and 0 for off) #define AutoSaveModeEEPROMAddress 910 //Memory address for auto save mode (0-Off, 1-Auto Save Only, 2-Auto Save and Load) #define BrightnessEEPROMAddress 911 //Memory address for display brightness //Saved Values EEPROM start addresses - for first block (manual save) #define SavedMaxAngleAddress 10 //Maximum angle (tenths of a degree) #define SavedMaxWheelieTimeAddress 12 //Maximum wheelie time (ms) #define SavedTotalDistanceAddress 16 //Total ride distance (inches) #define SavedTotalTimeAddress 20 //Total ride time (seconds) #define SavedMaxSpeedAddress 24 //Max Speed (inch/sec) #define SavedTotalEnergyAddress 28 //Total Energy (tenths of a kcal) #define SavedDisplayModeAddress 32 //Display mode last used #define SavedValuesAvailableAddress 33 //Set to 100 when values saved #define SaveSetByteSize 60 //Number of bytes reserved for each set of saved data (includes reserved allocation for future use) #define MaxSaveLocation 10 //Number of sets of user saved values that can be stored #define MaxAutoSaveLocation 5 //Number of sets of autosave values that can be stored (the set used is cycled to act as wear levelling) //Miscellaeneous #define LongIntMax 4294967295 //Maximum value of an unsigned long integer, used in timer check in case value has reset to zero in the interval //Global variables double MinAngle = 15.0; //Minimum angle required to trigger and sustain wheelie measurement, in degrees, must be a floating point value, not an integer double AngleDatum = 0; //Datum angle in degrees to be subtracted from measured angle to compesate for mounting angle of device, set on startup double TiltDatum = 0; //Datum tilt angle in degrees to be subtracted from measured angle to compesate for mounting angle of device, set on startup double Angle; //Measured angle in degrees, positive if front of bike higher than rear double Tilt; //Measured tilt angle in degrees, positive if front of bike higher than rear unsigned long AngleAveragingTime = 250; //Angle measurement averaging time in in milliseconds (reduces spurious values resulting from random vibrations) - NB it is in hundredths of second in EEPROM and when used in this variable in setup double SumAngle; //Sum of measured angles in degrees, used to calculate average over the measurement interval double SumTilt; //Sum of measured tilt angles in degrees, used to calculate average over the measurement interval unsigned long NAngleMeasurements; //Number of measurements in average over the measurement interval double AverageAngle; //Average of Angle over the measurement interval, in degrees double AverageTilt; //Average of Tilt Angle over the measurement interval, in degrees unsigned long AverageAngleDatum; //Timer datum for averaging, milliseconds since switch-on, returns to zero after LongIntMax ms double MaxAngle; //Maximum recorded angle in degrees byte TiltMeas = 0; //Tilt measurement on or off byte RevCount; //Revs counted in speed averaging time unsigned long AverageSpeedDatum; //Timer datum for averaging, milliseconds since switch-on, returns to zero after LongIntMax ms unsigned long OldRevTime; //Timer value at last rev sensor contact closure byte NewRevPulse = 1; //Indicates if a new contact closure (1) which will be counted or an ongoing one (0) that won't unsigned long WheelieStartTime; //Timer datum for wheelie duration in milliseconds unsigned long TotalWheelieTime = 0; //Duration of current wheelie in milliseconds unsigned long MaxWheelieTime; //Maximum recorded wheelie duration in milliseconds unsigned long LastDispUD; //Timer value at last display update unsigned long OldDisplayButtonTime; //Timer value at last button press unsigned long OldSaveButtonTime; //Timer value at last button press double LatestDistance = 0.0; //Distance in latest measurement interval (inches) double TotalDistance = 0.0; //Total Distance (inches) double TotalTime = 0.0; //Total Time (seconds) when speed > 0 double LatestSpeed = 0.0; //Speed in last measurement period (inches/second) double AverageSpeed = 0.0; //Average speed (inches/second) when speed > 0 double MaxSpeed = 0.0; //Maximum Speed (inches/second) double DispDistance = 0.0; //Total Distance (in display units) double DispTotalTime = 0.0; //Total Time (in display units) when speed > 0 double DispLatestSpeed = 0.0; //Speed in last measurement period (in display units) double DispAverageSpeed = 0.0; //Average speed (in display units) when speed > 0 double DispMaxSpeed = 0.0; //Maximum Speed (in display units) byte RideDistanceUnit = 0; //Units to use on ride display for distance - depends if short or long distance byte RideTimeUnit = 0; //Units to use on ride display for time - depends if short or long distance byte Calibrated = 0; //If 1, Angle calibration has been completed byte WheelieTimerStarted = 0; //Wheelie currently in progress (previous angle exceeded the minimum threshold) byte WeatherStationAvailable = 1; //If 1, Weather station detected and working, otherwise 0 and weather station functions will be disabled byte ClockAvailable = 1; //If 1, real time clock detected and working, otherwise 0 and clock display will be disabled byte RevSensorAvailable = 1; //If 1, revolution sensor detected, otherwise 0 and speed and distance displays will be disabled byte DisplayMode = 0; //Current display mode (see display mode #define values) byte OldDisplayMode = 0; //Display mode at previous display write (see display mode #define values) float AtmosPressure; //Atmospheric pressure from weather station in millibar float Altitude; //Altitude calculated from weather station pressure in metres above sea level byte WheelSize; //Wheel diameter in halves of an inch, used with the rev counter to calculate speed and distance byte SpeedUnits; //Units for speed 0 = km/h, 1 = mph, 2 = m/s char SpeedNames[SpeedUnitsMax + 1][SpeedNamesLen] = {{'k', 'm', '/', 'h', '\0'},{'m', 'p', 'h', ' ', '\0'},{'m', '/', 's', ' ', '\0'}}; //Unit names for speed. Second subscript is maximum size plus 1 for the null termination byte DistUnits; //Units for distance travelled 0 = metric, 1 = imperial char DistNamesShort[DistUnitsMax + 1][DistNamesShortLen] = {{'m', ' ', '\0'},{'y', 'd', '\0'}}; //Unit names for short distance. Second subscript is maximum size plus 1 for the null termination char DistNamesLong[DistUnitsMax + 1][DistNamesLongLen] = {{'k', 'm', '\0'},{'m', 'i', '\0'}}; //Unit names for long distance. Second subscript is maximum size plus 1 for the null termination byte OldMinute = 60; //Minute for real time clock when last updated, default to 60 (invalid value) so refreshed on first write of display double Energy = 0.0; //Calculated total energy since reset in kcal double DispEnergy = 0.0; //Calculated total energy since reset in display units byte EnergyUnits = 0; //Units for energy display char EnergyNames[EnergyUnitsMax + 1][EnergyNamesLen] = {{'k', 'c', 'a', 'l', '\0'},{'J', ' ', ' ', ' ', '\0'}}; //Unit names for energy. Second subscript is maximum size plus 1 for the null termination double Power = 0.0; //Instantaneous power in kcal/hr double DispPower = 0.0; //Calculated instantaneous power in display units byte PowerUnits = 0; //Units for power display char PowerNames[PowerUnitsMax + 1][PowerNamesLen] = {{'W', 'a', 't', 't', '\0'},{'k', 'c', 'a', 'l', '/', 'h', '\0'}}; //Unit names for power. Second subscript is maximum size plus 1 for the null termination byte RiderWeight = 0; //Rider weight kg byte BikeWeight = 0; //Rider weight kg byte AutoSaveValuesChanged = 0; //1 if values have changed since previous autosave unsigned long AutoSaveTimeDatum; //Timer value at last autosave unsigned long AutoSaveIdleDatum ; //Timer value at start of no movement byte LastSaveLoc = 0; //Last used manual save location, 0 to 10, 0 is also the autosave location byte AutoSaveMode = 0; //Auto Save Mode (0-Off, 1-Auto Save Only, 2-Auto Save and Load) byte AutoSaveCount = 0; //Number of autosaves since power on - for testing purposes only byte AutoSaveLoc = 0; //Autosave location to read/write byte Brightness = 1; //Display brightness //Initialise hardware HCRTC HCRTC; //Initialise real time clock first as it includes a Wire.begin(); statement SSD1306AsciiWire oled; //Initialise OLED display BME280 bme280; //Weather station //Setup function - this code runs once whenever the Arduino is first powered up or after a reset (by grounding the RST pin) void setup() { //Initialise I2C communications //Wire.begin(); Not required as already called by initialisation of real time clock //Initialise accelerometer Wire.beginTransmission(I2CAccAdd); //Open device, address with SDO pin grounded (alt address off) Wire.write(GRangeAdd); //Address of g range of sensor Wire.write(GRangeSet); //Set g range to +/-4g Wire.endTransmission(); Wire.beginTransmission(I2CAccAdd); Wire.write(PowerSaveAdd); //Power Save address Wire.write(PowerSaveMode); //Set to measurement mode Wire.endTransmission(); //Initialise LED pin modes and state pinMode(PinLEDG, OUTPUT); //Set digital pin as output pinMode(PinLEDY, OUTPUT); //Set digital pin as output digitalWrite(PinLEDG, LOW); //LED off digitalWrite(PinLEDY, LOW); //LED off //Initialise buttons OldDisplayButtonTime = millis(); //Initialise time button last pressed OldSaveButtonTime = LongIntMax - MaxSaveButtonInterval; //Initialise time button last pressed so can do first save immediately pinMode(PinDisplayButton, INPUT_PULLUP); //Set digital pin as input with pullup to 5V (input will be low when button pressed and high when button not pressed) pinMode(PinSaveButton, INPUT_PULLUP); //Set digital pin as input with pullup to 5V (input will be low when button pressed and high when button not pressed) //Initialise revolution sensor pinMode(PinRevSensor, INPUT_PULLUP); //Set digital pin as input with pullup to 5V (input will be low when magnet next to switch and high otherwise) pinMode(PinRevSensorPresent, INPUT_PULLUP); //Set digital pin as input with pullup to 5V (input will be low when no plug inserted so socket contact closed and high when plug inserted and contact open) //Check if sensor present, switch contact on socket closed if plug not inserted if(!digitalRead(PinRevSensorPresent)) { RevSensorAvailable = 0; } //Initialise Display oled.begin(&Adafruit128x64, I2COLedAdd); // set up the OLED's number of pixels (columns and rows) oled.setFont(System5x7); //Set font name, System5x7 is a fixed width font with each character 5 pixels wide and 7 pixels tall oled.set2X(); //Double default font size, (System 5x7 font at x2 size gives 10 characters per line) Brightness = EEPROM.read(BrightnessEEPROMAddress); oled.setContrast(Brightness); //Initialise real time clock HCRTC.RTCRead(I2CDS1307Add); //Check if clock working if(HCRTC.GetSecond() > 59) //Return 85 if no real time clock present - need to check if always the case { ClockAvailable = 0; } //Enter setup mode if the display button is pressed if (!digitalRead(PinDisplayButton)) //Read button directly as we don't need debounce { //Button Pressed, enter setup mode oled.print(F("Setup Mode")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Release")); oled.clearToEOL(); oled.setCursor(0,6); oled.print(F("Button!")); oled.clearToEOL(); //Wait for button to be released do { } while (!digitalRead(PinDisplayButton)); OldDisplayButtonTime = millis(); //Reset time of last button press if(ClockAvailable) { //Prompt whether to set clock oled.setCursor(0,2); oled.print(F("Set Clock?")); oled.clearToEOL(); oled.setCursor(0,4); oled.clearToEOL(); oled.setCursor(0,6); oled.print(F("No")); oled.clearToEOL(); byte SetClock = 0; do { if (CheckDisplayButton()) { //Button pressed: increment value SetClock++; if(SetClock > 1) //Jump to minimum value { SetClock = 0; } //Update display oled.setCursor(0,6); if(SetClock == 0) { oled.print(F("No")); } else { oled.print(F("Yes")); } oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element if(SetClock) { byte RTCDataMax[6]; //Array to hold maximum value of each element of the date and time RTCDataMax[0] = 59; //Maximum seconds RTCDataMax[1] = 59; //Maximum minute RTCDataMax[2] = 23; //Maximum hour RTCDataMax[3] = 31; //Maximum date (no check made for shorter months) RTCDataMax[4] = 12; //Maximum month RTCDataMax[5] = 99; //Maximum year byte RTCDataMin[6]; //Array to hold minimum value of each element of the date and time RTCDataMin[0] = 0; //Minimum seconds RTCDataMin[1] = 0; //Minimum minute RTCDataMin[2] = 0; //Minimum hour RTCDataMin[3] = 1; //Minimum date RTCDataMin[4] = 1; //Minimum month RTCDataMin[5] = 0; //Minimum year byte RtcData[6]; //Array to save clock values //Print fixed text to screen oled.setCursor(0,2); oled.print(F("Set Clock")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Set")); oled.clearToEOL(); oled.setCursor(0,6); oled.clearToEOL(); //Retrieve current time HCRTC.RTCRead(I2CDS1307Add); RtcData[0] = HCRTC.GetSecond(); //Seconds RtcData[1] = HCRTC.GetMinute(); //Minutes RtcData[2] = HCRTC.GetHour(); //Hours RtcData[3] = HCRTC.GetDay(); //Date RtcData[4] = HCRTC.GetMonth(); //Month RtcData[5] = HCRTC.GetYear(); //Year //Loop over elements of date and time for (byte ClockElement = 5; ClockElement < 6; ClockElement--) //After 0 value will overload to 255 hence the end condition { //State what is being set oled.setCursor(49,4); //Position to write switch (ClockElement) { case 5: oled.print(F("Year")); break; case 4: oled.print(F("Month")); break; case 3: oled.print(F("Date")); break; case 2: oled.print(F("Hour")); break; case 1: oled.print(F("Minute")); break; case 0: oled.print(F("Second")); break; } oled.clearToEOL(); //Print current value to screen oled.setCursor(0,6); oled.print(RtcData[ClockElement]); oled.clearToEOL(); //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value RtcData[ClockElement]++; if(RtcData[ClockElement] > RTCDataMax[ClockElement]) //Jump to minimum value { RtcData[ClockElement] = RTCDataMin[ClockElement]; } //Update display oled.setCursor(0,6); oled.print(RtcData[ClockElement]); oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element } HCRTC.RTCWrite(I2CDS1307Add, RtcData[5], RtcData[4], RtcData[3], RtcData[2], RtcData[1], RtcData[0], 1); //Set Clock, last parameter is weekday and not used delay(100); //Delay to let the clock be set } } //Minimum angle to trigger a wheelie oled.setCursor(0,2); oled.print(F("Set Angle")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Threshold")); oled.clearToEOL(); //Read current angle from non-volatile EEPROM memory MinAngle = EEPROM.read(MinAngleEEPROMAddress); //Check MinAngle in range, if not default to smallest if((MinAngle < MinAngleMin) || (MinAngle > MinAngleMax)) { MinAngle = MinAngleMin; } //Write current value to screen oled.setCursor(0,6); oled.clearToEOL(); oled.setCursor(25,6); oled.set1X(); oled.print(F("o")); oled.set2X(); oled.setCursor(0,6); oled.print(MinAngle,0); //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value by 5 degrees MinAngle += 5; if(MinAngle > MinAngleMax) //Jump to minimum value { MinAngle = MinAngleMin; } //Update display oled.setCursor(0,6); oled.print(F(" ")); oled.setCursor(0,6); oled.print(MinAngle,0); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(MinAngleEEPROMAddress, (int)MinAngle); //Update stored value //Angle averaging time oled.setCursor(0,2); oled.print(F("Set Angle")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Ave. Time")); oled.clearToEOL(); //Read current averaging time from non-volatile EEPROM memory //NB the value of AngleAveragingTime her is in hundredths of a second, whereas when reset for main loop it is in ms. AngleAveragingTime = EEPROM.read(AngleAveTimeEEPROMAddress); //Check value in range, if not default to smallest if((AngleAveragingTime < MinAngleAveTime) || (AngleAveragingTime > MaxAngleAveTime)) { AngleAveragingTime = MinAngleAveTime; } //Write current value to screen oled.setCursor(0,6); oled.clearToEOL(); oled.setCursor(61,6); oled.print(F("ms")); oled.setCursor(0,6); oled.print(AngleAveragingTime * 10); //Convert to ms from hundredths of second as stored //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value AngleAveragingTime += 5; //Increment by 50ms if(AngleAveragingTime > MaxAngleAveTime) //Jump to minimum value { AngleAveragingTime = MinAngleAveTime; } //Update display oled.setCursor(0,6); oled.print(F(" ")); oled.setCursor(0,6); oled.print(AngleAveragingTime * 10); //Convert to ms from hundredths of second as stored } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(AngleAveTimeEEPROMAddress, AngleAveragingTime); //Update stored value //Tilt measurement included in determining whether a wheelie is sustained oled.setCursor(0,2); oled.print(F("Set Tilt")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Sensor")); oled.clearToEOL(); //Read current value from non-volatile EEPROM memory TiltMeas = EEPROM.read(TiltMeasEEPROMAddress); //Check Value in range, if not default to off if((TiltMeas > 1)) { TiltMeas = 0; } //Write current value to screen oled.setCursor(0,6); if(TiltMeas == 0) { oled.print(F("Off")); } else { oled.print(F("On")); } oled.clearToEOL(); //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value TiltMeas++; if(TiltMeas > 1) //Jump to minimum value { TiltMeas = 0; } //Update display oled.setCursor(0,6); if(TiltMeas == 0) { oled.print(F("Off")); } else { oled.print(F("On")); } oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(TiltMeasEEPROMAddress, TiltMeas); //Update stored value //Autosave Mode oled.setCursor(0,2); oled.print(F("Set Auto")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Save Mode")); oled.clearToEOL(); //Read current value from non-volatile EEPROM memory AutoSaveMode = EEPROM.read(AutoSaveModeEEPROMAddress); //Check Value in range, if not default to off if((AutoSaveMode > 2)) { AutoSaveMode = 0; } //Write current value to screen oled.setCursor(0,6); switch(AutoSaveMode) { case 0: oled.print(F("Off")); break; case 1: oled.print(F("Save Only")); break; case 2: oled.print(F("Save ")); oled.setCursor(59,6); //Reduce gaps between words to fit oled.print(F("& ")); oled.setCursor(82,6); oled.print(F("Load")); break; } oled.clearToEOL(); //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value AutoSaveMode++; if(AutoSaveMode > 2) //Jump to minimum value { AutoSaveMode = 0; } //Update display oled.setCursor(0,6); switch(AutoSaveMode) { case 0: oled.print(F("Off")); break; case 1: oled.print(F("Save Only")); break; case 2: oled.print(F("Save ")); oled.setCursor(59,6); //Reduce gaps between words to fit oled.print(F("& ")); oled.setCursor(82,6); oled.print(F("Load")); break; } oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(AutoSaveModeEEPROMAddress, AutoSaveMode); //Update stored value //Display Brightness oled.setCursor(0,2); oled.print(F("Set ")); oled.setCursor(45,2); //Compress gap between characters to make text fit oled.print(F("Display")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Brightness")); oled.clearToEOL(); //Read current value from non-volatile EEPROM memory Brightness = EEPROM.read(BrightnessEEPROMAddress); //Write current value to screen oled.setCursor(0,6); PrintBrightness(); //maps brightness value to Low, Medium or High and prints on screen //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value if(Brightness > 191) //Currently High, Jump to minimum value { Brightness = 1; } else { Brightness += 127; } //Update display oled.setCursor(0,6); PrintBrightness(); //maps brightness value to Low, Medium or High and prints on screen oled.setContrast(Brightness); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(BrightnessEEPROMAddress, Brightness); //Update stored value if (RevSensorAvailable) //Options pertaining to rev sensor, only display if sensor connected { //Set Wheel Size (for rev counter) oled.setCursor(0,2); oled.print(F("Set Wheel")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Diameter")); oled.clearToEOL(); //Read current wheel size from non-volatile EEPROM memory WheelSize = EEPROM.read(WheelSizeEEPROMAddress); //Check WheelSize in range, if not default to smallest if((WheelSize < MinWheelSize) || (WheelSize > MaxWheelSize)) { WheelSize = MinWheelSize; } //Write current value to screen oled.setCursor(0,6); oled.clearToEOL(); oled.setCursor(56,6); oled.print(F("inches")); oled.setCursor(0,6); oled.print((((float)WheelSize) / 2.0), 1); //Convert to inches and display to 1 decimal place (should increment in multiples of 0.5") //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value WheelSize++; if(WheelSize > MaxWheelSize) //Jump to minimum value { WheelSize = MinWheelSize; } //Update display oled.setCursor(0,6); oled.print(F(" ")); oled.setCursor(0,6); oled.print((((float)WheelSize) / 2.0), 1); //Convert to inches and display to 1 decimal place (should increment in multiples of 0.5") } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(WheelSizeEEPROMAddress, WheelSize); //Update stored value //Set Rider Weight oled.setCursor(0,2); oled.print(F("Set Rider")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Weight")); oled.clearToEOL(); //Read current rider weight from non-volatile EEPROM memory RiderWeight = EEPROM.read(RiderWeightEEPROMAddress); //Check Rider Weight in range, if not default to smallest if((RiderWeight < MinRiderWeight) || (RiderWeight > MaxRiderWeight)) { RiderWeight = MinRiderWeight; } //Write current value to screen oled.setCursor(0,6); oled.clearToEOL(); oled.setCursor(46,6); oled.print(F("kg")); oled.setCursor(0,6); oled.print(RiderWeight); //Display value //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value RiderWeight++; if(RiderWeight > MaxRiderWeight) //Jump to minimum value { RiderWeight = MinRiderWeight; } //Update display oled.setCursor(0,6); oled.print(F(" ")); oled.setCursor(0,6); oled.print(RiderWeight); //Display value } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(RiderWeightEEPROMAddress, RiderWeight); //Update stored value //Set Bike Weight oled.setCursor(0,2); oled.print(F("Set Bike")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Weight")); oled.clearToEOL(); //Read current bike weight from non-volatile EEPROM memory BikeWeight = EEPROM.read(BikeWeightEEPROMAddress); //Check Bike Weight in range, if not default to smallest if((BikeWeight < MinBikeWeight) || (BikeWeight > MaxBikeWeight)) { BikeWeight = MinBikeWeight; } //Write current value to screen oled.setCursor(0,6); oled.clearToEOL(); oled.setCursor(46,6); oled.print(F("kg")); oled.setCursor(0,6); oled.print(BikeWeight); //Display value //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value BikeWeight++; if(BikeWeight > MaxBikeWeight) //Jump to minimum value { BikeWeight = MinBikeWeight; } //Update display oled.setCursor(0,6); oled.print(F(" ")); oled.setCursor(0,6); oled.print(BikeWeight); //Display value } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(BikeWeightEEPROMAddress, BikeWeight); //Update stored value //Set speed units oled.setCursor(0,2); oled.print(F("Set Units")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("for Speed")); oled.clearToEOL(); //Read current units from non-volatile EEPROM memory SpeedUnits = EEPROM.read(SpeedUnitsEEPROMAddress); //Check Speed Units in range, if not default to smallest if(SpeedUnits > SpeedUnitsMax) { SpeedUnits = 0; } //Write current value to screen oled.setCursor(0,6); for (byte CharNo = 0; CharNo < SpeedNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(SpeedNames [SpeedUnits][CharNo]); } oled.clearToEOL(); //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value SpeedUnits++; if(SpeedUnits > SpeedUnitsMax) //Jump to minimum value { SpeedUnits = 0; } //Update display oled.setCursor(0,6); for (byte CharNo = 0; CharNo < SpeedNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(SpeedNames [SpeedUnits][CharNo]); } oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(SpeedUnitsEEPROMAddress, SpeedUnits); //Update stored value //Set distance units oled.setCursor(0,2); oled.print(F("Set Units")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("for Dist.")); oled.clearToEOL(); //Read current units from non-volatile EEPROM memory DistUnits = EEPROM.read(DistUnitsEEPROMAddress); //Check Distance Units in range, if not default to smallest if(DistUnits > DistUnitsMax) { DistUnits = 0; } //Write current value to screen oled.setCursor(0,6); oled.print(F(" &")); oled.clearToEOL(); oled.setCursor(0,6); //Names of long distances for (byte CharNo = 0; CharNo < DistNamesLongLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(DistNamesLong [DistUnits][CharNo]); } oled.setCursor(60,6); //Names of short distances for (byte CharNo = 0; CharNo < DistNamesShortLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(DistNamesShort [DistUnits][CharNo]); } //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value DistUnits++; if(DistUnits > DistUnitsMax) //Jump to minimum value { DistUnits = 0; } //Update display oled.setCursor(0,6); oled.print(F(" &")); oled.clearToEOL(); oled.setCursor(0,6); //Names of long distances for (byte CharNo = 0; CharNo < DistNamesLongLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(DistNamesLong [DistUnits][CharNo]); } oled.setCursor(60,6); //Names of short distances for (byte CharNo = 0; CharNo < DistNamesShortLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(DistNamesShort [DistUnits][CharNo]); } } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(DistUnitsEEPROMAddress, DistUnits); //Update stored value //Set Energy Unit oled.setCursor(0,2); oled.print(F("Set Units")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("for Energy")); oled.clearToEOL(); //Read current units from non-volatile EEPROM memory EnergyUnits = EEPROM.read(EnergyUnitsEEPROMAddress); //Check Energy Units in range, if not default to smallest if(EnergyUnits > EnergyUnitsMax) { EnergyUnits = 0; } //Write current value to screen oled.setCursor(0,6); //Name of energy unit for (byte CharNo = 0; CharNo < EnergyNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(EnergyNames [EnergyUnits][CharNo]); } oled.clearToEOL(); //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value EnergyUnits++; if(EnergyUnits > EnergyUnitsMax) //Jump to minimum value { EnergyUnits = 0; } //Update display oled.setCursor(0,6); //Name of energy units for (byte CharNo = 0; CharNo < EnergyNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(EnergyNames [EnergyUnits][CharNo]); } oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(EnergyUnitsEEPROMAddress, EnergyUnits); //Update stored value //Set Power Unit oled.setCursor(0,2); oled.print(F("Set Units")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("for Power")); oled.clearToEOL(); //Read current units from non-volatile EEPROM memory PowerUnits = EEPROM.read(PowerUnitsEEPROMAddress); //Check Power Units in range, if not default to smallest if(PowerUnits > PowerUnitsMax) { PowerUnits = 0; } //Write current value to screen oled.setCursor(0,6); //Name of energy unit for (byte CharNo = 0; CharNo < PowerNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(PowerNames [PowerUnits][CharNo]); } oled.clearToEOL(); //Wait for button presses to change value do { if (CheckDisplayButton()) { //Button pressed: increment value PowerUnits++; if(PowerUnits > PowerUnitsMax) //Jump to minimum value { PowerUnits = 0; } //Update display oled.setCursor(0,6); //Name of power units for (byte CharNo = 0; CharNo < PowerNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(PowerNames [PowerUnits][CharNo]); } oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed OldDisplayButtonTime = millis(); //Reset timeout for next element EEPROM.update(PowerUnitsEEPROMAddress, PowerUnits); //Update stored value //Revolution sensor check oled.setCursor(0,2); oled.print(F("Rev Sensor")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Test")); oled.clearToEOL(); oled.setCursor(0,6); oled.print(F("Revs: 0")); oled.clearToEOL(); byte DoneMessage = 0; //1 after message changed RevCount = 0; //Stores counted revs for the test byte OldRevCount = 0; //Stores previous counted revs for the test OldRevTime = millis(); //Keep counting revs and updating display until button pressed do { //Show messgae stating how to end test if(!DoneMessage && (IntervalCheck(OldRevTime, SetupTimeout))) { oled.setCursor(0,2); oled.print(F("End: Press")); oled.clearToEOL(); oled.setCursor(0,4); oled.print(F("Display")); oled.clearToEOL(); DoneMessage = 1; } //Detect low pulse from Rev sensor ReadRevSensor(); if(RevCount != OldRevCount) { OldRevCount = RevCount; oled.setCursor(72,6); oled.print(RevCount); oled.clearToEOL(); } } while (digitalRead(PinDisplayButton)); OldDisplayButtonTime = millis(); //Reset last button press time //End of setup parameters for rev sensor } //End of setup, display will be refreshed in main loop } //Initialise angle averageing parameters AverageAngleDatum = millis(); SumAngle = 0.0; SumTilt = 0.0; NAngleMeasurements = 0; //Initialise average speed parameters AverageSpeedDatum = millis() - SpeedAveragingTime; //So calculates immediately on first main loop and displays any loaded values straight away //Initialise rev counting (it may have been used in setup rev test) RevCount = 0; OldRevTime = millis(); NewRevPulse = 1; //Read units from non-volatile memory SpeedUnits = EEPROM.read(SpeedUnitsEEPROMAddress); DistUnits = EEPROM.read(DistUnitsEEPROMAddress); WheelSize = EEPROM.read(WheelSizeEEPROMAddress); BikeWeight = EEPROM.read(BikeWeightEEPROMAddress); RiderWeight = EEPROM.read(RiderWeightEEPROMAddress); EnergyUnits = EEPROM.read(EnergyUnitsEEPROMAddress); PowerUnits = EEPROM.read(PowerUnitsEEPROMAddress); MinAngle = EEPROM.read(MinAngleEEPROMAddress); AngleAveragingTime = (EEPROM.read(AngleAveTimeEEPROMAddress)) * 10; //Convert to ms from hundredths of a second TiltMeas = EEPROM.read(TiltMeasEEPROMAddress); AutoSaveMode = EEPROM.read(AutoSaveModeEEPROMAddress); //Display Brightness has already been set when display initialised, and updated if changed in setup //Initialise weather station if(!bme280.init()) { WeatherStationAvailable = 0; //Weather station did not initialise, disable weather station functionality } //Load saved values if Load button pressed and values are available if (!digitalRead(PinSaveButton)) { //Prompt to release button oled.clear(); oled.println(F("Load Saved")); oled.println(F("Values:")); oled.println(F("Release")); oled.println(F("Button!")); //Wait for button to be released do { } while (!digitalRead(PinSaveButton)); //Prompt for memory address oled.clear(); oled.println(F("Select")); oled.println(F("Memory")); oled.println(F("Location:")); if(LastSaveLoc == MaxSaveLocation + 1) //Following a Reset All { oled.print(F("Reset All")); } else { oled.print(LastSaveLoc); if(LastSaveLoc == 0) { oled.print(F(" (Auto)")); } } //Wait for button presses to change value OldDisplayButtonTime = millis(); //Reset timeout for next element do { if (CheckDisplayButton()) { //Button pressed: increment value LastSaveLoc ++; if(LastSaveLoc > MaxSaveLocation + 1) //Jump to minimum value { LastSaveLoc = 0; } //Update display oled.setCursor(0,6); if(LastSaveLoc <= MaxSaveLocation) { oled.print(LastSaveLoc); if(LastSaveLoc == 0) { oled.print(F(" (Auto)")); } } else { oled.print(F("Reset All")); } oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed if(LastSaveLoc > MaxSaveLocation) { //Indicates reset selected //Zero the values MaxAngle = 0.0; //Maximum angle MaxWheelieTime = 0; //Maximum wheelie time TotalDistance = 0.0; //Total ride distance TotalTime = 0.0; //Total ride time MaxSpeed = 0.0; //Max Speed Energy = 0.0; //Total energy DisplayMode = 0; //Display Mode //Zero the autosave values AutoSaveLoc = AutoSaveNextLocation(); unsigned int AddrOffset = (MaxSaveLocation + AutoSaveLoc - 1) * SaveSetByteSize; EEPROMWriteUint16(SavedMaxAngleAddress + AddrOffset,0); //Maximum angle EEPROMWriteUint32(SavedMaxWheelieTimeAddress + AddrOffset,0); //Maximum wheelie time EEPROMWriteUint32(SavedTotalDistanceAddress + AddrOffset,0); //Total ride distance EEPROMWriteUint32(SavedTotalTimeAddress + AddrOffset,0); //Total ride time EEPROMWriteUint32(SavedMaxSpeedAddress + AddrOffset,0); //Max Speed EEPROMWriteUint32(SavedTotalEnergyAddress + AddrOffset,0); //Total energy EEPROM.update(SavedDisplayModeAddress + AddrOffset, 0); //Display Mode AutoSaveSetLocation(); //Set current autosave location //Confirmation message oled.clear(); oled.println(F("Values")); oled.println(F("Reset")); oled.println(F("to Zero")); } else { unsigned int AddrOffset; if (LastSaveLoc == 0) { //Autosave location selected (0) AutoSaveLoc = AutoSaveLastLocation(); //Returns MaxAutoSaveLocation if no autosave data available which is further checked below AddrOffset = (MaxSaveLocation + AutoSaveLoc - 1) * SaveSetByteSize; } else { AddrOffset = (LastSaveLoc - 1) * SaveSetByteSize; } if(EEPROM.read(SavedValuesAvailableAddress + AddrOffset) == 100) //Won't read values if manual memory location empty or no autosave data available { MaxAngle = ((double)EEPROMReadUint16(SavedMaxAngleAddress + AddrOffset)) / 10.0; //Maximum angle MaxWheelieTime = EEPROMReadUint32(SavedMaxWheelieTimeAddress + AddrOffset); //Maximum wheelie time TotalDistance = (double)EEPROMReadUint32(SavedTotalDistanceAddress + AddrOffset); //Total ride distance TotalTime = (double)EEPROMReadUint32(SavedTotalTimeAddress + AddrOffset); //Total ride time MaxSpeed = (double)EEPROMReadUint32(SavedMaxSpeedAddress + AddrOffset); //Max Speed Energy = ((double)EEPROMReadUint32(SavedTotalEnergyAddress + AddrOffset)) / 10.0; //Total energy DisplayMode = EEPROM.read(SavedDisplayModeAddress + AddrOffset); //Display Mode //Confirmation message oled.clear(); oled.println(F("Values")); oled.println(F("Retrieved")); } else { //Error message oled.clear(); oled.println(F("Error: No")); oled.println(F("Saved Data")); } } OldDisplayButtonTime = millis(); //Reset timeout for clearing confirmation message do { } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout/2)); //After timeout, proceed } else { //Default: Autoload if values are available and auto load enabled unsigned int AddrOffset; AutoSaveLoc = AutoSaveLastLocation(); //Returns MaxAutoSaveLocation if no autosave data available which is further checked below AddrOffset = (MaxSaveLocation + AutoSaveLoc - 1) * SaveSetByteSize; if((EEPROM.read(SavedValuesAvailableAddress + AddrOffset) == 100) && (AutoSaveMode == 2)) { MaxAngle = ((double)EEPROMReadUint16(SavedMaxAngleAddress + AddrOffset)) / 10.0; //Maximum angle MaxWheelieTime = EEPROMReadUint32(SavedMaxWheelieTimeAddress + AddrOffset); //Maximum wheelie time TotalDistance = (double)EEPROMReadUint32(SavedTotalDistanceAddress + AddrOffset); //Total ride distance TotalTime = (double)EEPROMReadUint32(SavedTotalTimeAddress + AddrOffset); //Total ride time MaxSpeed = (double)EEPROMReadUint32(SavedMaxSpeedAddress + AddrOffset); //Max Speed Energy = ((double)EEPROMReadUint32(SavedTotalEnergyAddress + AddrOffset)) / 10.0; //Total energy DisplayMode = EEPROM.read(SavedDisplayModeAddress + AddrOffset); //Display Mode } } AutoSaveTimeDatum = millis(); //Initialise time of last autosave AutoSaveIdleDatum = millis(); //Initialise time of last stationary period OldDisplayMode = DisplayMode; //Display configured on first iteration of main loop, so does not need refreshing again on first standard iteration of loop } //Loop function - this runs continuously once Setup function has completed void loop() { //Read the button to change display mode ReadDisplayButton(); //Read button to save values ReadSaveButton(); //Read Rev Sensor if(RevSensorAvailable) { ReadRevSensor(); } //Read the angle from the accelerometer byte _buff[6]; int i = 0; Wire.beginTransmission(I2CAccAdd); Wire.write(XStartAdd); //x data start address (2 bytes for x, followed by 2 for y and 2 for z = 6 bytes in consecutive addresses 0x32 to 0x37 Wire.endTransmission(); Wire.beginTransmission(I2CAccAdd); Wire.requestFrom(I2CAccAdd,6); //Read 6 bytes to buffer: x, y, z values, 2 bytes each while(Wire.available()) { _buff[i] = Wire.read(); //Read a byte from buffer to array i++; } Wire.endTransmission(); //Copy angle values to variables from the buffer float x = (((int)_buff[1]) << 8) | _buff[0]; //this is the x value but it is not used float y = (((int)_buff[3]) << 8) | _buff[2]; float z = (((int)_buff[5]) << 8) | _buff[4]; //Scale values x *= 0.0078; //this is the x value but it is not used y *= 0.0078; z *= 0.0078; Angle = (atan2(-y, z) * 180.0) / M_PI; //This is the angle in degrees calculated from the accelerometer y and z values Tilt = (atan2(x, sqrt(y * y + z * z)) * 180.0) / M_PI; //This is the angle in degrees calculated from the accelerometer x, y and z values which would give the sideways abgle from verical, and is not used if (!Calibrated) //Device not yet calibrated to compensate for mounting angle { //This code calibrates the device and sets up the display after switch-on //Store the first measured angle, bike should be level when device switched on to ensure accurate calibration AngleDatum = Angle; TiltDatum = Tilt; //Initialise fixed text on display DisplaySetup(); LastDispUD = millis(); //Record time that display was last updated Calibrated = 1; //Set calibrated flag so this code does not run again } else { //Main measurement loop, runs repeatedly once calibration is complete //Calculate Calibrated angle by subtracting the datum angle Angle = Angle - AngleDatum; Tilt = Tilt - TiltDatum; //Translate significantly negative angles to positive if(Angle < -90.0) { Angle += 360.0; } //Update the averaging parameters SumAngle += Angle; //Add latest value to the sum of measured angles SumTilt += Tilt; NAngleMeasurements++; //Add 1 to number of measurements in the average if (IntervalCheck(AverageSpeedDatum, SpeedAveragingTime) && RevSensorAvailable) //Speed Averaging time complete, calculate the latest and overall average speed and total distance { //Distance increment (inches) LatestDistance = RevCount * WheelSize * 0.5 * M_PI; //diameter x pi, convert to full inches from halves of an inch TotalDistance += LatestDistance; //Total Riding Time (seconds) if(RevCount) { TotalTime += (SpeedAveragingTime / 1000.0); // this excludes stops AutoSaveValuesChanged = 1; //Some distance has been travelled so autosave will be needed AutoSaveIdleDatum = millis(); //Don't autosave during riding, to preserve EEPROM } //Latest Speed (inches per second) LatestSpeed = LatestDistance / (SpeedAveragingTime / 1000.0); //Maximum Speed if (LatestSpeed > MaxSpeed) { MaxSpeed = LatestSpeed; } //Overall Speed (inches per second) if(TotalTime > 0.0) { AverageSpeed = TotalDistance / TotalTime; } else { AverageSpeed = 0.0; } //Power in kcal/h //Add metabolic effort scaled from (rider weight in kg x 6 = kcal/hr at 10mph, scales with speed as energy per unit distance roughly constant if (AverageAngle > -10.0) //Assume no energy expended on significant downhill gradient { //Note no energy expended if speed is zero Power = RiderWeight * 6.0 * (LatestSpeed / 176.0); //176 inch/s = 10mph //Add effect of uphill gradient, total mass x g x vertical distance in metres, divide by time increment for power in Watts, convert to kcal/h by multiplying by 3600s/hr / 4184j/kcal //Note answer zero if distance increment is zero if ((AverageAngle) > 0.0 && (AverageAngle <= 30.0)) //Only calculate for plausible angles { Power += (((RiderWeight + BikeWeight) * 9.81 * LatestDistance * 0.0254 * tan(AverageAngle * (M_PI / 180.0))) / (SpeedAveragingTime / 1000.0)) * (3600.0 / 4184.0); //0.0254 converts distance to metres. Convert angle to radians. Convert time to seconds. Convert answer from Watts to kcal/h } } else { Power = 0.0; //Downhill } //Energy in kcal from power in kcal/hr : multiply by hours in the averaging period and add to previous value Energy += (Power * ((SpeedAveragingTime / 1000.0) / 3600.0)); //Convert above to display units //Speeds double SpeedFactor; switch (SpeedUnits) { case SpeedKMH: SpeedFactor = 0.09144; //inch/second to km/h break; case SpeedMPH: SpeedFactor = 0.056818; //inch/second to km/h break; case SpeedMS: SpeedFactor = 0.0254; //inch/second to km/h break; } DispLatestSpeed = LatestSpeed * SpeedFactor; DispAverageSpeed = AverageSpeed * SpeedFactor; DispMaxSpeed = MaxSpeed * SpeedFactor; //Distances double DistanceFactor; switch (DistUnits) { case DistMetric: DistanceFactor = 0.0254; //inch to metres break; case DistImperial: DistanceFactor = 0.0277; //inch to yard break; } DispDistance = TotalDistance * DistanceFactor; if (DispDistance > 500.0) { //Use long distance measurement switch (DistUnits) { case DistMetric: DispDistance /= 1000.0 ; //metres to kilometres break; case DistImperial: DispDistance /= 1760.0; //yards to miles break; } RideDistanceUnit = DistLong; } else { RideDistanceUnit = DistShort; } //Times DispTotalTime = TotalTime / 60.0; //Convert to minutes if(DispTotalTime > 60.0) { DispTotalTime = TotalTime / 60.0; //Convert to hours RideTimeUnit = TimeLong; } else { RideTimeUnit = TimeShort; } //Energy switch (EnergyUnits) { case Energykcal: DispEnergy = Energy; //No conversion break; case EnergyJoule: DispEnergy = Energy * 4184.0; //kcal to joule break; } //Power switch (PowerUnits) { case Powerkcalhr: DispPower = Power; //No conversion break; case PowerWatt: DispPower = (Power * 4184.0) / 3600.0; //kcal/hr to watt (J/s) break; } //Reset averaging parameters AverageSpeedDatum = millis(); RevCount = 0; } if (IntervalCheck(AverageAngleDatum, AngleAveragingTime)) //Angle Averaging time complete, calculate the average and update calculations { //Calculate averaged angle as sum of angles divided by number of measurements AverageAngle = SumAngle / NAngleMeasurements; AverageTilt = SumTilt / NAngleMeasurements; if(AverageAngle > MaxAngle) //If average angle exceeds the previous maximum, update the maximum value { MaxAngle = AverageAngle; } if ((AverageAngle >= MinAngle) && (!WheelieTimerStarted)) //If minimum angle requirement for a wheelie is exceeded and wheelie not in progress, start timing the wheelie. Tilt only considered to sustain wheelie not to trigger it { WheelieTimerStarted = 1; //Set wheelie in progress flag WheelieStartTime = millis(); //Store start time of wheelie digitalWrite(PinLEDY, HIGH); //Yellow LED on to indicate wheelie in progress } if (WheelieTimerStarted) //If wheelie in progress, update the duration of the wheelie and if angle no longer meets minimum requirement end the wheelie { if(IntervalCheck(WheelieStartTime, MinWheelieTime)) //Update elapsed time, only if duration of wheelie exceeds the minimum threshold (this stops brief durations of high angles resetting the results of the previous wheelie) { TotalWheelieTime = millis() - WheelieStartTime; } if (TotalWheelieTime > MaxWheelieTime) //Store maximum wheelie duration if it exceeds the previous maximum { MaxWheelieTime = TotalWheelieTime; digitalWrite(PinLEDG, HIGH); //Green LED on - indicates current wheelie has a record breaking time } else { digitalWrite(PinLEDG, LOW); //Green LED off - indicates current wheelie is not a record breaking time } if ((AverageAngle < MinAngle) && ((TiltMeas == 0) || ((AverageTilt < MinAngle) && (AverageTilt > (-1.0 * MinAngle))))) //Wheelie has ended, angle no longer meets the minimum requirement and tilt angle is within threshold of vertical to either side { WheelieTimerStarted = 0; //Stop wheelie timer digitalWrite(PinLEDY, LOW); //Yellow LED off - Wheelie no longer in progress if (TotalWheelieTime == MaxWheelieTime) { AutoSaveValuesChanged = 1; //New maximum length of completed wheelie } } } //Update Display if (IntervalCheck(LastDispUD, DisplayUDInt)) //Only update display at stated interval { if (DisplayMode != OldDisplayMode) //Display mode has changed, clear text and write fixed text { DisplaySetup(); } if (DisplayMode == WheelieDisplayModeAT) //Wheelie display { //Current angle display oled.setCursor(66,0); //Set cursor position: first value is the pixel of the column (far left = 0), second value is the row in 8 pixel blocks (System 5x7 font at x2 size gives 4 rows of text, so only use row numbers 0, 2, 4, 6 or text will overlap) if (AverageAngle <= -99.95) { oled.print(AverageAngle,0); //0 decimal places if less than -100 degrees , or the text won't fit on the display } else { oled.print(AverageAngle,1); //Otherwise display with 1 decimal place } oled.clearToEOL(); //Clear remainder of current value, if text is overwritten and has less digits it will not completely clear the old value //Maximum angle display oled.setCursor(66,2); //Move to start of second row (actually third row as rows count from zero but each row of text consumes two rows as double height text used oled.print(MaxAngle,1); //Write angle to 1 decimal place. Since calibrated to zero, the maximum should never be negative so don't need to worry about values < -100 oled.clearToEOL(); //Clear rest of current value //Current wheelie duration display oled.setCursor(66,4); //Move to start of third row ( = 0 + (2 x (text row number - 1)) if ((TotalWheelieTime/1000.0) < 999.95) //If less than 1000 seconds there is space for 1 decimal place { oled.print((TotalWheelieTime/1000.0),1); //Convert milliseconds to seconds and display with 1 decimal place } else //If 1000 seconds or more no space for decimal place { oled.print((TotalWheelieTime/1000.0),0); //Convert milliseconds to seconds and display with no decimal places } oled.clearToEOL(); //Clear rest of current value //Maximum time display oled.setCursor(66,6); //Move to start of fourth row ( = 0 + (2 x (text row number - 1)) if ((MaxWheelieTime/1000.0) < 999.95) //If less than 1000 seconds there is space for 1 decimal place { oled.print((MaxWheelieTime/1000.0),1); //Convert milliseconds to seconds and display with 1 decimal place } else //If 1000 seconds or more no space for decimal place { oled.print((MaxWheelieTime/1000.0),0); //Convert milliseconds to seconds and display with no decimal places } oled.clearToEOL(); //Clear rest of current value } if (DisplayMode == WheelieDisplayModeTime) //Wheelie time display { oled.setCursor(0,3); //Move to start of fourth row (each row of text consumes two rows as double height text used //Set font oled.setFont(lcdnums12x16); //Set font name, lcdnums12x16 is a fixed width font with each character 14 pixels wide and 24 pixels tall if ((TotalWheelieTime/1000.0) < 999.95) //If less than 1000 seconds there is space for 1 decimal place { oled.print((TotalWheelieTime/1000.0),1); //Convert milliseconds to seconds and display with 1 decimal place } else //If 1000 seconds or more no space for decimal place { oled.print((TotalWheelieTime/1000.0),0); //Convert milliseconds to seconds and display with no decimal places } //Wipe remainder of old number. oled.setFont(ZevvPeep8x16); //lcdnums12x16 does not wipe out current text with spaces. To wipe with this font requires 50% more characters as narrower oled.clearToEOL(); //Reset font to default oled.setFont(System5x7); //Set font name, System5x7 is a fixed width font with each character 5 pixels wide and 7 pixels tall } if (DisplayMode == SpeedDisplayMode) //Speedometer display { oled.setCursor(0,3); //Move to start of fourth row (each row of text consumes two rows as double height text used //Set font oled.setFont(lcdnums12x16); //Set font name, lcdnums12x16 is a fixed width font with each character 14 pixels wide and 24 pixels tall oled.print(DispLatestSpeed,1); //Print Speed to 1 decimal place //Wipe remainder of old number. oled.setFont(ZevvPeep8x16); //lcdnums12x16 does not wipe out current text with spaces. To wipe with this font requires 50% more characters as narrower oled.clearToEOL(); //Reset font to default oled.setFont(System5x7); //Set font name, System5x7 is a fixed width font with each character 5 pixels wide and 7 pixels tall } if (DisplayMode == DistanceDisplayMode) //Odometer display { //Units oled.setCursor(106,0); if (RideDistanceUnit == DistShort) { for (byte CharNo = 0; CharNo < DistNamesShortLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(DistNamesShort [DistUnits][CharNo]); } } else { for (byte CharNo = 0; CharNo < DistNamesLongLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(DistNamesLong [DistUnits][CharNo]); } } oled.clearToEOL(); //Erase rest of old text //Value oled.setCursor(0,3); //Move to start of fourth row (each row of text consumes two rows as double height text used //Set font oled.setFont(lcdnums12x16); //Set font name, lcdnums12x16 is a fixed width font with each character 14 pixels wide and 24 pixels tall if (DispDistance < 999.95) { oled.print(DispDistance,1); //Print Distance to 1 decimal place } else { oled.print(DispDistance,0); //Print Distance with no decimal place } //Wipe remainder of old number. oled.setFont(ZevvPeep8x16); //lcdnums12x16 does not wipe out current text with spaces. To wipe with this font requires 50% more characters as narrower oled.clearToEOL(); //Reset font to default oled.setFont(System5x7); //Set font name, System5x7 is a fixed width font with each character 5 pixels wide and 7 pixels tall } if (DisplayMode == RideDisplayMode) //Ride Stats display { //Units //Distance oled.setCursor(0,2); byte CharNo; if (RideDistanceUnit == DistShort) { for (CharNo = 0; CharNo < DistNamesShortLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(DistNamesShort [DistUnits][CharNo]); } } else { for (CharNo = 0; CharNo < DistNamesLongLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(DistNamesLong [DistUnits][CharNo]); } } for (; CharNo < 5; CharNo++) //Delete rest of old value. No start condition as using current value of CharNo { oled.print(F(" ")); } //Time if (RideTimeUnit == TimeShort) { oled.setCursor(0,4); oled.print(F("Mins ")); } else { oled.setCursor(0,4); oled.print(F("Hrs ")); } //Values oled.setCursor(60,2); if (DispDistance < 999.95) { oled.print(DispDistance,1); //Print distance to 1 decimal place } else { oled.print(DispDistance,0); //Print distance with no decimal place } oled.clearToEOL(); //Erase rest of old value oled.setCursor(60,4); if (DispTotalTime < 999.95) { oled.print(DispTotalTime,1); //Print time to 1 decimal place } else { oled.print(DispTotalTime,0); //Print time with no decimal place } oled.clearToEOL(); //Erase rest of old value oled.setCursor(60,6); oled.print(DispAverageSpeed,1); //Print Speed to 1 decimal place oled.clearToEOL(); //Erase rest of old value } if (DisplayMode == SpeedStatsDisplayMode) //Speed Stats display { oled.setCursor(60,2); oled.print(DispLatestSpeed,1); //Print Speed to 1 decimal place oled.clearToEOL(); //Erase rest of old value oled.setCursor(60,4); oled.print(DispAverageSpeed,1); //Print Speed to 1 decimal place oled.clearToEOL(); //Erase rest of old value oled.setCursor(60,6); oled.print(DispMaxSpeed,1); //Print Speed to 1 decimal place oled.clearToEOL(); //Erase rest of old value } if (DisplayMode == EnergyDisplayMode) //Total Energy display { oled.setCursor(0,3); //Move to start of second row (actually third row as rows count from zero but each row of text consumes two rows as double height text used oled.setFont(lcdnums12x16); //Set font name, lcdnums12x16 is a fixed width font with each character 14 pixels wide and 24 pixels tall if (DispEnergy < 999.95) { oled.print(DispEnergy,1); //Print Energy to 1 decimal place } if (DispEnergy >= 999.95) { if(DispEnergy >= 99999.5) { //Use smaller font and reposition oled.set1X(); oled.setCursor(0,4); oled.setFont(lcdnums12x16); } oled.print(DispEnergy,0); //Print Energy with no decimal place //Wipe remainder of old number. oled.setFont(ZevvPeep8x16); //lcdnums12x16 does not wipe out current text with spaces. To wipe with this font requires 50% more characters as narrower oled.clearToEOL(); oled.set2X(); } //Reset font to default oled.setFont(System5x7); //Set font name, System5x7 is a fixed width font with each character 5 pixels wide and 7 pixels tall } if (DisplayMode == PowerDisplayMode) //Instantaneous power display { //Value oled.setCursor(0,2); //Move to start of second row (actually third row as rows count from zero but each row of text consumes two rows as double height text used //Set font oled.setFont(lcdnums12x16); //Set font name, lcdnums12x16 is a fixed width font with each character 14 pixels wide and 24 pixels tall if (DispPower < 999.95) { oled.print(DispPower,1); //Print power to 1 decimal place } else { oled.print(DispPower,0); //Print power with no decimal place } //Wipe remainder of old number. oled.setFont(ZevvPeep8x16); //lcdnums12x16 does not wipe out current text with spaces. To wipe with this font requires 50% more characters as narrower oled.clearToEOL(); //Reset font to default oled.setFont(System5x7); //Set font name, System5x7 is a fixed width font with each character 5 pixels wide and 7 pixels tall } if (DisplayMode == ClockDisplayMode) //Clock display { HCRTC.RTCRead(I2CDS1307Add); //Update the clock if (HCRTC.GetMinute() != OldMinute) //Only refresh display if minute has changed { OldMinute = HCRTC.GetMinute(); //Update the stored minute for this refresh oled.clear(); //No fixed text //Date oled.setCursor(17,0); //Centre text oled.print(HCRTC.GetDateString()); //Print Date //Time oled.setCursor(4,3); //Move to start of second row (actually third row as rows count from zero but each row of text consumes two rows as double height text used //Set font oled.setFont(lcdnums12x16); //Set font name, lcdnums12x16 is a fixed width font with each character 14 pixels wide and 24 pixels tall oled.print(HCRTC.GetTimeString()); //Print time //Reset font to default oled.setFont(System5x7); //Set font name, System5x7 is a fixed width font with each character 5 pixels wide and 7 pixels tall } } if (DisplayMode == WeatherDisplayMode) //Weather display { //Temperature oled.setCursor(66,2); //Set cursor position: first value is the pixel of the column (far left = 0), second value is the row in 8 pixel blocks (System 5x7 font at x2 size gives 4 rows of text, so only use row numbers 0, 2, 4, 6 or text will overlap) oled.print(bme280.getTemperature(),1); //Temperature to 1 decimal place oled.clearToEOL(); //Erase rest of old value //Relative humidity oled.setCursor(66,4); //Move to start of second row (actually third row as rows count from zero but each row of text consumes two rows as double height text used oled.print(bme280.getHumidity(),1); //Write relative humidity to 1 decimal place. oled.clearToEOL(); //Erase rest of old value //Atmospheric pressure oled.setCursor(66,6); //Move to start of third row ( = 0 + (2 x (text row number - 1)) AtmosPressure = (bme280.getPressure()/100.0); //Retrieve and store pressure in variable after converting from Pascals to millibar (divide by 100) if (AtmosPressure >= 999.95) { oled.print(AtmosPressure,0); //Write atmospheric pressure to zero decimal places } else { oled.print(AtmosPressure,1); //Write atmospheric pressure to 1 decimal place } oled.clearToEOL(); //Erase rest of old value } if (DisplayMode == DiagnosticDisplayMode) //Diagnostics display { oled.setCursor(66,2); if (AverageAngle <= -99.95) { oled.print(AverageAngle,0); //0 decimal places if less than -100 degrees , or the text won't fit on the display } else { oled.print(AverageAngle,1); //Otherwise display with 1 decimal place } oled.clearToEOL(); //Clear remainder ofcurrent value, if text is overwritten and has less digits it will not completely clear the old value oled.setCursor(66,4); if (AverageTilt <= -99.95) { oled.print(AverageTilt,0); //0 decimal places if less than -100 degrees , or the text won't fit on the display } else { oled.print(AverageTilt,1); //Otherwise display with 1 decimal place } oled.clearToEOL(); //Clear remainder of current value, if text is overwritten and has less digits it will not completely clear the old value } //Test code to display number of autosaves //oled.setCursor(0,0); //oled.print(AutoSaveCount); OldDisplayMode = DisplayMode; //Store previous display mode so the fixed text is updated when the mode is changed LastDispUD = millis(); //Note time when display last updated } //Average value has been processed, reset averaging for new averaging period AverageAngleDatum = millis(); //Start time of averaging period NAngleMeasurements = 0; //Zero measurements taken SumAngle = 0.0; //Sum of values is zero SumTilt = 0.0; //Autosave if stationary for at least the minimum period, minimum period between saves, values have changed and autosave enabled (not 0) if(IntervalCheck(AutoSaveIdleDatum,AutoSaveIdleTime) && IntervalCheck(AutoSaveTimeDatum,AutoSaveIdleTime) && AutoSaveValuesChanged && AutoSaveMode) { AutoSave(); } } } } //Functions below here are called by the setup or loop functions, or each other //__________ //Function to save values to autosave location void AutoSave() { //Store current values to EEPROM autosave location AutoSaveLoc = AutoSaveNextLocation(); unsigned int AddrOffset = (MaxSaveLocation + AutoSaveLoc - 1) * SaveSetByteSize; EEPROMWriteUint16(SavedMaxAngleAddress + AddrOffset,(unsigned int)(MaxAngle * 10.0)); //Maximum angle EEPROMWriteUint32(SavedMaxWheelieTimeAddress + AddrOffset,(unsigned long)MaxWheelieTime); //Maximum wheelie time EEPROMWriteUint32(SavedTotalDistanceAddress + AddrOffset,(unsigned long)TotalDistance); //Total ride distance EEPROMWriteUint32(SavedTotalTimeAddress + AddrOffset,(unsigned long)TotalTime); //Total ride time EEPROMWriteUint32(SavedMaxSpeedAddress + AddrOffset,(unsigned long)MaxSpeed); //Max Speed EEPROMWriteUint32(SavedTotalEnergyAddress + AddrOffset,(unsigned long)(Energy * 10.0)); //Total energy EEPROM.update(SavedDisplayModeAddress + AddrOffset, DisplayMode); //Display Mode AutoSaveSetLocation(); //Set current autosave location AutoSaveValuesChanged = 0; AutoSaveTimeDatum = millis(); AutoSaveCount++; } //__________ //Function to return last used Autosave location (for wear levelling) byte AutoSaveLastLocation() { for (byte SaveLoc = 1; SaveLoc <= MaxAutoSaveLocation; SaveLoc++) { if(EEPROM.read(SavedValuesAvailableAddress + ((MaxSaveLocation + SaveLoc - 1) * SaveSetByteSize)) == 100) { return SaveLoc; //First identified autosave location flagged as in use } } return MaxAutoSaveLocation; //No autosave locations in use, will be detected by further check on reading data } //__________ //Function to return next used Autosave location (for wear levelling) byte AutoSaveNextLocation() { byte NextLoc = AutoSaveLastLocation() + 1; if(NextLoc > MaxAutoSaveLocation) { NextLoc = 1; } return NextLoc; } //__________ //Function to set current Autosave location flags (for wear levelling) void AutoSaveSetLocation() { for (byte SaveLoc = 1; SaveLoc <= MaxAutoSaveLocation; SaveLoc++) { if (SaveLoc == AutoSaveLoc) { EEPROM.update(SavedValuesAvailableAddress + ((MaxSaveLocation + SaveLoc - 1) * SaveSetByteSize),100); } else { EEPROM.update(SavedValuesAvailableAddress + ((MaxSaveLocation + SaveLoc - 1) * SaveSetByteSize),0); } } } //__________ //Function to check if display button is pressed - for use in setup byte CheckDisplayButton() { //Must exceed minimum interval between keypresses to avoid multiple triggers from a single button press if(IntervalCheck(OldDisplayButtonTime, MaxDisplayButtonInterval)) { //Read the button value if (!digitalRead(PinDisplayButton)) //Button is LOW when pressed { OldDisplayButtonTime = millis(); return 1; //Button pressed } else { return 0; } } return 0; } //__________ //Function to write fixed text to display when initialising device of after changing display mode void DisplaySetup() { oled.clear(); //Clear the display if(DisplayMode == WheelieDisplayModeAT) { oled.println(F("Angle")); //Write text. Println moves to next line after writing text. F() stores text in program memory, not data memory oled.println(F("Max")); oled.println(F("Time")); oled.print(F("Max s")); //Degree symbol not available in standard ASCII character set, write half size "o" in right place on first row oled.set1X(); //Change to small font oled.setCursor(51,2); //Set position (4 double size characters at 2x5 pixels wide each, 2 spaces of 2x1 pixels each plus 4 pixels to centre the symbol) oled.print(F("o")); //"Degree" symbol oled.set2X(); //Return to double size font } if(DisplayMode == WheelieDisplayModeTime) { oled.setFont(ZevvPeep8x16); oled.set1X(); oled.print(F("Wheelie Time s")); oled.setFont(System5x7); oled.set2X(); } if(DisplayMode == SpeedDisplayMode) { oled.print(F("Speed")); //Display speed units oled.setCursor(73,0); for (byte CharNo = 0; CharNo < SpeedNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(SpeedNames [SpeedUnits][CharNo]); } } if(DisplayMode == DistanceDisplayMode) { oled.print(F("Odometer")); } if(DisplayMode == RideDisplayMode) { oled.print(F("Ride Stats")); //Write text. Println moves to next line after writing text. F() stores text in program memory, not data memory //Units for distance not set as depends whether a small or large distance //Units for time not set as depends on value //Units for speed oled.setCursor(0,6); for (byte CharNo = 0; CharNo < SpeedNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(SpeedNames [SpeedUnits][CharNo]); } } if(DisplayMode == SpeedStatsDisplayMode) { oled.print(F("Speed")); //Display speed units oled.setCursor(72,0); for (byte CharNo = 0; CharNo < SpeedNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(SpeedNames [SpeedUnits][CharNo]); } oled.setCursor(0,2); oled.println(F("Inst")); oled.println(F("Ave")); oled.println(F("Max")); } if(DisplayMode == EnergyDisplayMode) { oled.print(F("Energy")); //Display energy units oled.setCursor(85,0); for (byte CharNo = 0; CharNo < EnergyNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(EnergyNames [EnergyUnits][CharNo]); } } if(DisplayMode == PowerDisplayMode) { oled.print(F("Inst Power")); //Display power units oled.set1X(); //Change to small font oled.setCursor(0,7); for (byte CharNo = 0; CharNo < PowerNamesLen - 1; CharNo++) //Loop over all characters except null termination { oled.print(PowerNames [PowerUnits][CharNo]); } oled.set2X(); //Change to larger font, the default } if(DisplayMode == ClockDisplayMode) { //No fixed text for clock display mode OldMinute = 60; //Ensure clock updates as soom as possible } if(DisplayMode == WeatherDisplayMode) { oled.println(F("Weather")); oled.println(F("T C")); //Temperature in degrees Celcius. Write text. Println moves to next line after writing text. F() stores text in program memory, not data memory oled.println(F("RH %")); //Relative humidity in percent oled.println(F("P mb")); //Atmospheric pressure in millibar //Degree symbol not available in standard ASCII character set, write half size "o" in right place on first row oled.set1X(); //Change to small font oled.setCursor(38,2); //Set position (3 double size characters at 2x5 pixels wide each, 2 spaces of 2x1 pixels each plus 4 pixels to centre the symbol) oled.println(F("o")); //"Degree" symbol oled.set2X(); //Return to double size font } if(DisplayMode == DiagnosticDisplayMode) { oled.println(F("Diagnostic")); oled.println(F("Angle")); oled.println(F("Tilt")); oled.print(F("Ver")); oled.setCursor(66,6); oled.print(SWVersion); } } //__________ //Read an unsigned 16 bit integer from 2 EEPROM addresses unsigned int EEPROMReadUint16(unsigned int StartAddress) { byte TempBytes[2]; //Temporary store for the upper and lower byte for (byte j = 0; j < 2; j++) { TempBytes[j] = EEPROM.read(StartAddress + j); //First argument is address, second is value } return ((int)(TempBytes[0]) << 8) + ((int)(TempBytes[1])); } //__________ //Read an unsigned 32 bit integer from 4 EEPROM addresses unsigned long EEPROMReadUint32(unsigned int StartAddress) { byte TempBytes[4]; //Temporary store for the bytes, unsigned long is four bytes for (byte j = 0; j < 4; j++) { TempBytes[j] = EEPROM.read(StartAddress + j); //Argument is address } //Convert to unsigned long return ((long)(TempBytes[0]) << 24) + ((long)(TempBytes[1]) << 16) + ((long)(TempBytes[2]) << 8) + ((long)(TempBytes[3])); } //__________ //Write an unsigned 16 bit integer to 2 EEPROM addresses void EEPROMWriteUint16(unsigned int StartAddress, unsigned int ValToWrite) { //Check the data will fit in EEPROM, E2END returns maximum address which varies by Arduino model if(StartAddress < E2END) { byte TempBytes[2]; TempBytes[0] = (byte) ((ValToWrite & 0x0000FF00) >> 8 ); TempBytes[1] = (byte) ((ValToWrite & 0X000000FF) ); for (byte j = 0; j < 2; j++) { EEPROM.update(StartAddress + j, TempBytes[j]); //First argument is address, second is value } } } //__________ //Write an unsigned 32 bit integer to 4 EEPROM addresses void EEPROMWriteUint32(unsigned int StartAddress, unsigned long ValToWrite) { //Check the data will fit in EEPROM, E2END returns maximum address which varies by Arduino model if(StartAddress + 3 <= E2END) { byte TempBytes[4]; //Temporary store for the bytes, unsigned long is four bytes TempBytes[0] = (byte) ((ValToWrite & 0xFF000000) >> 24 ); TempBytes[1] = (byte) ((ValToWrite & 0x00FF0000) >> 16 ); TempBytes[2] = (byte) ((ValToWrite & 0x0000FF00) >> 8 ); TempBytes[3] = (byte) ((ValToWrite & 0X000000FF) ); for (byte j = 0; j < 4; j++) { EEPROM.update(StartAddress + j, TempBytes[j]); //First argument is address } } } //__________ //Function to determine if current time exceeds a datum time by a minimum amount //Return 1 if the time is exceeded or 0 otherwise byte IntervalCheck(unsigned long Datum, unsigned long Interval) { unsigned long CurrentTime = millis(); //Read current time in milliseconds if ((CurrentTime - Datum) > Interval) //This still works with rollover of millis { return 1; //Interval has been exceeded } else { return 0; //Interval not exceeded } } //__________ //Function to display brightness setting //maps brightness setting (0-255) to Low, Medium or High void PrintBrightness() { if(Brightness < 64) { oled.print(F("Low")); } else { if(Brightness < 191) { oled.print(F("Medium")); } else { oled.print(F("High")); } } oled.clearToEOL(); } //__________ //Function to read value of display button input, increment DisplayMode by 1 if pressed (allowing for debounce) void ReadDisplayButton() { //Must exceed minimum interval between keypresses to avoid multiple triggers from a single button press if(IntervalCheck(OldDisplayButtonTime, MaxDisplayButtonInterval)) { //Read the button value if (!digitalRead(PinDisplayButton)) //Button is LOW when pressed { OldDisplayButtonTime = millis(); //Store time of this button press for debounce DisplayMode++; //Increment display mode //Checks that hardware available - must be in same order as in DisplayMode if ((DisplayMode == SpeedDisplayMode) && (!RevSensorAvailable)) //If mode uses rev sensor and sensor not available, select the next mode { DisplayMode++; //Increment display mode } if ((DisplayMode == DistanceDisplayMode) && (!RevSensorAvailable)) //If mode uses rev sensor and sensor not available, select the next mode { DisplayMode++; //Increment display mode } if ((DisplayMode == RideDisplayMode) && (!RevSensorAvailable)) //If mode uses rev sensor and sensor not available, select the next mode { DisplayMode++; //Increment display mode } if ((DisplayMode == SpeedStatsDisplayMode) && (!RevSensorAvailable)) //If mode uses rev sensor and sensor not available, select the next mode { DisplayMode++; //Increment display mode } if ((DisplayMode == EnergyDisplayMode) && (!RevSensorAvailable)) //If mode uses rev sensor and sensor not available, select the next mode { DisplayMode++; //Increment display mode } if ((DisplayMode == PowerDisplayMode) && (!RevSensorAvailable)) //If mode uses rev sensor and sensor not available, select the next mode { DisplayMode++; //Increment display mode } if ((DisplayMode == ClockDisplayMode) && (!ClockAvailable)) //If clock mode and clock not available, select the next mode { DisplayMode++; //Increment display mode } if ((DisplayMode == WeatherDisplayMode) && (!WeatherStationAvailable)) //If weather station mode and weather station not available, select the next mode { DisplayMode++; //Increment display mode } if(DisplayMode > MaxDisplayMode) //If display mode exceeds maximum available mode, cycle back to first mode { DisplayMode = 0; } AutoSaveValuesChanged = 1; //Need to autosave display mode } } } //__________ //Function to read value of save button input, save values if pressed (allowing for debounce) void ReadSaveButton() { //Must exceed minimum interval between keypresses to avoid multiple triggers from a single button press if(IntervalCheck(OldSaveButtonTime, MaxSaveButtonInterval)) { //Read the button value if (!digitalRead(PinSaveButton)) //Button is LOW when pressed { OldSaveButtonTime = millis(); //Store time of this button press for debounce //Prompt to release button oled.clear(); oled.println(F("Save")); oled.println(F("Values:")); oled.println(F("Release")); oled.println(F("Button!")); //Wait for button to be released do { } while (!digitalRead(PinSaveButton)); //Prompt for memory address oled.clear(); oled.println(F("Select")); oled.println(F("Memory")); oled.println(F("Location:")); if(LastSaveLoc > MaxSaveLocation) //Following a Reset All { LastSaveLoc = 0; } oled.print(LastSaveLoc); if(LastSaveLoc == 0) { oled.print(F(" (Auto)")); } //Wait for button presses to change value OldDisplayButtonTime = millis(); //Reset timeout for next element do { if (CheckDisplayButton()) { //Button pressed: increment value LastSaveLoc ++; if(LastSaveLoc > MaxSaveLocation) //Jump to minimum value { LastSaveLoc = 0; } //Update display oled.setCursor(0,6); oled.print(LastSaveLoc); if(LastSaveLoc == 0) { oled.print(F(" (Auto)")); } oled.clearToEOL(); } } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout)); //After timeout, proceed unsigned int AddrOffset; if (LastSaveLoc == 0) { //Autosave location selected (0) AutoSaveLoc = AutoSaveNextLocation(); AddrOffset = (MaxSaveLocation + AutoSaveLoc - 1) * SaveSetByteSize; } else { AddrOffset = (LastSaveLoc - 1) * SaveSetByteSize; } //Store current values to EEPROM, offset by the save location number multiplied by the size allocated to each set of saved values EEPROMWriteUint16(SavedMaxAngleAddress + AddrOffset,(unsigned int)(MaxAngle * 10.0)); //Maximum angle EEPROMWriteUint32(SavedMaxWheelieTimeAddress + AddrOffset,(unsigned long)MaxWheelieTime); //Maximum wheelie time EEPROMWriteUint32(SavedTotalDistanceAddress + AddrOffset,(unsigned long)TotalDistance); //Total ride distance EEPROMWriteUint32(SavedTotalTimeAddress + AddrOffset,(unsigned long)TotalTime); //Total ride time EEPROMWriteUint32(SavedMaxSpeedAddress + AddrOffset,(unsigned long)MaxSpeed); //Max Speed EEPROMWriteUint32(SavedTotalEnergyAddress + AddrOffset,(unsigned long)(Energy * 10.0)); //Total energy EEPROM.update(SavedDisplayModeAddress + AddrOffset, DisplayMode); //Display Mode //Reset Autosave parameters if saved in location 0 if(LastSaveLoc == 0) { AutoSaveSetLocation(); //Set current autosave location AutoSaveValuesChanged = 0; AutoSaveTimeDatum = millis(); } else { EEPROM.update(SavedValuesAvailableAddress + AddrOffset, 100); //Values available in manual save location } //Confirmation message oled.clear(); oled.println(F("Values")); oled.print(F("Saved")); OldDisplayButtonTime = millis(); //Reset timeout for clearing confirmation message //Force display refresh if (DisplayMode > 0) { OldDisplayMode = DisplayMode - 1; } else { OldDisplayMode = MaxDisplayMode; } do { } while (!IntervalCheck(OldDisplayButtonTime, SetupTimeout/2)); //After timeout, proceed } } } //__________ //Function to read rev sensor and increment rev count (allowing for debounce) void ReadRevSensor() { if (!digitalRead(PinRevSensor)) //Pin is Low when contact closed { if (NewRevPulse) //Contact has opened since last rev was counted { if(IntervalCheck(OldRevTime, MinRevSensorTime)) //Minimum time between contact closures has been exceeded { OldRevTime = millis(); //Store time of this contact closure for debounce RevCount++; //Increment rev counter NewRevPulse = 0; //Only record next contact closed state after contact has been released } } } else //Pin high: contact open { NewRevPulse = 1; //Contact released, so increment rev count on next contact closure subject to debounce between contact closures } }