// ---------------------------------------------------------------------------------------------------------------------
// Rower Console With Hall Sensor
// Written by Martin Maillardet for ESP32 Dev Module
// V0.1 August 2021
//
// Hardware requirements:
// * ESP32-WROOM-32
// * 20 x 4 character LCD screen.
// * Push button for pause/resume. (on digital pin 2)
// * Push button for cycling through display modes. (on digital pin 3)
// * Rotary Encoder
// * Hall sensor which should be attached rower to sense when the rolling sit pass over. (on digital pin 4)
//
// Noteworthy features:
// * Computes time counted in ascending order (in hours, minutes and seconds), distance in kilometers,
// speed in kilometers per hour.
// * Computes strokes, average strokes per hour and maximum strokes per hour, rowing time, kilometers rowed,
// average kilometers per hour, maximum kilometers per hour, speed, maximum speed and average speed for all sessions/periods recorded.
// (shown in "S##" data when looking at session data in pause mode)
// * Computes total strokes, average strokes per hour and maximum strokes per hour, total time, total kilometers rowed,
// average kilometers per hour and maximum kilometers per hour rowed for all sessions/periods recorded.
// (shown in "Tot" data when looking at session data in pause mode)
// * Stores up to 49 sessions/periods which can be viewed when in pause mode by pressing the Display Mode button
// NOTE: 50th session gets recorded in position for session 49 thus overriding data for 49th session.
// * No data is being recorded while in pause mode.
//
//
// Thanks to Alan De Windt (alan_dewindt@yahoo.com), the coding of distance, speed and lap counter is based on his own "Bicycle Odometer & Speedometer"
// (https://create.arduino.cc/projecthub/alan_dewindt/bicycle-odometer-and-speedometer-with-99-lap-period-recorder-331d2b?ref=tag&ref_id=speedometer&offset=0)
// Thanks to Cimanes for the cadence code based on his "Bike trainer logger"
// (https://create.arduino.cc/projecthub/cimanes/bike-trainer-logger-8d97d6?ref=search&ref_id=bike&offset=3)
// Thanks to Curious Scientist, the code part of menu is based on his "Advanced menu system with rotary encoder"
// (https://curiousscientist.tech/blog/20x4lcd-rotaryencoder-menu)
//
// ---------------------------------------------------------------------------------------------------------------------
#include "AiEsp32RotaryEncoder.h"
#define ROTARY_ENCODER_A_PIN 32
#define ROTARY_ENCODER_B_PIN 33
#define ROTARY_ENCODER_BUTTON_PIN 25
#define ROTARY_ENCODER_STEPS 4
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS);
#include <EEPROM.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 20, 4); //------- Set the LCD address to 0x27 for a 20 chars and 4 line lcd
#define EEPROM_SIZE 512
//---------------------------------------------- Menu Variables
int menuCounter = 0; //counts the clicks of the rotary encoder between menu items (0-3 in this case)
bool distBetwin2Strokes_selected = false; //enable/disable to change the value of menu item
bool resistanceLevel_selected = false;
bool strokeCountSessionTarget_selected = false;
bool strokeCountTotalTarget_selected = false;
//Note: if a menu is selected ">" becomes "X".
bool refreshLCD = true; //refreshes values
bool refreshSelection = false; //refreshes selection (> / X)
//---------------------------------------------- Counter Variables
float distBetwin2Strokes = 6.1; //------- Distance betwin too strokes expressed in Meters
float resistanceLevel = 2.2; //------- Resistance Level of the Rower
int strokeCountSessionTarget = 500;
int strokeCountTotalTarget = 1500;
const int PAUSEBUTTON = 16;
boolean lastPauseButton = LOW;
boolean currentPauseButton = LOW;
const int DISPLAYMODEBUTTON = 17;
boolean lastDisplayModeButton = LOW;
boolean currentDisplayModeButton = LOW;
const int STROKESENSOR = 18;
boolean lastStrokeSensor = LOW;
boolean currentStrokeSensor = LOW;
boolean startShown = HIGH;
unsigned long startShownTime = 0;
unsigned long blinkStartMessageDelay = 3000;
boolean paused = LOW;
boolean pausedShown = LOW;
unsigned long pausedStartTime = 0;
boolean strokeActionShown = LOW;
unsigned long strokeActionShownStartTime = 0;
boolean letsGoShown = LOW;
unsigned long letsGoStartTime = 0;
unsigned long lastStrokeStartTime = 0;
unsigned long strokingTime = 0;
unsigned long lastMinuteStrokeStartTime = 0;
int currentDisplayMode = 0;
int showSession = 0;
int sessionCurrentlyShown = 50;
int currentSession = 0;
float currentDistance;
unsigned long currentDuration;
int currentMaximumKPH;
int currentAverageKPH;
int currentKPH;
int averageDistancePerMinute;
float arrayDistance[50];
unsigned long arrayDuration[50];
int arrayMaximumKPH[50];
int arrayAverageKPH[50];
int arrayDistancePerMinute[50];
unsigned long arrayStrokeCount[50];
byte arrayMaximumCadence[50];
byte arrayAverageCadence[50];
byte arrayCadence[50];
unsigned int currentSitCount = 0;
unsigned int currentStrokeCount = 0;
unsigned int lastStrokeCount = 0;
unsigned int lastMinuteStrokeCount = 0;
unsigned int lastMinuteStrokeCounter = 0;
unsigned long currentTime = 0;
unsigned long sessionStartTime = 0;
float km = 0.00;
float kph = 0.00;
int intHours;
int intMinutes;
int intSeconds;
const unsigned long MILLISECONDSINSECOND = 1000;
const unsigned long MILLISECONDSINMINUTE = 60000;
const unsigned long MILLISECONDSINHOUR = 3600000;
//---------------------------------------------- Cadence Variables
const int DELAYTIME2CADENCEPULSEMAX = 5000 ; // Max allowed time delay between cadence pulses
const int DELAYTIME2CADENCEPULSEMIN = 200 ; // Min time delay between cadence pulses
const int DELAYTIMEPRINTCADENCE = 1000 ; // Time delay for serial print refresh
unsigned long tprint = 0 ; // Time reference in miliseconds for signal print
byte currentCadence = 0 ; // Cadence (rpm)
byte Cadence0 = 0 ; // Cadence reference for filter
byte Dcad = 10 ; // Max cadence difference
byte currentAverageCadence = 0 ; // Average cadence (pedals / min)
byte currentMaximumCadence = 0 ; // Maximum cadence (pedals / min)
const byte HALL_SENSOR_LED = 26;
//***********************************************************************************************************************************************
// SETUP *
//***********************************************************************************************************************************************
void setup()
{
Serial.begin(115200); // begin in 115200 baud
//---------------------------------------------- Initialize Rotary
rotaryEncoder.begin();
rotaryEncoder.setup(
[] { rotaryEncoder.readEncoder_ISR(); },
[] { rotary_onButtonClick(); });
//---------------------------------------------- Configure digital input pins for push buttons and Hall sensor
pinMode (STROKESENSOR, INPUT);
pinMode (PAUSEBUTTON, INPUT);
pinMode (DISPLAYMODEBUTTON, INPUT);
pinMode (HALL_SENSOR_LED, OUTPUT);
//---------------------------------------------- Initialize maximum KPH in totals as this may not be calculated if no maximum was computed for sessions
//---------------------------------------------- and there may be random data in memory location
arrayMaximumKPH[0] = 0;
//---------------------------------------------- Initialize LCD screen
lcd.begin(20, 4);
lcd.init();
lcd.backlight();
lcd.clear();
EEPROM.begin(EEPROM_SIZE);
delay(500);
distBetwin2Strokes = EEPROM.readFloat(0);
resistanceLevel = EEPROM.readFloat(8);
strokeCountSessionTarget = EEPROM.readInt(16);
strokeCountTotalTarget = EEPROM.readInt(24);
}
//***********************************************************************************************************************************************
// LOOP *
//***********************************************************************************************************************************************
void loop()
{
//---------------------------------------------- Get current millis
currentTime = millis();
//---------------------------------------------- show "PRESS BUTTON TO START"
if (startShown)
{
StartMessage();
}
//---------------------------------------------- Read Stroke Hall sensor
currentStrokeSensor = debounce(lastStrokeSensor, STROKESENSOR);
if (lastStrokeSensor == HIGH && currentStrokeSensor == LOW)
{
//---------------------------------------------- If initial "PRESS BUTTON TO START" is not displayed and not currently paused...
if (!startShown && !paused)
{
//---------------------------------------------- Increase Stroke count
currentSitCount++;
digitalWrite(HALL_SENSOR_LED, HIGH);
delay(20);
digitalWrite(HALL_SENSOR_LED, LOW);
if (currentSitCount%2)
{
currentStrokeCount++;
//---------------------------------------------- Display "+" to show that one stroke was recorded
lcd.setCursor(4, 0);
lcd.print(F("+"));
strokeActionShown = HIGH;
strokeActionShownStartTime = currentTime;
//---------------------------------------------- Compute millis it took for this latest stroke
if (lastStrokeStartTime > 0)
{
strokingTime = currentTime - lastStrokeStartTime;
//---------------------------------------------- Compute current speed in kilometers per hour based on time it took to complete last stroke
kph = (3600000 / strokingTime) * distBetwin2Strokes / 1000;
currentKPH = kph;
//---------------------------------------------- If current speed is new maximum speed for this session then store it
if (currentMaximumKPH < currentKPH)
{
currentMaximumKPH = currentKPH;
}
}
lastStrokeStartTime = currentTime;
}
}
}
lastStrokeSensor = currentStrokeSensor;
//---------------------------------------------- Calculate Cadence
//---------------------------------------------- Cadence calculation upon cadence pulse
if ((currentStrokeCount > 1) and strokingTime > DELAYTIME2CADENCEPULSEMIN)
{
if (Cadence0 < 15) Dcad = 8;
else Dcad = 5;
//---------------------------------------------- Calculate the cadence
currentCadence = (60000 / strokingTime);
//---------------------------------------------- Filter
currentCadence = constrain(currentCadence, max(0, Cadence0 - Dcad), Cadence0 + Dcad);
Cadence0 = currentCadence; //------- Refresh Cadence reference for filter
//----------------------------------------------If current cadence is new maximum cadence for this session then store it
if (currentMaximumCadence < currentCadence)
{
currentMaximumCadence = currentCadence;
}
//----------------------------------------------Force cadence to "0" when no pulses are detected for the pre-selected time delay:
if ((currentTime - lastStrokeStartTime) > DELAYTIME2CADENCEPULSEMAX) currentCadence = 0;
//----------------------------------------------Calculate average values
if (currentTime - strokingTime < 2000) currentAverageCadence = currentCadence; //------- Delay before begin calculation of average cadence (2000)
else currentAverageCadence = (currentStrokeCount * 60000 / (currentTime - sessionStartTime)) ; //------- Average cadence (strokes / min)
//----------------------------------------------Calculate strokes last minute
if (currentTime - sessionStartTime < 60000)
{ //------ Delay before begin calculation of strokes last minute (60000)
lastMinuteStrokeCounter = currentStrokeCount;
lastMinuteStrokeStartTime = sessionStartTime;
}
if (currentTime - sessionStartTime > 60000)
{
if (currentTime - lastMinuteStrokeStartTime >= 60000)
{
lastMinuteStrokeCount = (currentStrokeCount - lastStrokeCount);
lastMinuteStrokeStartTime = currentTime;
lastStrokeCount = currentStrokeCount;
}
lastMinuteStrokeCounter = (currentStrokeCount - lastStrokeCount);
}
}
PressPauseButton();
PressModeButton();
MessageTiming();
//---------------------------------------------- If "PUSH BUTTON TO START" is not showing and not currently paused...
if (!startShown && !paused)
{
//---------------------------------------------- Compute milliseconds since start of session
currentDuration = currentTime - sessionStartTime;
//---------------------------------------------- Compute distance and average kilometers per hour if stroke has moved
if (currentStrokeCount > 0)
{
//---------------------------------------------- Compute kilometers rowed
//---------------------------------------------- Distance betwin too strokes is in meters
currentDistance = currentStrokeCount * distBetwin2Strokes / 1000;
//---------------------------------------------- Compute average kilometers per hour since start of session
currentAverageKPH = currentDistance * 3600000 / currentDuration;
}
}
//---------------------------------------------- If no messages are currently showing then update data on display
if (!startShown && !letsGoShown && !pausedShown)
{
if (currentDisplayMode < 3)
{
if (currentDisplayMode == 1)
{
UpdateDataOnDisplay();
rotaryEncoder.disable();
}
//---------------------------------------------- When Pressing Mode Button
else
{
rotaryEncoder.enable();
rotary_loop();
if(refreshLCD == true) //If we are allowed to update the LCD ...
{
updateLCD(); // ... we update the LCD ...
//... also, if one of the menus are already selected...
if(distBetwin2Strokes_selected == true || resistanceLevel_selected == true || strokeCountSessionTarget_selected == true || strokeCountTotalTarget_selected == true)
{
// do nothing
}
else
{
rotaryEncoder.setBoundaries(0, 3, false); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
rotaryEncoder.setAcceleration(0);
if (menuCounter != rotaryEncoder.readEncoder())
{
rotaryEncoder.reset (menuCounter);
}
updateCursorPosition(); //update the position
}
refreshLCD = false; //reset the variable - wait for a new trigger
}
if(refreshSelection == true) //if the selection is changed
{
updateSelection(); //update the selection on the LCD
refreshSelection = false; // reset the variable - wait for a new trigger
}
}
}
//---------------------------------------------- Otherwise device is paused so show historical session information
else
{
ShowHistoricalSessionsInformations();
rotaryEncoder.enable();
RotaryHistoricalSessionsInformations();
}
}
}
//***********************************************************************************************************************************************
// START MESSAGE *
//***********************************************************************************************************************************************
void StartMessage ()
{
if ((currentTime - startShownTime > 0) && (currentTime - startShownTime < blinkStartMessageDelay))
{
lcd.setCursor(0, 1);
lcd.print(F("----PRESSEZ SUR-----"));
lcd.setCursor(0, 2);
lcd.print(F("----DEBUT/PAUSE-----"));
}
if ((currentTime - startShownTime > blinkStartMessageDelay) && (currentTime - startShownTime < blinkStartMessageDelay + 50))
{
lcd.clear();
}
if ((currentTime - startShownTime > blinkStartMessageDelay + 100) && (currentTime - startShownTime < blinkStartMessageDelay * 2))
{
lcd.setCursor(0, 0);
lcd.print(F("Dist/CdR (m) -> "));
lcd.setCursor(16,0); //1st line, 10th block
lcd.print(F(" ")); //erase the content by printing space over it
lcd.setCursor(16,0); //1st line, 10th block
if (distBetwin2Strokes < 10)
{
lcd.print (F(" "));
lcd.print(distBetwin2Strokes,1);
}
else
{
lcd.print(distBetwin2Strokes,1); //print the value of distBetwin2Strokes variable
}
//-----------------------------------
lcd.setCursor(0, 1);
lcd.print(F("Niv de Force -> "));
lcd.setCursor(16,1);
lcd.print(F(" "));
lcd.setCursor(16,1);
if (resistanceLevel < 10)
{
lcd.print (F(" "));
lcd.print(resistanceLevel,1);
}
else
{
lcd.print(resistanceLevel,1);
}
//---------------------
lcd.setCursor(0, 2);
lcd.print(F("CdR/Seance -> "));
lcd.setCursor(16,2);
lcd.print(F(" "));
lcd.setCursor(16,2);
if (strokeCountSessionTarget < 10)
{
lcd.print (F(" "));
lcd.print(strokeCountSessionTarget);
}
else if (strokeCountSessionTarget < 100)
{
lcd.print (F(" "));
lcd.print(strokeCountSessionTarget);
}
else if (strokeCountSessionTarget < 1000)
{
lcd.print (F(" "));
lcd.print(strokeCountSessionTarget);
}
else
{
lcd.print(strokeCountSessionTarget); //
}
//---------------------
lcd.setCursor(0, 3);
lcd.print(F("Obj CdR Tot -> "));
lcd.setCursor(16,3);
lcd.print(F(" "));
lcd.setCursor(16,3);
if (strokeCountTotalTarget < 10)
{
lcd.print (F(" "));
lcd.print(strokeCountTotalTarget);
}
else if (strokeCountTotalTarget < 100)
{
lcd.print (F(" "));
lcd.print(strokeCountSessionTarget);
}
else if (strokeCountTotalTarget < 1000)
{
lcd.print (F(" "));
lcd.print(strokeCountTotalTarget);
}
else
{
lcd.print(strokeCountTotalTarget); //
}
}
if (currentTime - startShownTime > blinkStartMessageDelay * 2)
{
lcd.clear();
startShownTime = currentTime;
}
}
//***********************************************************************************************************************************************
// COMPUTE TIME *
//***********************************************************************************************************************************************
//---------------------------------------------- Compute hours, minutes and seconds for given duration expressed in milliseconds
void computeHMS(unsigned long duration)
{
float floatHours;
float floatMinutes;
float floatSeconds;
intHours = 0;
intMinutes = 0;
intSeconds = 0;
if (duration >= 1000)
{
floatSeconds = duration / MILLISECONDSINSECOND % 60;
intSeconds = floatSeconds;
floatMinutes = duration / MILLISECONDSINMINUTE % 60;
intMinutes = floatMinutes;
floatHours = duration / MILLISECONDSINHOUR % 24;
intHours = floatHours;
}
}
//***********************************************************************************************************************************************
// START/PAUSE BUTTON *
//***********************************************************************************************************************************************
//---------------------------------------------- When Pause Button is pressed
void PressPauseButton()
{
//---------------------------------------------- Read PAUSE/RESUME push button
currentPauseButton = debounce(lastPauseButton, PAUSEBUTTON);
if (lastPauseButton == LOW && currentPauseButton == HIGH)
{
//---------------------------------------------- If "PRESS BUTTON TO START" message has been showing then we now need to start 1st session/period
if (startShown)
{
startShown = LOW;
//---------------------------------------------- Show "C'EST PARTI!" message
ShowLetsGo();
letsGoShown = HIGH;
letsGoStartTime = currentTime;
currentSession = 1;
resetSessionVariables();
currentDisplayMode = 1;
}
else
{
//---------------------------------------------- Otherwise if pause is active then we need to take it out of pause and start new session/period
if (paused)
{
paused = LOW;
//---------------------------------------------- Show "C'EST PARTI!" message
ShowLetsGo();
letsGoShown = HIGH;
letsGoStartTime = currentTime;
//---------------------------------------------- Increment session counter
currentSession++;
//---------------------------------------------- If we are starting a 100th session/period then we should write data into 99th array position (overwriting this session)
//---------------------------------------------- as we can only keep track of 99 sessions/periods in total
if (currentSession > 49)
{
currentSession = 49;
//---------------------------------------------- Pretend session 50 (out-of-bounds value) is currently shown (even though 99 is currently shown)
//---------------------------------------------- to force display of new data for session 49
sessionCurrentlyShown = 50;
}
resetSessionVariables();
currentDisplayMode = 1;
}
//---------------------------------------------- Otherwise pause is not currently active so we need to save session/period data and activate pause
else
{
paused = HIGH;
//---------------------------------------------- Calculate duration
currentDuration = currentTime - sessionStartTime;
//---------------------------------------------- If session duration is less than 2 seconds (which means user pressed the pause button while "LET'S GO" message
//---------------------------------------------- was shown) then do not store the session/ignore it
if (currentDuration < 3000)
{
currentSession--;
}
//---------------------------------------------- Otherwise store the session
else
{
//---------------------------------------------- Compute distance and average kilometers per hour if stroke moved
if (currentStrokeCount > 0)
{
currentDistance = currentStrokeCount * distBetwin2Strokes / 1000;
currentAverageKPH = currentDistance * 3600000 / currentDuration;
averageDistancePerMinute = (currentDistance * 60000 * 1000) / currentDuration;
}
//---------------------------------------------- Store data for session/period into array
arrayDistance[currentSession] = currentDistance;
arrayDuration[currentSession] = currentDuration;
arrayAverageKPH[currentSession] = currentAverageKPH;
arrayMaximumKPH[currentSession] = currentMaximumKPH;
arrayStrokeCount[currentSession] = currentStrokeCount;
arrayMaximumCadence[currentSession] = currentMaximumCadence;
arrayAverageCadence[currentSession] = currentAverageCadence;
arrayDistancePerMinute[currentSession] = averageDistancePerMinute;
// Update totals for all sessions/periods
arrayDistance[0] = arrayDistance[0] + currentDistance;
arrayDuration[0] = arrayDuration[0] + currentDuration;
arrayAverageKPH[0] = arrayDistance[0] * 3600000 / arrayDuration[0];
if (currentMaximumKPH > arrayMaximumKPH[0])
{
arrayMaximumKPH[0] = currentMaximumKPH;
}
arrayStrokeCount[0] = arrayStrokeCount[0] + currentStrokeCount;
if (currentMaximumCadence > arrayMaximumCadence[0])
{
arrayMaximumCadence[0] = currentMaximumCadence;
}
arrayAverageCadence[0] = arrayAverageCadence[0] + currentAverageCadence;
arrayDistancePerMinute[0] = (arrayDistance[0] * 60000 * 1000) / arrayDuration[0];
}
//---------------------------------------------- In case "LET'S GO!" has been showing, turn it off now since we want to show "PAUSED!" message
//---------------------------------------------- and we don't want it to be removed when "LET'S GO!" times out
letsGoShown = LOW;
//---------------------------------------------- Show "PAUSED!" message
showPaused();
pausedShown = HIGH;
pausedStartTime = currentTime;
//---------------------------------------------- We will need to show data for session which was just finished
showSession = currentSession;
currentDisplayMode = 3;
//---------------------------------------------- Set out-of-bounds value to sessionCurrentlyShown to force session data to be shown
sessionCurrentlyShown = 50;
}
}
}
lastPauseButton = currentPauseButton;
}
//***********************************************************************************************************************************************
// ROTARY ENCODER LOOP *
//***********************************************************************************************************************************************
void rotary_loop()
{
if(distBetwin2Strokes_selected == true)
{
if (rotaryEncoder.encoderChanged())
{
distBetwin2Strokes = (float)rotaryEncoder.readEncoder() / 10; // / 10.0
updateLCD();
}
}
else if(resistanceLevel_selected == true)
{
if (rotaryEncoder.encoderChanged())
{
resistanceLevel = (float)rotaryEncoder.readEncoder() / 10; // / 10.0
updateLCD();
}
}
else if(strokeCountSessionTarget_selected == true)
{
if (rotaryEncoder.encoderChanged())
{
strokeCountSessionTarget = rotaryEncoder.readEncoder();
updateLCD();
}
}
else if(strokeCountTotalTarget_selected == true)
{
if (rotaryEncoder.encoderChanged())
{
strokeCountTotalTarget = rotaryEncoder.readEncoder();
updateLCD();
}
}
else
{
if (rotaryEncoder.encoderChanged())
{
menuCounter = rotaryEncoder.readEncoder();
//Serial.println(menuCounter);
refreshLCD = true; //reset the variable - wait for a new trigger
}
}
}
//***********************************************************************************************************************************************
// ROTARY ENCODER BUTTON *
//***********************************************************************************************************************************************
void rotary_onButtonClick()
{
static unsigned long lastTimePressed = 0;
if (millis() - lastTimePressed < 200)
return;
lastTimePressed = millis();
switch(menuCounter)
{
case 0:
distBetwin2Strokes_selected = !distBetwin2Strokes_selected; //we change the status of the variable to the opposite
break;
case 1:
resistanceLevel_selected = !resistanceLevel_selected;
break;
case 2:
strokeCountSessionTarget_selected = !strokeCountSessionTarget_selected;
break;
case 3:
strokeCountTotalTarget_selected = !strokeCountTotalTarget_selected;
break;
}
refreshLCD = true; //Refresh LCD after changing the value of the menu
refreshSelection = true; //refresh the selection ("X")
}
//***********************************************************************************************************************************************
// MODE BUTTON *
//***********************************************************************************************************************************************
//---------------------------------------------- When Mode Button is pressed
void PressModeButton()
{
//---------------------------------------------- Read DISPLAY MODE push button
currentDisplayModeButton = debounce(lastDisplayModeButton, DISPLAYMODEBUTTON);
if (lastDisplayModeButton == LOW && currentDisplayModeButton == HIGH)
{
//---------------------------------------------- Otherwise if "LET'S GO!" message is not shown nor is "PAUSED!" message shown...
if (!startShown && !letsGoShown && !pausedShown)
{
//---------------------------------------------- If not currently paused (so session is ongoing)...
if (!paused)
{
//---------------------------------------------- Flip between the two different display modes available
if (currentDisplayMode == 1)
{
currentDisplayMode = 2;
updateLCD();
}
else
{
currentDisplayMode = 1;
eepromSave();
}
//---------------------------------------------- Clear display and show appropriate labels
showLabels(currentDisplayMode);
}
//---------------------------------------------- Otherwise we are in paused mode so cycle through session data available, including totals page
else
{
currentDisplayMode = 3;
showSession++;
if (showSession > currentSession)
{
showSession = 0; // Show totals
}
}
}
}
lastDisplayModeButton = currentDisplayModeButton;
if (currentDisplayMode == 1)
{
distBetwin2Strokes_selected = false;
resistanceLevel_selected = false;
strokeCountSessionTarget_selected = false;
strokeCountTotalTarget_selected = false;
}
}
//***********************************************************************************************************************************************
// UPDATE LCD *
//***********************************************************************************************************************************************
void updateLCD()
{
//MENU 0
lcd.setCursor(16,0); //1st line, 10th block
lcd.print(F(" ")); //erase the content by printing space over it
lcd.setCursor(16,0); //1st line, 10th block
if (distBetwin2Strokes < 10)
{
lcd.print (F(" "));
lcd.print(distBetwin2Strokes,1);
}
else
{
lcd.print(distBetwin2Strokes,1); //print the value of distBetwin2Strokes variable
}
//MENU 1
lcd.setCursor(16,1);
lcd.print(F(" "));
lcd.setCursor(16,1);
if (resistanceLevel < 10)
{
lcd.print (F(" "));
lcd.print(resistanceLevel,1);
}
else
{
lcd.print(resistanceLevel,1);
}
//MENU 2
lcd.setCursor(16,2);
lcd.print(F(" "));
lcd.setCursor(16,2);
if (strokeCountSessionTarget < 10)
{
lcd.print (F(" "));
lcd.print(strokeCountSessionTarget);
}
else if (strokeCountSessionTarget < 100)
{
lcd.print (F(" "));
lcd.print(strokeCountSessionTarget);
}
else if (strokeCountSessionTarget < 1000)
{
lcd.print (F(" "));
lcd.print(strokeCountSessionTarget);
}
else
{
lcd.print(strokeCountSessionTarget); //
}
//MENU 3
lcd.setCursor(16,3);
lcd.print(F(" "));
lcd.setCursor(16,3);
if (strokeCountTotalTarget < 10)
{
lcd.print (F(" "));
lcd.print(strokeCountTotalTarget);
}
else if (strokeCountTotalTarget < 100)
{
lcd.print (F(" "));
lcd.print(strokeCountSessionTarget);
}
else if (strokeCountTotalTarget < 1000)
{
lcd.print (F(" "));
lcd.print(strokeCountTotalTarget);
}
else
{
lcd.print(strokeCountTotalTarget); //
}
refreshLCD = true;
}
//***********************************************************************************************************************************************
// PRINT & MENU NAVIGATION *
//***********************************************************************************************************************************************
void updateCursorPosition()
{
//Clear display's ">" parts
lcd.setCursor(0,0); //1st line, 1st block
lcd.print(F(" ")); //erase by printing a space
lcd.setCursor(0,1);
lcd.print(F(" "));
lcd.setCursor(0,2);
lcd.print(F(" "));
lcd.setCursor(0,3);
lcd.print(F(" "));
//Place cursor to the new position
switch(menuCounter) //this checks the value of the counter (0, 1, 2 or 3)
{
case 0:
lcd.setCursor(0,0); //1st line, 1st block
lcd.print(F(">"));
break;
//-------------------------------
case 1:
lcd.setCursor(0,1); //2nd line, 1st block
lcd.print(F(">"));
break;
//-------------------------------
case 2:
lcd.setCursor(0,2); //3rd line, 1st block
lcd.print(F(">"));
break;
//-------------------------------
case 3:
lcd.setCursor(0,3); //4th line, 1st block
lcd.print(F(">"));
break;
}
}
void updateSelection()
{
//When a menu is selected ">" becomes "X"
if(distBetwin2Strokes_selected == true)
{
lcd.setCursor(0,0); //1st line, 1st block
lcd.print(F("X"));
rotaryEncoder.setBoundaries(0, 200, false); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
rotaryEncoder.setAcceleration(100);
if (distBetwin2Strokes != rotaryEncoder.readEncoder())
{
rotaryEncoder.reset (distBetwin2Strokes*10);
}
}
//-------------------
if(resistanceLevel_selected == true)
{
lcd.setCursor(0,1); //2nd line, 1st block
lcd.print(F("X"));
rotaryEncoder.setBoundaries(0, 70, false); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
rotaryEncoder.setAcceleration(50);
if (resistanceLevel != rotaryEncoder.readEncoder())
{
rotaryEncoder.reset (resistanceLevel*10);
}
}
//-------------------
if(strokeCountSessionTarget_selected == true)
{
lcd.setCursor(0,2); //3rd line, 1st block
lcd.print(F("X"));
rotaryEncoder.setBoundaries(0, 9999, false); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
rotaryEncoder.setAcceleration(1000);
if (strokeCountSessionTarget != rotaryEncoder.readEncoder())
{
rotaryEncoder.reset (strokeCountSessionTarget);
}
}
//-------------------
if(strokeCountTotalTarget_selected == true)
{
lcd.setCursor(0,3); //4th line, 1st block
lcd.print(F("X"));
rotaryEncoder.setBoundaries(0, 9999, false); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
rotaryEncoder.setAcceleration(1000);
if (strokeCountTotalTarget != rotaryEncoder.readEncoder())
{
rotaryEncoder.reset (strokeCountTotalTarget);
}
}
}
//***********************************************************************************************************************************************
// MESSAGES DURATION *
//***********************************************************************************************************************************************
//---------------------------------------------- Set Duration Messages
void MessageTiming()
{
//---------------------------------------------- If stroke indicator has been showing, take if off if it has been 250 millis or more
if (strokeActionShown && !startShown && !paused && (currentTime >= (strokeActionShownStartTime + 200)))
{
strokeActionShown = LOW;
lcd.setCursor(4, 0);
lcd.print(F(" "));
}
//---------------------------------------------- If stroke indicator has been showing, take current KPH to 0 if it has been 3000 millis or more
if (!startShown && !paused && (currentTime >= (lastStrokeStartTime + 3000)) && currentKPH > 0)
{
currentKPH = 0;
}
//---------------------------------------------- If "LET'S GO!" has been showing, take it off if it has been 2 seconds or more
if (letsGoShown && (currentTime >= (letsGoStartTime + 2000)))
{
letsGoShown = LOW;
showLabels(currentDisplayMode);
}
//---------------------------------------------- If "Paused!" has been showing, take it off if it has been 2 seconds or more
if (pausedShown && (currentTime >= (pausedStartTime + 2000)))
{
pausedShown = LOW;
showLabels(currentDisplayMode);
}
}
//***********************************************************************************************************************************************
// RESET COUNTER VARIABLES *
//***********************************************************************************************************************************************
//---------------------------------------------- Reset all variables used for calculating current/ongoing session
void resetSessionVariables()
{
currentStrokeCount = 0;
sessionStartTime = currentTime;
currentDistance = 0;
currentDuration = 0;
currentMaximumKPH = 0;
currentAverageKPH = 0;
currentStrokeCount = 0;
currentMaximumCadence = 0;
currentAverageCadence = 0;
}
//***********************************************************************************************************************************************
// PRINT *
//***********************************************************************************************************************************************
//---------------------------------------------- Show "LET'S GO!" Message
void ShowLetsGo()
{
lcd.clear();
lcd.setCursor(7, 1);
lcd.print(F("C'EST"));
lcd.setCursor(7, 2);
lcd.print(F("PARTI!"));
}
//---------------------------------------------- Show "PAUSED!" Message
void showPaused()
{
lcd.clear();
lcd.setCursor(7, 1);
lcd.print(F("PAUSE!"));
}
//---------------------------------------------- Show appropriate labels for current mode
void showLabels(int currentDisplayMode)
{
lcd.clear();
switch (currentDisplayMode)
{
case 1:
lcd.setCursor(0, 0);
lcd.print(F("CdR:"));
lcd.setCursor(0, 1);
lcd.print(F("CPM"));
lcd.setCursor(8, 1);
lcd.print(F("Mx"));
lcd.setCursor(15, 1);
lcd.print(F("Mo"));
lcd.setCursor(0, 2);
lcd.print(F("Vit"));
lcd.setCursor(8, 2);
lcd.print(F("Mx"));
lcd.setCursor(15, 2);
lcd.print(F("Mo"));
lcd.setCursor(11, 3);
lcd.print(F("D:"));
lcd.setCursor(0, 3);
lcd.print(F("CDM"));
break;
case 2:
lcd.setCursor(1, 0);
lcd.print(F("Dist/CdR (m):"));
lcd.setCursor(1, 1);
lcd.print(F("Niv de Force:"));
lcd.setCursor(1, 2);
lcd.print(F("CdR/Seance:"));
lcd.setCursor(1, 3);
lcd.print(F("Obj CdR Tot:"));
break;
}
}
/*
Serial.print(" currentStrokeCount:");
Serial.print(currentStrokeCount);
Serial.print(" currentCadence:");
Serial.print(currentCadence);
Serial.print(" Cadence0:");
Serial.print(Cadence0);
Serial.print(" Dcad:");
Serial.print(Dcad);
Serial.print(" tprint:");
Serial.print(tprint);
Serial.print(" currentAverageCadence:");
Serial.println(currentAverageCadence);
Serial.print(" lastStrokeStartTime:");
Serial.print(lastStrokeStartTime);
Serial.print(" sessionStartTime:");
Serial.println(sessionStartTime);
Serial.print(" lastStrokeCount:");
Serial.print(lastStrokeCount);
Serial.print(" lastMinuteStrokeCount:");
Serial.print(lastMinuteStrokeCount);
Serial.print(" strokingTime:");
Serial.print(strokingTime);
Serial.print(" lastMinuteStrokeStartTime:");
Serial.println(lastMinuteStrokeStartTime);
*/
void UpdateDataOnDisplay()
{
//---------------------------------------------- Current Distance
lcd.setCursor(13, 3);
if (currentDistance < 10)
{
lcd.print(F("0"));
lcd.print(currentDistance);
}
else
{
lcd.print(currentDistance);
}
lcd.print(F("km"));
//---------------------------------------------- Current Duration
computeHMS(currentDuration);
lcd.setCursor(12, 0);
if (intHours < 10)
{
lcd.print(F("0"));
}
lcd.print(intHours);
lcd.print(F(":"));
if (intMinutes < 10)
{
lcd.print(F("0"));
}
lcd.print(intMinutes);
lcd.print(F(":"));
if (intSeconds < 10)
{
lcd.print(F("0"));
}
lcd.print(intSeconds);
//---------------------------------------------- Current Speed
lcd.setCursor(3, 2);
if (currentKPH < 10)
{
lcd.print(F("00"));
lcd.print(currentKPH);
}
else if (currentKPH < 100)
{
lcd.print(F("0"));
lcd.print(currentKPH);
}
else
{
lcd.print(currentKPH);
}
//---------------------------------------------- Current Maximum Speed
lcd.setCursor(10, 2);
if (currentMaximumKPH < 10)
{
lcd.print(F("00"));
lcd.print(currentMaximumKPH);
}
else if (currentMaximumKPH < 100)
{
lcd.print(F("0"));
lcd.print(currentMaximumKPH);
}
else
{
lcd.print(currentMaximumKPH);
}
//---------------------------------------------- Current Average Speed
lcd.setCursor(17, 2);
if (currentAverageKPH < 10)
{
lcd.print(F("00"));
lcd.print(currentAverageKPH);
}
else if (currentAverageKPH < 100)
{
lcd.print(F("0"));
lcd.print(currentAverageKPH);
}
else
{
lcd.print(currentAverageKPH);
}
//---------------------------------------------- Print Cadence
//---------------------------------------------- Cadence Print Refresh
if (currentTime - tprint > DELAYTIMEPRINTCADENCE)
{
//---------------------------------------------- Current Stroke Number
lcd.setCursor(5, 0);
if (currentStrokeCount < 10)
{
lcd.print(F("000"));
lcd.print(currentStrokeCount);
}
else if (currentStrokeCount < 100)
{
lcd.print(F("00"));
lcd.print(currentStrokeCount);
}
else if (currentStrokeCount < 1000)
{
lcd.print(F("0"));
lcd.print(currentStrokeCount);
}
else
{
lcd.print(currentStrokeCount);
}
//---------------------------------------------- Current Cadence
lcd.setCursor(3, 1);
if (currentCadence < 10)
{
lcd.print(F("00"));
lcd.print(currentCadence);
}
else if (currentCadence < 100)
{
lcd.print(F("0"));
lcd.print(currentCadence);
}
else
{
lcd.print(currentCadence);
}
//---------------------------------------------- Current Maximum Cadence
lcd.setCursor(10, 1);
if (currentMaximumCadence < 10)
{
lcd.print(F("00"));
lcd.print(currentMaximumCadence);
}
else if (currentMaximumCadence < 100)
{
lcd.print(F("0"));
lcd.print(currentMaximumCadence);
}
else
{
lcd.print(currentMaximumCadence);
}
//---------------------------------------------- Current Average Cadence
lcd.setCursor(17, 1);
if (currentAverageCadence < 10)
{
lcd.print(F("00"));
lcd.print(currentAverageCadence);
}
else if (currentAverageCadence < 100)
{
lcd.print(F("0"));
lcd.print(currentAverageCadence);
}
else
{
lcd.print(currentAverageCadence);
}
//---------------------------------------------- Last Minute Strokes
lcd.setCursor(3, 3);
if (lastMinuteStrokeCounter < 10)
{
lcd.print(F("0"));
lcd.print(lastMinuteStrokeCounter);
}
else
{
lcd.print(lastMinuteStrokeCounter);
}
lcd.setCursor(5, 3);
lcd.print(F("("));
lcd.setCursor(6, 3);
if (lastMinuteStrokeCount < 10)
{
lcd.print(F("0"));
lcd.print(lastMinuteStrokeCount);
}
else
{
lcd.print(lastMinuteStrokeCount);
}
lcd.setCursor(8, 3);
lcd.print(F(")"));
tprint = currentTime;
}
}
void RotaryHistoricalSessionsInformations()
{
int16_t encoderDelta = rotaryEncoder.encoderChanged();
//optionally we can ignore whenever there is no change
if (encoderDelta == 0)
return;
//for some cases we only want to know if value is increased or decreased (typically for menu items)
if (encoderDelta > 0)
showSession++;
if (showSession > currentSession)
{
showSession = 0; // Show totals
}
if (encoderDelta < 0)
showSession--;
if (showSession < 0)
{
showSession = currentSession;
}
if (showSession > currentSession)
{
showSession = 0; // Show totals
}
}
void ShowHistoricalSessionsInformations()
{
//---------------------------------------------- Update display only if we need to show data for different session to that currently shown
//---------------------------------------------- this way display is not constantly cleared and refreshed with same data which would
//---------------------------------------------- cause display to flicker and is not needed anyway as data is not changing
if (sessionCurrentlyShown != showSession)
{
sessionCurrentlyShown = showSession;
lcd.clear();
lcd.setCursor(0, 0);
if (showSession == 0)
{
lcd.print(F("Tot"));
}
else
{
lcd.print(F("S"));
if (showSession < 10 )
{
lcd.print(F("0"));
lcd.print(showSession);
}
else
{
lcd.print(showSession);
}
}
//---------------------------------------------- Print Labels
lcd.setCursor(0, 1);
lcd.print(F("Cps/min"));
lcd.setCursor(8, 1);
lcd.print(F("Mx"));
lcd.setCursor(15, 1);
lcd.print(F("Mo"));
lcd.setCursor(0, 2);
lcd.print(F("Vitesse"));
lcd.setCursor(8, 2);
lcd.print(F("Mx"));
lcd.setCursor(15, 2);
lcd.print(F("Mo"));
lcd.setCursor(11, 3);
lcd.print(F("D:"));
lcd.setCursor(0, 3);
lcd.print(F("DMPM"));
//---------------------------------------------- Sessions Average Speed
lcd.setCursor(17, 2);
if (arrayAverageKPH[showSession] < 10)
{
lcd.print(F("00"));
lcd.print(arrayAverageKPH[showSession]);
}
else if (arrayAverageKPH[showSession] < 100)
{
lcd.print(F("0"));
lcd.print(arrayAverageKPH[showSession]);
}
else
{
lcd.print(arrayAverageKPH[showSession]);
}
//---------------------------------------------- Sessions Maximum Speed
lcd.setCursor(10, 2);
if (arrayMaximumKPH[showSession] < 10)
{
lcd.print(F("00"));
lcd.print(arrayMaximumKPH[showSession]);
}
else if (arrayMaximumKPH[showSession] < 100)
{
lcd.print(F("0"));
lcd.print(arrayMaximumKPH[showSession]);
}
else
{
lcd.print(arrayMaximumKPH[showSession]);
}
//---------------------------------------------- Sessions Distance
lcd.setCursor(13, 3);
if (arrayDistance[showSession] < 10)
{
lcd.print(F("0"));
lcd.print(currentDistance);
}
else
{
lcd.print(arrayDistance[showSession]);
}
lcd.print(F("km"));
//---------------------------------------------- Sessions Duration
computeHMS(arrayDuration[showSession]);
lcd.setCursor(12, 0);
if (intHours < 10)
{
lcd.print(F("0"));
}
lcd.print(intHours);
lcd.print(F(":"));
if (intMinutes < 10)
{
lcd.print(F("0"));
}
lcd.print(intMinutes);
lcd.print(F(":"));
if (intSeconds < 10)
{
lcd.print(F("0"));
}
lcd.print(intSeconds);
//---------------------------------------------- Sessions Stroke Number
lcd.setCursor(5, 0);
if (arrayStrokeCount[showSession] < 10)
{
lcd.print(F("000"));
lcd.print(arrayStrokeCount[showSession]);
}
else if (arrayStrokeCount[showSession] < 100)
{
lcd.print(F("00"));
lcd.print(arrayStrokeCount[showSession]);
}
else if (arrayStrokeCount[showSession] < 1000)
{
lcd.print(F("0"));
lcd.print(arrayStrokeCount[showSession]);
}
else
{
lcd.print(arrayStrokeCount[showSession]);
}
//---------------------------------------------- Sessions Maximum Cadence
lcd.setCursor(10, 1);
if (arrayMaximumCadence[showSession] < 10)
{
lcd.print(F("00"));
lcd.print(arrayMaximumCadence[showSession]);
}
else if (arrayMaximumCadence[showSession] < 100)
{
lcd.print(F("0"));
lcd.print(arrayMaximumCadence[showSession]);
}
else
{
lcd.print(arrayMaximumCadence[showSession]);
}
//---------------------------------------------- Sessions Average Cadence
lcd.setCursor(17, 1);
if (arrayAverageCadence[showSession] < 10)
{
lcd.print(F("00"));
lcd.print(arrayAverageCadence[showSession]);
}
else if (arrayAverageCadence[showSession] < 100)
{
lcd.print(F("0"));
lcd.print(arrayAverageCadence[showSession]);
}
else
{
lcd.print(arrayAverageCadence[showSession]);
}
//---------------------------------------------- Sessions Average Distance Per Minute
lcd.setCursor(4, 3);
if (arrayDistancePerMinute[showSession] < 10)
{
lcd.print(F("00"));
lcd.print(arrayDistancePerMinute[showSession]);
}
else if (arrayDistancePerMinute[showSession] < 100)
{
lcd.print(F("0"));
lcd.print(arrayDistancePerMinute[showSession]);
}
else
{
lcd.print(arrayDistancePerMinute[showSession]);
lcd.print(F("m"));
}
}
}
void eepromSave()
{
EEPROM.writeFloat(0,distBetwin2Strokes);
EEPROM.writeFloat(8,resistanceLevel);
EEPROM.writeInt(16,strokeCountSessionTarget);
EEPROM.writeInt(24,strokeCountTotalTarget);
EEPROM.commit();
}
//---------------------------------------------- A debouncing function that can be used for any button
boolean debounce(boolean last, int pin)
{
boolean current = digitalRead(pin);
if (last != current)
{
delay(10);
current = digitalRead(pin);
}
return current;
}
Comments
Please log in or sign up to comment.