I would like to share the release method that I'm using lately in my sessions.
As I can not use the freezer (there are more people living in the same place), I did a DIY release method with some spare parts that I got at home.
The list of items that you are going to need if you do want to build one:
I drilled the necessary holes in the box:
The frontal with the rotatory encoder, the On/off Switch and the LCD Screen
At the top, I have drilled two holes that give access to Arduino's USB and power jack (easier to upload sketches and also it gives the possibility of playing without the need of a battery)
On one side I drilled the hole for the electromagnet that will keep the keys until countdown ends and in the rear side, the magnet to secure the box in any metallic lamp.
I like to secure all the parts with hot glue when I know for sure that everything works.
In case anyone is going to build one, here there are the schematics with the connections:
This is the code you have to upload to your Arduino:
Code: Select all
#include <Wire.h>
#include <SoftwareSerial.h>
#include <LiquidCrystal_I2C.h>
#define outputA 5 // rotatory encoder output A.
#define outputB 7 // rotatory encoder output A.
#define buttonPin 6 // rotatory encoder push button.
#define magnetPin 3 // magnet output pin.
#define customRX 8 // rx port to comunicate with aditional devices without using the Arduino's serial port (USB or pins 0 and 1).
#define customTX 9 // tx port to comunicate with aditional devices without using the Arduino's serial port (USB or pins 0 and 1).
// Variables used in the program
int long remaining = 1; // time in minutes left to end the session. The minimum allowed is 1 minute. The maximum is 999 minutes.
int long remainingInSec = 1; // time in seconds left to end the session. The minimum allowed is 60 seconds. The maximum is 59.940 seconds.
long countdownHour = 0; // needed for time operations, it will express the remaining hours.
long countdownMinute = 0; // needed for time operations, it will express the remaining minutes.
long countdownSec = 0; // needed for time operations, it will express the remaining seconds.
long delayedCountdownMinute = 0; // needed for time operations, it will express the remaining minutes.
long delayedCountdownSec = 0; // needed for time operations, it will express the remaining seconds.
int cursorPosition = 1; // defines where in the screen we are. It can be a position inside one screen or a whole screen.
bool session = false; // if true, the session has started (countdown, waiting for external events, etc...). If false we are in the setup yet.
bool preSession = false; // if true, we are in the delayed start countdown. We can go back yet to change any setting.
bool aleatory = false; // defines if we are going to play with a random duration of time between the max and the min or with a fixed amount of time.
bool visible = true; // if true, the countdown and a progress bar will be shown on the screen. If false, the remaining time will be hidden.
int short maximum = 1; // variable holding the maximum amount of time that we do want the session to last.
int short tempmaximum = 1; // temporal variable to perform time operations before changing maximum value.
int short minimum = 1; // variable holding the minimum amount of time that we do want the session to last if we setup a random duration.
int short tempminimum = 1; // temporal variable to perform time operations before changing minimum value.
int power = 255; // the value that we are going to analogWrite to the pin that controls the electromagnet.
int tempPower = 0; // temporal variable to perform math operations before changing power value.
int short delayedStart = 1; // amount of minutes between the setup ending and session starting. It will prevent that users can see random time calculated and leave the session.
int delayedStartInSec = 60; // amount of seconds between the setup ending and session starting. It will prevent that users can see random time calculated and leave the session.
int short tempDelayedStart = 1; // temporal variable to perform math operations before changing delayed start value.
unsigned long previousMillis = 0; // it stores the moment the screen blinked before. It will allow showing the selected value without using code blocking delays.
unsigned long preSessionMillis = 0; // it stores the last time that we performed a countdown update during the pre-session.
unsigned long sessionMillis = 0; // it stores the last time that we performed a countdown update during the session.
const int short blinkInterval = 700; // ms between screen blinking. The lower the value is, the faster the screen will blink.
int blinkState = LOW; // stores current blinking state to toggle between showing the value or hidding it.
int backlightState = LOW; // stores current lcd backlight state to toggle between on and off, so the user will see that the countdown is working even in hidden mode.
int long b = 1; // variable that we are going to use to compare if the user has inputted a bigger minimum than maximum.
int long a = 0; // variable that we are going to use to compare if the user has inputted a bigger minimum than maximum.
// Button timing variables
int debounce = 50; // ms debounce period to prevent flickering when pressing or releasing the button.
int DCgap = 500; // max ms between clicks for a double click event.
int holdTime = 1200; // ms hold period: how long to wait for press+hold event.
int longHoldTime = 3000; // ms long hold period: how long to wait for press+hold event.
// Button variables
boolean buttonVal = HIGH; // value read from button.
boolean buttonLast = HIGH; // buffered value of the button's previous state.
boolean DCwaiting = false; // whether we're waiting for a double click (down).
boolean DConUp = false; // whether to register a double click on next release, or whether to wait and click.
boolean singleOK = true; // whether it's OK to do a single click.
long downTime = -1; // time the button was pressed down.
long upTime = -1; // time the button was released.
boolean ignoreUp = false; // whether to ignore the button release because the click+hold was triggered.
boolean waitForUp = false; // when held, whether to wait for the up event.
boolean holdEventPast = false; // whether or not the hold event happened already.
boolean longHoldEventPast = false; // whether or not the long hold event happened already.
// Rotatory encoder variables
int aState; // current state of the rotatory encoder.
int aLastState; // last state of the rotatory encoder, needed to compare if it has changed.
int counter = 0; // counts the number of rotations performed.
// We do declare the Liquid Crystal Display that we are going to use, with the I2C address and the number of columns and rows.
// Find out your I2C address here: https://create.arduino.cc/projecthub/abdularbi17/how-to-scan-i2c-address-in-arduino-eaadda
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Now we are going to create the special characters that we do want to show on the LCD screen.
// At first we do create the characters that form part of the progress bar.
byte one[8] = {
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000
};
byte two[8] = {
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000
};
byte three[8] = {
B11100,
B11100,
B11100,
B11100,
B11100,
B11100,
B11100,
B11100
};
byte four[8] = {
B11110,
B11110,
B11110,
B11110,
B11110,
B11110,
B11110,
B11110
};
byte five[8] = {
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111
};
// Next we are going to create the characters that are going to be shown like the word 'to' and a padlock opened and closed
byte openPadlock[8] = {
B01100,
B10010,
B00001,
B00001,
B11111,
B11011,
B11011,
B11111
};
byte ToChar[8] = {
B01110,
B00100,
B00100,
B00000,
B01110,
B01010,
B01110,
B00000
};
byte closedPadlock[8] = {
B01110,
B10001,
B10001,
B10001,
B11111,
B11011,
B11011,
B11111
};
SoftwareSerial customSerial(customRX, customTX);
void setup()
{
pinMode (outputA, INPUT);
pinMode (outputB, INPUT);
pinMode (buttonPin, INPUT_PULLUP);
lcd.init();
lcd.createChar(0, ToChar);
lcd.createChar(1, one);
lcd.createChar(2, two);
lcd.createChar(3, three);
lcd.createChar(4, four);
lcd.createChar(5, five);
lcd.createChar(6, openPadlock);
lcd.createChar(7, closedPadlock);
// We do start the backlight of the lcd screen
lcd.backlight();
// We do write the messages of the first screen
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(6, 0);
lcd.print(maximum);
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(6, 1);
lcd.print(minimum);
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(15, 0);
lcd.print("N");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
lcd.setCursor(15, 1);
lcd.print("N");
Serial.begin (9600);
customSerial.begin(9600);
aLastState = digitalRead(outputA); //Leemos el valor incial
}
void loop()
{
unsigned long currentMillis = millis();
if (currentMillis - preSessionMillis >= 1000) {
preSessionMillis = millis();
if ((preSession == true) && (cursorPosition == 8)) {
delayedStartInSec --;
Serial.print("P");
Serial.println(delayedStartInSec);
customSerial.print("P");
customSerial.println(delayedStartInSec);
if (delayedStartInSec > 0) {
delayedCountdownMinute = (delayedStartInSec / 60) % 60;
delayedCountdownSec = delayedStartInSec % 60;
if (delayedCountdownMinute < 10) {
lcd.setCursor(11, 0);
lcd.print("0");
lcd.setCursor(12, 0);
lcd.print(delayedCountdownMinute);
} else {
lcd.setCursor(11, 0);
lcd.print(delayedCountdownMinute);
}
if (delayedCountdownSec < 10) {
lcd.setCursor(14, 0);
lcd.print("0");
lcd.setCursor(15, 0);
lcd.print(delayedCountdownSec);
} else {
lcd.setCursor(14, 0);
lcd.print(delayedCountdownSec);
}
updateProgressBar(delayedStartInSec, delayedStart * 60, 1);
} else {
cursorPosition = 0;
preSession = false;
lcd.noBacklight();
lcd.setCursor(0, 0);
lcd.print("STARTING SESSION");
lcd.setCursor(0, 1);
lcd.print(" ");
delay(500);
lcd.backlight();
delay(500);
lcd.noBacklight();
delay(500);
lcd.backlight();
delay(500);
lcd.setCursor(8, 1);
lcd.write(6);
lcd.noBacklight();
delay(500);
lcd.backlight();
delay(500);
lcd.setCursor(8, 1);
lcd.write(7);
delay(1500);
if (aleatory == true) {
if (maximum > minimum) {
remaining = random(minimum, maximum + 1);
} else {
remaining = random(maximum, minimum + 1);
}
} else {
if (maximum > minimum) {
remaining = maximum;
} else {
remaining = minimum;
}
}
Serial.print("D");
Serial.println(remaining * 60);
customSerial.print("D");
customSerial.println(remaining * 60);
remainingInSec = remaining * 60;
if (visible == true) {
lcd.setCursor(0, 0);
lcd.print("FREE IN ");
lcd.setCursor(0, 1);
lcd.print(" ");
countdownHour = remainingInSec / 3600;
countdownMinute = ((remainingInSec / 60) % 60);
countdownSec = remainingInSec % 60;
lcd.setCursor(8, 0);
if (countdownHour < 10) {
lcd.print("0");
}
lcd.print(countdownHour);
lcd.print(":");
if (countdownMinute < 10) {
lcd.print("0");
}
lcd.print(countdownMinute);
lcd.print(":");
if (countdownSec < 10) {
lcd.print("0");
}
lcd.print(countdownSec);
updateProgressBar(remainingInSec, remaining * 60, 1);
} else {
lcd.setCursor(0, 0);
lcd.print(" SESSION IN ");
lcd.setCursor(1, 1);
lcd.print(" PROGRESS ");
lcd.setCursor(0, 1);
lcd.write(7);
lcd.setCursor(15, 1);
lcd.write(7);
}
cursorPosition = 9;
session = true;
}
}
}
if (currentMillis - sessionMillis >= 1000) {
sessionMillis = millis();
if ((session == true) && (cursorPosition == 9)) {
remainingInSec --;
if (remainingInSec > 0) {
Serial.print("S");
Serial.println(remainingInSec);
customSerial.print("S");
customSerial.println(remainingInSec);
if (visible == true) {
countdownHour = remainingInSec / 3600;
countdownMinute = ((remainingInSec / 60) % 60);
countdownSec = remainingInSec % 60;
lcd.setCursor(8, 0);
if (countdownHour < 10) {
lcd.print("0");
}
lcd.print(countdownHour);
lcd.print(":");
if (countdownMinute < 10) {
lcd.print("0");
}
lcd.print(countdownMinute);
lcd.print(":");
if (countdownSec < 10) {
lcd.print("0");
}
lcd.print(countdownSec);
updateProgressBar(remainingInSec, remaining * 60, 1);
} else {
if (backlightState == LOW) {
backlightState = HIGH;
lcd.backlight();
} else {
backlightState = LOW;
lcd.noBacklight();
}
}
} else {
cursorPosition = 10;
session = false;
lcd.setCursor(0, 0);
lcd.print("SESSION ENDED :)");
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(7, 1);
lcd.write(6);
analogWrite(magnetPin, 0);
lcd.backlight();
Serial.print("E");
customSerial.println("E");
}
}
}
if (Serial.available() > 0) {
int received1 = Serial.read();
if (received1 == 'C') {
shortKeyPress();
} else if (received1 == 'D') {
doubleKeyPress();
} else if (received1 == 'L') {
longKeyPress();
} else if (received1 == '0') {
externalEndSession();
} else if (received1 == '1') {
externalTimeAddition(1);
} else if (received1 == '2') {
externalTimeAddition(2);
} else if (received1 == '3') {
externalTimeAddition(3);
} else if (received1 == '4') {
externalTimeAddition(4);
} else if (received1 == '5') {
externalTimeAddition(5);
} else if (received1 == '6') {
externalTimeAddition(6);
} else if (received1 == '7') {
externalTimeAddition(7);
} else if (received1 == '8') {
externalTimeAddition(8);
} else if (received1 == '9') {
externalTimeAddition(9);
}
}
if (customSerial.available() > 0) {
int received2 = customSerial.read();
if (received2 == 'C') {
shortKeyPress();
} else if (received2 == 'D') {
doubleKeyPress();
} else if (received2 == 'L') {
longKeyPress();
} else if (received2 == '0') {
externalEndSession();
} else if (received2 == '1') {
externalTimeAddition(1);
} else if (received2 == '2') {
externalTimeAddition(2);
} else if (received2 == '3') {
externalTimeAddition(3);
} else if (received2 == '4') {
externalTimeAddition(4);
} else if (received2 == '5') {
externalTimeAddition(5);
} else if (received2 == '6') {
externalTimeAddition(6);
} else if (received2 == '7') {
externalTimeAddition(7);
} else if (received2 == '8') {
externalTimeAddition(8);
} else if (received2 == '9') {
externalTimeAddition(9);
}
}
if (currentMillis - previousMillis >= blinkInterval) {
previousMillis = currentMillis;
if (blinkState == LOW) {
blinkState = HIGH;
if (cursorPosition == 1) {
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
}
if (cursorPosition == 2) {
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
}
if (cursorPosition == 3) {
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
}
if (cursorPosition == 4) {
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
}
} else {
blinkState = LOW;
if (cursorPosition == 1) {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
}
if (cursorPosition == 2) {
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
}
if (cursorPosition == 3) {
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(8, 0);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
}
if (cursorPosition == 4) {
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(8, 1);
lcd.print(" ");
}
}
}
aState = digitalRead(outputA);
if (aState != aLastState) {
if (digitalRead(outputB) != aState) {
if (cursorPosition == 4) {
lcd.setCursor(15, 1);
lcd.print("Y");
visible = false;
}
if (cursorPosition == 3) {
lcd.setCursor(15, 0);
lcd.print("Y");
aleatory = true;
}
if (cursorPosition == 1) {
counter ++;
tempmaximum = maximum + counter;
if (tempmaximum < 1) {
tempmaximum = 1;
counter = 1;
}
if (tempmaximum > 999) {
tempmaximum = 999;
counter = 999;
}
if (tempmaximum < 10) {
lcd.setCursor(4, 0);
lcd.print(" ");
lcd.setCursor(6, 0);
lcd.print(tempmaximum);
}
if ((tempmaximum > 9) && (tempmaximum < 100)) {
lcd.setCursor(4, 0);
lcd.print(" ");
lcd.setCursor(5, 0);
lcd.print(tempmaximum);
}
if (tempmaximum > 99) {
lcd.setCursor(4, 0);
lcd.print(tempmaximum);
}
}
if (cursorPosition == 2) {
counter ++;
tempminimum = counter + minimum;
if (tempminimum < 1) {
tempminimum = 1;
counter = 1;
}
if (tempminimum > 999) {
tempminimum = 999;
counter = 999;
}
if (tempminimum < 10) {
lcd.setCursor(4, 1);
lcd.print(" ");
lcd.setCursor(6, 1);
lcd.print(tempminimum);
}
if ((tempminimum > 9) && (tempminimum < 100)) {
lcd.setCursor(4, 1);
lcd.print(" ");
lcd.setCursor(5, 1);
lcd.print(tempminimum);
}
if (tempminimum > 99) {
lcd.setCursor(4, 1);
lcd.print(tempminimum);
}
}
if (cursorPosition == 6) {
counter ++;
tempDelayedStart = counter + delayedStart;
if (tempDelayedStart < 1) {
tempDelayedStart = 1;
counter = 1;
}
if (tempDelayedStart > 30) {
tempDelayedStart = 30;
counter = 30;
}
if (tempDelayedStart < 10) {
lcd.setCursor(13, 1);
lcd.print(" ");
lcd.setCursor(14, 1);
lcd.print(tempDelayedStart);
}
if ((tempDelayedStart > 9) && (tempDelayedStart < 100)) {
lcd.setCursor(13, 1);
lcd.print(" ");
lcd.setCursor(13, 1);
lcd.print(tempDelayedStart);
}
}
if (cursorPosition == 7) {
counter = counter + 4;
tempPower = power + counter;
if (tempPower > 254) {
tempPower = 255;
counter = 0;
}
updateProgressBar(tempPower, 255, 1);
analogWrite(magnetPin, tempPower);
}
if (cursorPosition == 8) {
counter = counter + 5;
delayedStartInSec = delayedStartInSec + counter;
if (delayedStartInSec > 600) {
delayedStartInSec = 600;
}
if (delayedStartInSec > delayedStart * 60) {
delayedStart = delayedStartInSec / 60;
}
updateProgressBar(delayedStartInSec, delayedStart * 60, 1);
counter = 0;
}
} else {
if (cursorPosition == 4) {
lcd.setCursor(15, 1);
lcd.print("N");
visible = true;
}
if (cursorPosition == 3) {
lcd.setCursor(15, 0);
lcd.print("N");
aleatory = false;
}
if (cursorPosition == 1) {
counter --;
tempmaximum = maximum + counter;
if (tempmaximum < 1) {
tempmaximum = 1;
counter = 1;
}
if (tempmaximum > 999) {
tempmaximum = 999;
counter = 999;
}
if (tempmaximum < 10) {
lcd.setCursor(4, 0);
lcd.print(" ");
lcd.setCursor(6, 0);
lcd.print(tempmaximum);
}
if ((tempmaximum > 9) && (tempmaximum < 100)) {
lcd.setCursor(4, 0);
lcd.print(" ");
lcd.setCursor(5, 0);
lcd.print(tempmaximum);
}
if (tempmaximum > 99) {
lcd.setCursor(4, 0);
lcd.print(tempmaximum);
}
}
if (cursorPosition == 2) {
counter --;
tempminimum = minimum + counter;
if (tempminimum < 1) {
tempminimum = 1;
counter = 1;
}
if (tempminimum > 999) {
tempminimum = 999;
counter = 999;
}
if (tempminimum < 10) {
lcd.setCursor(4, 1);
lcd.print(" ");
lcd.setCursor(6, 1);
lcd.print(tempminimum);
}
if ((tempminimum > 9) && (tempminimum < 100)) {
lcd.setCursor(4, 1);
lcd.print(" ");
lcd.setCursor(5, 1);
lcd.print(tempminimum);
}
if (tempminimum > 99) {
lcd.setCursor(4, 1);
lcd.print(tempminimum);
}
}
if (cursorPosition == 6) {
counter --;
tempDelayedStart = delayedStart + counter;
if (tempDelayedStart < 1) {
tempDelayedStart = 1;
counter = 1;
}
if (tempDelayedStart > 30) {
tempDelayedStart = 30;
counter = 30;
}
if (tempDelayedStart < 10) {
lcd.setCursor(13, 1);
lcd.print(" ");
lcd.setCursor(14, 1);
lcd.print(tempDelayedStart);
}
if ((tempDelayedStart > 9) && (tempDelayedStart < 100)) {
lcd.setCursor(13, 1);
lcd.print(" ");
lcd.setCursor(13, 1);
lcd.print(tempDelayedStart);
}
}
if (cursorPosition == 7) {
counter = counter - 4;
tempPower = power + counter;
if (tempPower < 1) {
tempPower = 1;
counter = 0;
}
updateProgressBar(tempPower, 255, 1);
analogWrite(magnetPin, tempPower);
}
}
}
aLastState = aState; // We do store the last value
// Get button event and act accordingly
int c = checkButton();
if (c == 1) {
shortKeyPress();
}
if (c == 2) {
doubleKeyPress();
}
if (c == 3) {
longKeyPress();
}
}
// called when button is kept pressed for less than 1 seconds
void shortKeyPress() {
if (cursorPosition == 5) {
cursorPosition = 1;
}
if (cursorPosition < 6 && cursorPosition > 0) {
cursorPosition ++;
counter = 0;
} else if (cursorPosition == 6) {
cursorPosition = 0;
delayedStart = tempDelayedStart;
delayedStartInSec = delayedStart * 60;
counter = 0;
lcd.setCursor(0, 0);
lcd.print("MAGNET STRENGTH");
lcd.setCursor(0, 1);
lcd.print(" ");
updateProgressBar(power, 255, 1);
cursorPosition = 7;
analogWrite(magnetPin, power);
} else if (cursorPosition == 7) {
cursorPosition = 0;
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 0);
lcd.print("START IN: ");
lcd.setCursor(13, 0);
lcd.print(":");
delayedStart = tempDelayedStart;
delayedStartInSec = delayedStart * 60;
delayedCountdownMinute = (delayedStart % 60);
delayedCountdownSec = (delayedStartInSec) % 60;
lcd.setCursor(11, 0);
if (delayedCountdownMinute < 10) {
lcd.print("0");
}
lcd.print(delayedCountdownMinute);
lcd.setCursor(14, 0);
if (delayedCountdownSec < 10) {
lcd.print("0");
}
lcd.print(delayedCountdownSec);
updateProgressBar(delayedStart * 60, delayedStart * 60, 1);
cursorPosition = 8;
delay(1000);
preSession = true;
} else if (cursorPosition == 10) {
//We have to reset all the variables to their default values
remaining = 1;
remainingInSec = 1;
countdownHour = 0;
countdownMinute = 0;
countdownSec = 0;
delayedCountdownMinute = 0;
delayedCountdownSec = 0;
session = false;
preSession = false;
aleatory = false;
visible = true;
maximum = 1;
tempmaximum = 1;
minimum = 1;
tempminimum = 1;
power = 255;
tempPower = 0;
delayedStart = 1;
delayedStartInSec = 60;
tempDelayedStart = 1;
b = 1;
a = 0;
// We do start the backlight of the lcd screen
lcd.backlight();
// We do write the messages of the first screen
lcd.setCursor(0, 0);
lcd.print("MAX= ");
lcd.setCursor(6, 0);
lcd.print(maximum);
lcd.setCursor(0, 1);
lcd.print("MIN= ");
lcd.setCursor(6, 1);
lcd.print(minimum);
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(15, 0);
lcd.print("N");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
lcd.setCursor(15, 1);
lcd.print("N");
cursorPosition = 1;
}
maximum = tempmaximum;
minimum = tempminimum;
}
// called when button is kept pressed twice in less than 500ms
void doubleKeyPress() {
if ((cursorPosition < 5) && (cursorPosition > 0)) {
maximum = tempmaximum;
minimum = tempminimum;
cursorPosition = 0;
if (maximum > minimum) {
b = maximum;
a = minimum;
} else {
a = maximum;
b = minimum;
}
if ((aleatory == true) && (a != b)) {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print(" ");
if (a < 10) {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(2, 0);
lcd.print(a);
}
if ((a > 9) && (a < 100)) {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(1, 0);
lcd.print(a);
}
if (a > 99) {
lcd.setCursor(0, 0);
lcd.print(a);
}
lcd.setCursor(3, 0);
lcd.write(0);
if (b < 10) {
lcd.setCursor(4, 0);
lcd.print(b);
lcd.setCursor(6, 0);
lcd.print("min");
}
if ((b > 9) && (b < 100)) {
lcd.setCursor(4, 0);
lcd.print(b);
lcd.setCursor(6, 0);
lcd.print("m.");
}
if (b > 99) {
lcd.setCursor(4, 0);
lcd.print(b);
lcd.setCursor(7, 0);
lcd.print("'");
}
} else {
if (maximum > minimum) {
b = maximum;
a = minimum;
} else {
a = maximum;
b = minimum;
}
lcd.setCursor(0, 0);
lcd.print(" ");
if (b < 10) {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(2, 0);
lcd.print(b);
}
if ((b > 9) && (b < 100)) {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(1, 0);
lcd.print(b);
}
if (b > 99) {
lcd.setCursor(0, 0);
lcd.print(b);
}
lcd.setCursor(4, 0);
lcd.print("min.");
}
lcd.setCursor(0, 1);
lcd.print("START DELAY = ");
lcd.setCursor(14, 1);
lcd.print(delayedStart);
lcd.setCursor(15, 1);
lcd.print("'");
if (visible == true) {
lcd.setCursor(9, 0);
lcd.print("VISIBLE");
} else {
lcd.setCursor(10, 0);
lcd.print("HIDDEN");
}
cursorPosition = 6;
} else if (cursorPosition == 6) {
cursorPosition = 0;
counter = 0;
lcd.setCursor(0, 0);
lcd.print("MAGNET STRENGTH");
lcd.setCursor(0, 1);
lcd.print(" ");
updateProgressBar(power, 255, 1);
cursorPosition = 7;
analogWrite(magnetPin, power);
} else if (cursorPosition == 7) {
cursorPosition = 0;
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 0);
lcd.print("START IN: ");
lcd.setCursor(13, 0);
lcd.print(":");
delayedStart = tempDelayedStart;
delayedStartInSec = delayedStart * 60;
delayedCountdownMinute = (delayedStart % 60);
delayedCountdownSec = (delayedStartInSec) % 60;
lcd.setCursor(11, 0);
if (delayedCountdownMinute < 10) {
lcd.print("0");
}
lcd.print(delayedCountdownMinute);
lcd.setCursor(14, 0);
if (delayedCountdownSec < 10) {
lcd.print("0");
}
lcd.print(delayedCountdownSec);
updateProgressBar(delayedStart * 60, delayedStart * 60, 1);
cursorPosition = 8;
delay(1000);
preSession = true;
} else if (cursorPosition == 10) {
//We have to reset all the variables to their default values
remaining = 1;
remainingInSec = 1;
countdownHour = 0;
countdownMinute = 0;
countdownSec = 0;
delayedCountdownMinute = 0;
delayedCountdownSec = 0;
session = false;
preSession = false;
aleatory = false;
visible = true;
maximum = 1;
tempmaximum = 1;
minimum = 1;
tempminimum = 1;
power = 255;
tempPower = 0;
delayedStart = 1;
delayedStartInSec = 60;
tempDelayedStart = 1;
b = 1;
a = 0;
// We do start the backlight of the lcd screen
lcd.backlight();
// We do write the messages of the first screen
lcd.setCursor(0, 0);
lcd.print("MAX= ");
lcd.setCursor(6, 0);
lcd.print(maximum);
lcd.setCursor(0, 1);
lcd.print("MIN= ");
lcd.setCursor(6, 1);
lcd.print(minimum);
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(15, 0);
lcd.print("N");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
lcd.setCursor(15, 1);
lcd.print("N");
cursorPosition = 1;
}
}
void longKeyPress() {
if (cursorPosition > 5 && cursorPosition != 10 && session == false) {
preSession = false;
cursorPosition = 0;
analogWrite(magnetPin, 0);
delay(200);
lcd.setCursor(0, 0);
lcd.print("MAX= ");
lcd.setCursor(0, 1);
lcd.print("MIN= ");
lcd.setCursor(8, 0);
lcd.print("RANDOM= ");
if (aleatory == true) {
lcd.setCursor(15, 0);
lcd.print("Y");
} else {
lcd.setCursor(15, 0);
lcd.print("N");
}
lcd.setCursor(8, 1);
lcd.print("HIDDEN= ");
if (visible == true) {
lcd.setCursor(15, 1);
lcd.print("N");
} else {
lcd.setCursor(15, 1);
lcd.print("Y");
}
if (minimum < 10) {
lcd.setCursor(6, 1);
lcd.print(minimum);
}
if ((minimum > 9) && (minimum < 100)) {
lcd.setCursor(5, 1);
lcd.print(minimum);
}
if (minimum > 99) {
lcd.setCursor(4, 1);
lcd.print(minimum);
}
if (maximum < 10) {
lcd.setCursor(6, 0);
lcd.print(maximum);
}
if ((maximum > 9) && (maximum < 100)) {
lcd.setCursor(5, 0);
lcd.print(maximum);
}
if (maximum > 99) {
lcd.setCursor(4, 0);
lcd.print(maximum);
}
cursorPosition = 1;
} else if (cursorPosition == 10) {
//We have to reset all the variables to their default values
remaining = 1;
remainingInSec = 1;
countdownHour = 0;
countdownMinute = 0;
countdownSec = 0;
delayedCountdownMinute = 0;
delayedCountdownSec = 0;
session = false;
preSession = false;
aleatory = false;
visible = true;
maximum = 1;
tempmaximum = 1;
minimum = 1;
tempminimum = 1;
power = 255;
tempPower = 0;
delayedStart = 1;
delayedStartInSec = 60;
tempDelayedStart = 1;
b = 1;
a = 0;
// We do start the backlight of the lcd screen
lcd.backlight();
// We do write the messages of the first screen
lcd.setCursor(0, 0);
lcd.print("MAX=");
lcd.setCursor(6, 0);
lcd.print(maximum);
lcd.setCursor(0, 1);
lcd.print("MIN=");
lcd.setCursor(6, 1);
lcd.print(minimum);
lcd.setCursor(8, 0);
lcd.print("RANDOM=");
lcd.setCursor(15, 0);
lcd.print("N");
lcd.setCursor(8, 1);
lcd.print("HIDDEN=");
lcd.setCursor(15, 1);
lcd.print("N");
cursorPosition = 1;
}
}
void updateProgressBar(unsigned long count, unsigned long totalCount, int lineToPrintOn) {
double factor = totalCount / 80.0;
int percent = (count + 1) / factor;
int number = percent / 5;
int remainder = percent % 5;
if (number > 0)
{
for (int j = 0; j < number; j++)
{
lcd.setCursor(j, lineToPrintOn);
lcd.write(5);
}
}
lcd.setCursor(number, lineToPrintOn);
if (remainder == 0) {
lcd.print(" ");
} else {
lcd.write(remainder);
}
if (number < 16) //
{
for (int j = number + 1; j <= 16; j++)
{
lcd.setCursor(j, lineToPrintOn);
lcd.print(" ");
}
}
}
int checkButton() {
int event = 0;
buttonVal = digitalRead(buttonPin);
// Button pressed down
if (buttonVal == LOW && buttonLast == HIGH && (millis() - upTime) > debounce)
{
downTime = millis();
ignoreUp = false;
waitForUp = false;
singleOK = true;
holdEventPast = false;
longHoldEventPast = false;
if ((millis() - upTime) < DCgap && DConUp == false && DCwaiting == true) DConUp = true;
else DConUp = false;
DCwaiting = false;
}
// Button released
else if (buttonVal == HIGH && buttonLast == LOW && (millis() - downTime) > debounce)
{
if (not ignoreUp)
{
upTime = millis();
if (DConUp == false) DCwaiting = true;
else
{
event = 2;
DConUp = false;
DCwaiting = false;
singleOK = false;
}
}
}
// Test for normal click event: DCgap expired
if ( buttonVal == HIGH && (millis() - upTime) >= DCgap && DCwaiting == true && DConUp == false && singleOK == true && event != 2)
{
event = 1;
DCwaiting = false;
}
// Test for hold
if (buttonVal == LOW && (millis() - downTime) >= holdTime) {
// Trigger "normal" hold
if (not holdEventPast)
{
event = 3;
waitForUp = true;
ignoreUp = true;
DConUp = false;
DCwaiting = false;
//downTime = millis();
holdEventPast = true;
}
// Trigger "long" hold
if ((millis() - downTime) >= longHoldTime)
{
if (not longHoldEventPast)
{
event = 4;
longHoldEventPast = true;
}
}
}
buttonLast = buttonVal;
return event;
}
void externalEndSession() {
if ((session == true) && (cursorPosition == 9)) {
cursorPosition = 0; // We do stop the countdown temporary
remaining = 0;
analogWrite(magnetPin, 0);
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print(" SESSION ENDING ");
lcd.setCursor(15, 1);
lcd.write(6);
lcd.setCursor(0, 1);
lcd.write(6);
lcd.setCursor(1, 1);
lcd.print(" RECEIVED ");
Serial.print("E");
customSerial.print("E");
}
cursorPosition = 10; //We do pass to session ended state
}
void externalTimeAddition(int timeAdded) {
if ((session == true) && (cursorPosition == 9)) {
cursorPosition = 0; // We do stop the countdown temporary
remainingInSec = remainingInSec + (timeAdded * 60);
if (remainingInSec > 59940) {
remainingInSec = 59940;
}
if (remainingInSec > remaining * 60) {
remaining = round(remainingInSec / 60);
}
// Depending on if we were playing with hidden screen or not, we will change the way the time addition message is shown
if (visible == false) {
// We do start the backlight of the lcd screen to ensure that user can see the message
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print(" TIME ADDITION ");
lcd.setCursor(0, 1);
lcd.print("RECEIVED! + ");
lcd.setCursor(10, 1);
lcd.print(timeAdded);
lcd.setCursor(12, 1);
lcd.print("min.");
delay(5000);
lcd.setCursor(0, 0);
lcd.print(" SESSION IN ");
lcd.setCursor(1, 1);
lcd.print(" PROGRESS ");
lcd.setCursor(0, 1);
lcd.write(7);
lcd.setCursor(15, 1);
lcd.write(7);
} else {
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print(" TIME ADDITION ");
lcd.setCursor(0, 1);
lcd.print("RECEIVED! + ");
lcd.setCursor(10, 1);
lcd.print(timeAdded);
lcd.setCursor(12, 1);
lcd.print("min.");
delay(1000);
lcd.noBacklight();
delay(1000);
lcd.backlight();
delay(1000);
lcd.noBacklight();
delay(1000);
lcd.backlight();
delay(1000);
lcd.noBacklight();
delay(1000);
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("FREE IN ");
lcd.setCursor(0, 1);
lcd.print(" ");
countdownHour = remainingInSec / 3600;
countdownMinute = ((remainingInSec / 60) % 60);
countdownSec = remainingInSec % 60;
lcd.setCursor(8, 0);
if (countdownHour < 10) {
lcd.print("0");
}
lcd.print(countdownHour);
lcd.print(":");
if (countdownMinute < 10) {
lcd.print("0");
}
lcd.print(countdownMinute);
lcd.print(":");
if (countdownSec < 10) {
lcd.print("0");
}
lcd.print(countdownSec);
updateProgressBar(remainingInSec, remaining * 60, 1);
}
cursorPosition = 9; //We do resume the countdown
}
}
The idea of this release method is to set some options like the desired duration of the session (maximum time in minutes, minimum time, fixed time or random duration, possibility of seeing how many time left during the session, duration of the pre-session countdown before knowing how much time the controller has randomly calculated and possibility of adjusting the electromagnet strength to save battery and heat).
Once you turn on the device, you'll get in your LCD screen 4 possible variables to config:
You can move to the next input clicking once the button of the rotatory encoder.
Moving the wheel of the rotatory encoder clockwise, to increase the time values. Moving it anti-clockwise, you decrease the value.
The maximum allowed is 999 minutes for each one. It doesn't matter if you input a minimum bigger than the maximum, the program will arrange the numbers in the next step.
The Random and the Hidden inputs do only allow Yes or No values.
RANDOM=Y means that the countdown will last an amount of time between the maximum and the minimum.
RANDOM=N means that the countdown will last as much as the bigger time input that you have selected. If MAX=20 and MIN=23, the session will last 23 minutes.
HIDDEN=N means that you will have the total amount of time in the LCD screen with a decreasing progress bar
HIDDEN=Y means that you will have no idea of how much time is left before the session ends.
Doing single clicks, you can change the input value, but to move to the next screen, you have to perform a double click.
In the second screen, you can see the configuration that you've set for your session in the upper row.
If you want to change some value, you can go back bydoing a long click on the embedded button.
It works like this:
- Single click = Change input field in first screen
- Single click = Go to next screen in every screen except first one
- Double click = Go to next screen
- Long Press = Go to first screen
In the bottom row, there's an input to set the total amount of time that you want to prepare yourself before the final countdown starts.
It is based on the same basis as some of boundanna's programs: if you are going to do a random duration session with this pre-session countdown, it gives you time to fix the box to an unreachable place and you can start cuffing yourself without knowing how much time the controller has calculated.
If you are going to play with a fixed time or with the hidden screen, then just set this value to 1 minute. The max allowed is 30 minutes. It is independent of the main countdown, so if you set 20 minutes of the session and 30 minutes of pre-session, the keys will not fall until 50 minutes later.
Once you have confirmed that the session settings are the ones that you want and the pre-session countdown duration, click or double click to advance to the next screen:
Here you can set the power of attraction of the magnet using the rotatory encoder. Set it to the minimum necessary to hold the keys. It will avoid unnecessary heat and will make your battery last longer.
Turn the wheel counterclockwise to get a zero attraction value and test that the keys fall always to a point where you can reach them.
It is a good idea to attach the keys to a little chain to gain some extra weight. Search the main site looking for ideas about keys attached to a string and other release methods.
When you have set the magnet desired strength, click (or double click) the button to start the pre-session countdown.
During the pre-session countdown, you can still go to the first screen and cancel the session by doing a long press on the button.
You can also increase the pre-session duration by turning the rotatory encoder clockwise to a maximum of 30 minutes.
When the pre-session countdown ends, the real countdown will start.
Once the timer gets to zero, the session will end, the electromagnet will turn off and you'll be free
I use to hang my box in one metal lamp like this:
The Arduino code is prepared to work with external programs or devices.
During the pre-session countdown, it prints via serial port the remaining seconds in this format: P240 (It means Pre-session and 240 seconds = 4 minutes).
When the pre-session ends, it prints the duration that the final countdown is going to be in this format D4020 (It means Duration and the number of seconds). The duration will be printed wherever if you are playing with a random timer or a fixed one.
During the session, there will be printed the total amount of seconds left in this format: S4020 (I guess that you get the idea).
When the session ends, an E will be printed in the serial port.
I have coded it including a second Serial Port using Arduinos Pins 8 and 9. So every Serial Print will be sent to both serials (the one that works via USB cable and RX/TX pins (0 and 1) and also with the custom serial via pins 8 and 9).
That means that you can include external devices as ESP32 or BT dongles to interact with the Arduino.
If you sent an integer from 1 to 9 during the session countdown, the remaining time will be increased in that number of minutes.
If you sent a zero (0) the session will end.
If you are going to connect an ESP32 (or ESP8266) remember voltage differences between ESP(3,3V) and Arduino Pins(5V). You can send data straight from the ESP to the Arduino (using TX pin at 3,3V on the ESP and receiving this data over the original TX pin in the Arduino (D0 at 5V) or via the specially created for the occasion D8 (CustomSerial RX)).
If you want to read in the ESP the data sent by the Arduino, then you are going to need a level converter to deal with voltage differences.
It continues in the second post…