// Copyright © 2018 José Alberto Orejuela García (josealberto4444)
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Additional permission under GNU AGPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with LiquidCrystal_I2C (or a modified version of that library), containing
// parts covered by the terms of LiquidCrystal_I2C's license, the licensors of
// this program grant you additional permission to convey the resulting work.
// Quiz
//
// This program is intended to be used in an Arduino board (or compatible, or
// simulator). The purpose is to have a screen where some questions will appear,
// and answer them with a remote control.
#include <LiquidCrystal_I2C.h> // Unlicensed, then non-free, so that's the reason for that additional permission in the preamble.
#include <IRremote.h> // LGPL-2.1 (great, no problem).
// IMPORTANT: Tweak IRremote library for using timer1 instead of timer2, or it
// will conflict with tone function. In the case of Arduino Uno (or any
// ATmega328), this is done (at the moment of writing) uncommenting line 194 and
// commentind 195 in boarddefs.h file.
// Set-up variables
const byte motionPin = 2; // Motion sensor pin.
LiquidCrystal_I2C lcd(0x27,16,2); // LCD address and size.
const byte irrecvPin = 10; // IR receptor pin.
IRrecv irrecv(irrecvPin); // Initialisation.
decode_results results;
char message[64]; // Displaying message, will be populated from FLASH memory.
//#define _DEBUG 0 // Uncomment to get debugging messages via serial port in certain steps.
// State-recording variables
volatile bool motion = false; // Is there any movement that should turn on the device?
bool screenState = false; // Is the screen actually on?
bool welcomeToneState = false; // Is the welcome tone sounding?
bool welcomeScreenState = false; // Is the welcome screen playing?
// Timing variables
const unsigned short secondsToInactive = 80;
volatile unsigned long prevActiveMillis = 0;
unsigned long prevToneMillis = 0;
unsigned long prevScreenMillis = 0;
const byte toneStep = 100;
unsigned short screenStep = 300; // Not a byte because it is modified to a value of 2000
byte toneCounter = 0;
byte screenCounter = 0;
byte questionCounter = 0;
byte qaCounter = 0;
// Remote-command variabes
const unsigned long remoteSwitchScroll = 0x1fe48b7;
const unsigned long remotePause = 0x1fee817;
const unsigned long remoteOne = 0x1fe807f;
const unsigned long remoteTwo = 0x1fe40bf;
const unsigned long remoteThree = 0x1fec03f;
const unsigned long remoteFour = 0x1fe20df;
// Scrolling class
class Scroller {
// Receive a message and scroll it through the screen
//
// If scrolling horizontally, charStep is the amount of characters in every
// step.
unsigned short counter;
const String spaceFilling = " ";
unsigned long prevMillis;
unsigned short updateStep;
byte charStep;
char direct;
public:
bool completelyDisplayed;
String message;
Scroller(String msg, char drc) {
reset(msg);
charStep = 2;
direct = drc;
if (direct == 'H') updateStep = 400;
if (direct == 'V') updateStep = 1000;
}
void reset(String msg) {
message = spaceFilling + msg + spaceFilling;
counter = 0;
completelyDisplayed = false;
}
void scroll() {
unsigned long currentMillis = millis();
if (direct == 'H') { // Horizontal scrolling.
if (counter == 0) {
lcd.setCursor(0, 0);
lcd.print(message.substring(counter, counter+16));
counter += charStep;
prevMillis = millis();
} else {
if (currentMillis - prevMillis >= updateStep) {
lcd.setCursor(0, 0);
lcd.print(message.substring(counter, counter+16));
counter += charStep;
prevMillis = millis();
if (counter >= message.length() - 16 + charStep) {
counter = 0;
completelyDisplayed = true;
}
}
}
} else if (direct == 'V') { // Vertical scrolling.
if (counter == 0) {
lcd.setCursor(0, 0);
lcd.print(message.substring(counter, counter+16));
lcd.setCursor(0, 1);
lcd.print(message.substring(counter+16, counter+32));
counter += 16;
prevMillis = millis();
} else {
if (currentMillis - prevMillis >= updateStep) {
lcd.setCursor(0, 0);
lcd.print(message.substring(counter, counter+16));
lcd.setCursor(0, 1);
lcd.print(message.substring(counter+16, counter+32));
counter += 16;
prevMillis = millis();
if (counter >= message.length()) {
counter = 0;
completelyDisplayed = true;
}
}
}
}
}
void switchDirection() {
if (direct == 'H') {
direct = 'V';
updateStep = 1000;
} else if (direct == 'V') {
direct = 'H';
updateStep = 400;
// Clear the second line of the screen.
lcd.setCursor(0, 1);
lcd.print(spaceFilling);
}
}
void pauseOrResume () {
if (isUpperCase(direct)) {
direct |= B00100000; // Change direction char to lower-case variant.
} else {
direct &= ~(B00100000); // To upper-case.
}
}
};
// Questions and answers
char buffer[256]; // Maximum length of any question or answer.
const char q_0[] PROGMEM = "What is the number of electrons going through the normal section of a wire whose current is 0.2 A during 16 s?";
const char a_00[] PROGMEM = "Answer 1: 10^16";
const char a_01[] PROGMEM = "Answer 2: 10^19";
const char a_02[] PROGMEM = "Answer 3: 1.6 * 10^19";
const char a_03[] PROGMEM = "Answer 4: 2.0 * 10^19";
const char q_1[] PROGMEM = "A particle's trajectory is given by the parametric equations x = t, y = t^2 / 2. What is its curvature radius?";
const char a_10[] PROGMEM = "Answer 1: (1+t^2)^(3/2)";
const char a_11[] PROGMEM = "Answer 2: (1+t^3)^(1/2)";
const char a_12[] PROGMEM = "Answer 3: (1+t)^(3/2)";
const char a_13[] PROGMEM = "Answer 4: (1+t)^(1/2)";
const char* const qa[] PROGMEM = {q_0, a_00, a_01, a_02, a_03, q_1, a_10, a_11, a_12, a_13};
const byte correctAnswers[] = {3, 0};
const byte numberOfQuestions = sizeof(qa)/10;
char* getQA(byte qaNumber) {
strcpy_P(buffer, (char*)pgm_read_word(&(qa[qaNumber])));
return buffer;
}
Scroller currentQA(getQA(qaCounter), 'H');
void isCorrect(byte ans) {
unsigned short confirmationDelay = 1000;
if (ans == correctAnswers[questionCounter]) {
questionCounter++;
if (questionCounter >= numberOfQuestions) {
questionCounter = 0;
}
qaCounter = 5*questionCounter;
currentQA.reset(getQA(qaCounter));
#ifdef _DEBUG
Serial.println(F("Correct answer"));
#endif
lcd.clear();
lcd.print(F("Correct!"));
lcd.setCursor(0,1);
lcd.print(F("Go for the next!"));
tone(8, 392, 160);
delay(160);
tone(8, 523, 400);
delay(confirmationDelay);
lcd.clear();
} else {
#ifdef _DEBUG
Serial.println(F("Wrong answer"));
#endif
lcd.clear();
lcd.print(F("Wrong answer..."));
lcd.setCursor(0,1);
lcd.print(F("Keep trying!"));
tone(8, 196, 160);
delay(160);
tone(8, 65, 400);
delay(confirmationDelay);
lcd.clear();
}
}
// Interruption-related functions
void detectMotion() {
motion = true;
prevActiveMillis = millis();
}
void updateActiveMillis() {
// Update the volatile variable prevActiveMillis securely
detachInterrupt(digitalPinToInterrupt(motionPin));
prevActiveMillis = millis();
attachInterrupt(digitalPinToInterrupt(motionPin), detectMotion, RISING);
}
void setup() {
pinMode(motionPin, INPUT);
attachInterrupt(digitalPinToInterrupt(motionPin), detectMotion, RISING);
lcd.init();
lcd.noDisplay();
irrecv.enableIRIn();
#ifdef _DEBUG
Serial.begin(9600);
#endif
}
void loop() {
// Trigger welcoming messages if there is motion and the screen is off:
if (motion && !(screenState)) {
welcomeToneState = true;
welcomeScreenState = true;
}
if (welcomeToneState) {
// Welcome tone: beep-beep happening at background.
unsigned long currentMillis = millis();
if (toneCounter == 0) {
tone(8, 4978, 40);
toneCounter++;
prevToneMillis = millis();
} else if ((toneCounter == 1) && (currentMillis - prevToneMillis >= toneStep)) {
tone(8, 4978, 40);
toneCounter = 0;
welcomeToneState = false;
}
}
if (welcomeScreenState) {
unsigned long currentMillis = millis();
// Welcome screen: the screenlight blinks twice and then a "Hello!" message
// is shown. This is splitted in several steps to be able to manage other
// tasks (like the welcome tone) at the same time.
if (screenCounter == 0) {
lcd.display();
lcd.backlight();
screenState = true;
screenCounter++;
prevScreenMillis = millis();
} else if (currentMillis - prevScreenMillis >= screenStep) {
switch (screenCounter) {
case 1:
lcd.noBacklight();
screenCounter++;
prevScreenMillis = millis();
break;
case 2:
lcd.backlight();
screenCounter++;
prevScreenMillis = millis();
break;
case 3:
lcd.noBacklight();
screenCounter++;
prevScreenMillis = millis();
break;
case 4:
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print(F("Hello!"));
screenStep = 2000;
screenCounter++;
prevScreenMillis = millis();
break;
case 5:
lcd.clear();
screenStep = 300;
screenCounter = 0;
welcomeScreenState = false;
break;
}
}
}
// Work only if the screen is on.
if (screenState) {
// If you are not welcoming, do your job.
if (!(welcomeToneState || welcomeScreenState)) {
#ifdef _DEBUG
Serial.println(currentQA.message);
#endif
if (!(currentQA.completelyDisplayed)) {
currentQA.scroll();
#ifdef _DEBUG
Serial.println(F("Scrolling message"));
#endif
} else {
qaCounter++;
if (qaCounter >= 5*(questionCounter+1)) {
qaCounter = 5*questionCounter;
#ifdef _DEBUG
Serial.println(F("Variable qaCounter overflowed > restarted"));
#endif
}
currentQA.reset(getQA(qaCounter));
#ifdef _DEBUG
Serial.println(F("Scrolling class reset with new message"));
#endif
}
if (irrecv.decode(&results)) {
switch (results.value) {
case remoteSwitchScroll:
currentQA.switchDirection();
updateActiveMillis();
break;
case remotePause:
currentQA.pauseOrResume();
updateActiveMillis();
break;
case remoteOne:
isCorrect(0);
updateActiveMillis();
break;
case remoteTwo:
isCorrect(1);
updateActiveMillis();
break;
case remoteThree:
isCorrect(2);
updateActiveMillis();
break;
case remoteFour:
isCorrect(3);
updateActiveMillis();
break;
}
irrecv.resume();
}
}
// Check if it's already time to go to sleep.
unsigned long currentMillis = millis();
// Securely save a copy of the volatile variable as it's longer than a byte
// (so it takes more than a cycle to save it and there can be reading
// problems).
detachInterrupt(digitalPinToInterrupt(motionPin)); // First, stop interruptions
unsigned long savedPrevActiveMillis = prevActiveMillis; // then save the volatile variable and
attachInterrupt(digitalPinToInterrupt(motionPin), detectMotion, RISING); // finally recall interruptions.
if ((currentMillis - savedPrevActiveMillis)/1000 >= secondsToInactive) {
// Go to sleep and switch all related state-variables.
lcd.clear();
lcd.noBacklight();
lcd.noDisplay();
screenState = false;
motion = false;
}
}
}