// 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;
// 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 = 7;
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;
// Remote-command variabes
const unsigned long remoteSwitchScroll = 0x1fe48b7;
const unsigned long remotePause = 0x1fee817;
// 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.
//
// TODO: Pause.
// TODO: A kind of manual scrolling, using the remote to move the text.
// TODO: Change in charStep and speed, saving it for every player.
unsigned short counter;
const String spaceFilling = " ";
String message;
unsigned long prevMillis;
unsigned short updateStep;
byte charStep;
char direct;
public:
Scroller(String msg, char drc) {
message = spaceFilling + msg + spaceFilling;
counter = 0;
charStep = 2;
direct = drc;
if (direct == 'H') updateStep = 400;
if (direct == 'V') updateStep = 1000;
}
void Update() {
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;
}
}
}
} 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;
}
}
}
}
}
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; // To lower-case.
} else {
direct &= ~(B00100000); // To upper-case.
}
}
};
// Interruption functions
void detectMotion() {
motion = true;
prevActiveMillis = millis();
}
// Set-up
Scroller aMessage("This is quite a long message to show in such a tiny screen. =P", 'H');
void setup() {
pinMode(motionPin, INPUT);
attachInterrupt(digitalPinToInterrupt(motionPin), detectMotion, RISING);
lcd.init();
lcd.noDisplay();
irrecv.enableIRIn();
}
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("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)) {
aMessage.Update();
if (irrecv.decode(&results)) {
switch (results.value) {
case remoteSwitchScroll:
aMessage.switchDirection();
prevActiveMillis = millis();
break;
case remotePause:
aMessage.pauseOrResume();
prevActiveMillis = millis();
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;
}
}
}