Last active
April 16, 2017 17:51
-
-
Save nhooyr/5e6e11318ee2505922c2ef89f0ad5a7e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <ArduinoSTL.h> | |
#include <LiquidCrystal.h> | |
#include <Servo.h> | |
// Buzzer represents a passive buzzer. | |
// It exposes an interface to control the sound produced by the buzzer. | |
class Buzzer { | |
// signal_pin is the pin to which the passive buzzer's signal pin | |
// is connected. | |
const uint8_t signal_pin; | |
public: | |
// Buzzer instantiates the class with the pin to which the passive | |
// buzzer's signal pin is connected. | |
Buzzer(const uint8_t signal_pin) : signal_pin(signal_pin) {} | |
// buzz will cause the passive buzzer to produce sound. | |
// It generates a square wave alternating current with the specified | |
// frequency on signal_pin. The frequency of the current will drive | |
// the frequency of the produced sound. The tone will be played | |
// indefinitely until buzz is called again. | |
// This is just a wrapper around the standard tone function. | |
void buzz(const unsigned int freq) { | |
tone(signal_pin, freq); | |
} | |
// Identical to the above but also takes a duration in milliseconds | |
// for how long the tone should be played. | |
void buzz(const unsigned int freq, const unsigned long duration) { | |
tone(signal_pin, freq, duration); | |
} | |
} buzzer(13); | |
// Lock abstracts away a servo as the locking mechanism. | |
// It exposes an interface to control and query the state of the mechanism. | |
class Lock : private Servo { | |
// signal_pin is the pin to which the servo's signal pin is connected. | |
const uint8_t signal_pin; | |
// unlocked holds the current state of the lock. | |
bool unlocked; | |
// sync_servo syncs the current state with the servo. | |
// For the servo, the unlocked state is 0 degrees and | |
// the locked state is 180 degrees. | |
void sync_servo() { | |
if (unlocked) { | |
Servo::write(0); | |
} else { | |
Servo::write(180); | |
} | |
} | |
public: | |
// Lock instantiates the class with the pin to which the servo's | |
// signal pin is connected. | |
Lock(const uint8_t signal_pin) : signal_pin(signal_pin) {} | |
// attach attaches the inherited servo object to the signal_pin and | |
// syncs the servo with the initial locked state. | |
void attach() { | |
Servo::attach(signal_pin); | |
sync_servo(); | |
} | |
// get_state returns the current state of the lock as an uppercase string. | |
String get_state() { | |
if (unlocked) { | |
return "UNLOCKED"; | |
} | |
return "LOCKED"; | |
} | |
// toggle toggles the state of the lock and syncs the new state | |
// to the servo. | |
void toggle() { | |
unlocked = !unlocked; | |
sync_servo(); | |
} | |
} lock(A0); | |
// Joystick exposes an interface to read positions from a joystick. | |
class Joystick { | |
// Pin to which the VRx pin of the joystick is connected. | |
const uint8_t x_pin; | |
// Pin to which the VRy pin of the joystick is connected. | |
const uint8_t y_pin; | |
// max is half the maximum value returned by analogRead. | |
// It is the absolute maximum value returned by read_offset. | |
const float max = 1023 / 2; | |
// read_offset is the same as analogRead but with the value | |
// translated such that it is an offset from 0 on the number line. | |
// The range of analogRead is from 0 to 1023, making the middle 511.5. | |
// If we take the middle and subtract the value from analogRead, | |
// we effectively made the value a offset from 0 and the previous middle | |
// became the max. | |
// In other words, we centered the range of the function at 0. | |
// The following diagrams may help clarify this. | |
// Original range: 0 ----------- 511.5 ---- 1023 | |
// New range: -511.5 ------- 0 ------- 511.5 | |
// In addition, for the joystick a high voltage means left or down. | |
// This also flips that so that a high voltage means up or right. | |
// See read_pos for why all this is important. | |
float read_offset(const uint8_t pin) { | |
return max - analogRead(pin); | |
} | |
// tolerance is the variance in which a position is detected. | |
// For example, in this case 100 units up, right, down or left | |
// from the origin will register as the middle position. | |
const float tolerance = 100; | |
// threshold is the maximum of read_offset offset by the tolerance. | |
// If either the x or y absolute value value is beyond this threshold, | |
// an up, right, down or left position will be registered. | |
// For example, in this case, 100 units left from the full right position will | |
// register as the right position. | |
const float threshold = max - tolerance; | |
public: | |
// Joystick initializes the class with the pins that | |
// the joystick's VRx and VRy pins are connected to. | |
// **IMPORTANT** | |
// The x and y pins take each other's place here because the | |
// joystick is rotated 90 degrees clockwise for optimal usability. | |
// If you do change this, you will need to also adjust the positions in read_pos. | |
// Just figure out the new positions that the values mean experimentally. | |
Joystick(const uint8_t x_pin, const uint8_t y_pin) | |
: x_pin(y_pin), y_pin(x_pin) {} | |
// Position contains the possible positions of the joystick. | |
enum class Position { | |
UP = 'U', | |
RIGHT = 'R', | |
DOWN = 'D', | |
LEFT = 'L', | |
MIDDLE = 'M', | |
VAGUE = '?', | |
}; | |
// read_pos reads x_pin and y_pin and interprets their values as a joystick position. | |
// read_offset is used instead of analogRead because since it makes the values an | |
// offset from 0, we can think of the joystick as centered on a cartesian plane. | |
// This makes the code that interprets the positions significantly easier to reason | |
// about and more symmetric. Up becomes a high offset from 0 in y and right becomes a high | |
// offset from 0 in x and vice versa for the opposite positions. | |
// The following diagrams may help clarify. | |
// Before: | |
// 0 | |
// | | |
// | | |
// | | |
// | | |
// 1023 --------- 511.5 --------- 0 | |
// | | |
// | | |
// | | |
// | | |
// 1023 | |
// After: | |
// 511.5 | |
// | | |
// | | |
// | | |
// | | |
// -511.5 --------- 0 --------- 511.5 | |
// | | |
// | | |
// | | |
// | | |
// -511.5 | |
Position read_pos() { | |
Position x_pos = Position::VAGUE; | |
float x = read_offset(x_pin); | |
if (x < -threshold) { | |
x_pos = Position::LEFT; | |
} else if (x > threshold) { | |
x_pos = Position::RIGHT; | |
} else if (x > -tolerance && x < tolerance) { | |
x_pos = Position::MIDDLE; | |
} | |
Position y_pos = Position::VAGUE; | |
float y = read_offset(y_pin); | |
if (y < -threshold) { | |
y_pos = Position::DOWN; | |
} else if (y > threshold) { | |
y_pos = Position::UP; | |
} else if (y > -tolerance && y < tolerance) { | |
y_pos = Position::MIDDLE; | |
} | |
// The following chain of if statements decides which position | |
// to prioritize, x_pos or y_pos. | |
// It also considers that Position::Middle should only be returned | |
// when both y_pos and x_pos are Position::Middle because otherwise, | |
// the joystick is not really in the middle. | |
if (x_pos == Position::MIDDLE) { | |
return y_pos; | |
} else if (y_pos == Position::MIDDLE) { | |
return x_pos; | |
} else if (x_pos == Position::VAGUE) { | |
// x is to the side of the middle but not quite right or left. | |
// We know that y_pos is not 'M' so we can safely return it. | |
// The joystick might be up but slightly to the side such | |
// that x_pos is vague. | |
return y_pos; | |
} else if (y_pos == Position::VAGUE) { | |
// y is above or below the middle but not quite up or down. | |
// We know that x_pos is not 'M' so we can safely return it. | |
// The joystick might be right but slightly up such that | |
// y_pos is vague. | |
return x_pos; | |
} | |
// The joystick is in a corner or not in the middle but not quite | |
// in any particular defined position yet. | |
return Position::VAGUE; | |
} | |
} joystick(A1, A2); | |
// LCD encapsulates the standard LiquidCrystal class to offer | |
// a similar interface to our other classes. It also adds a | |
// few convenience functions. | |
class LCD : private LiquidCrystal { | |
// Number of columns on the LCD. | |
const uint8_t cols; | |
// Number of rows on the LCD. | |
const uint8_t rows; | |
public: | |
// LCD calls the LiquidCrystal constructor to instantiate the inherited | |
// LiquidCrystal class with the pins on which the LCD is connected. | |
// In addition, it instantiates the LCD class with the rows and columns. | |
LCD(const uint8_t RS, const uint8_t E, const uint8_t D4, | |
const uint8_t D5, const uint8_t D6, const uint8_t D7, | |
const uint8_t cols, const uint8_t rows) | |
: LiquidCrystal(RS, E, D4, D5, D6, D7), cols(cols), rows(rows) {} | |
// attach initializes the LCD for operation. | |
void attach() { | |
LiquidCrystal::begin(cols, rows); | |
} | |
// Exposes some methods inherited from the LiquidCrystal class. | |
using LiquidCrystal::clear; | |
using LiquidCrystal::print; | |
using LiquidCrystal::setCursor; | |
// print will print the two string arguments on separate lines of the LCD. | |
void print(const String &l1, const String &l2) { | |
clear(); | |
print(l1); | |
setCursor(0, 1); | |
print(l2); | |
} | |
// print will convert the given position to a char and then print it. | |
void print(const Joystick::Position pos) { | |
print(char(pos)); | |
} | |
} lcd(6, 7, 8, 9, 10, 11, | |
16, 2); | |
// Guard represents a guard that guards the lock. | |
// It decides on when more input is necessary, when to toggle the state, | |
// when to ask the user to try again or when to enter lockdown mode. | |
// It exposes a simple interface to begin processing input. | |
class Guard { | |
// code stores the super secret code as a vector of joystick positions. | |
// A vector is like an array but appreciably easier to work with. | |
const std::vector<Joystick::Position> code; | |
// input stores the user input as a vector of joystick positions, | |
// just like code. | |
std::vector<Joystick::Position> input; | |
// tries_left is the number of attempts the user has left | |
// to get the code correct before the system enters lockdown mode. | |
int tries_left = 3; | |
public: | |
// Guard instanstiates the class with the given code. | |
Guard(std::vector<Joystick::Position> code) : code(code) {} | |
// get_tries_left is an accessor method for the tries_left member field. | |
int get_tries_left() { | |
return tries_left; | |
} | |
// Response contains the possible responses that Guard | |
// will give.. | |
enum class Response { | |
MORE, | |
SUCCESS, | |
FAILURE, | |
LOCKDOWN, | |
}; | |
// ask asks the guard what the system needs to do next now that the user | |
// has inputted another position. | |
// It returns the appropriate Response that the system should take. | |
Response ask(const Joystick::Position pos) { | |
// Push the position to end of the input vector. | |
input.push_back(pos); | |
if (input.size() < code.size()) { | |
// If the size of the input is less than the code, | |
// we need to read more positions. | |
return Response::MORE; | |
} | |
if (input != code) { | |
tries_left--; | |
if (tries_left == 0) { | |
return Response::LOCKDOWN; | |
} | |
// Reset the input vector. | |
input.resize(0); | |
return Response::FAILURE; | |
} | |
// Reset the input vector. | |
input.resize(0); | |
// When the user wants to toggle the state of the lock again, | |
// they once again get 3 tries before lockdown mode is initiated. | |
tries_left = 3; | |
return Response::SUCCESS; | |
} | |
} guard({Joystick::Position::UP, | |
Joystick::Position::UP, | |
Joystick::Position::DOWN, | |
Joystick::Position::RIGHT, | |
}); | |
// Enters lockdown mode. A blinking message will be printed | |
// on the LCD indefinitely. In addition, an alarm will be buzzed | |
// on the buzzer. | |
void lockdown() { | |
while (true) { | |
buzzer.buzz(3000); | |
lcd.print("LOCKDOWN", | |
"ALERTING POLICE"); | |
delay(600); | |
buzzer.buzz(100, 500); | |
lcd.clear(); | |
delay(500); | |
} | |
} | |
// Prints a message to the LCD displaying the current state | |
// of the lock and asking for the code. | |
void print_query() { | |
lcd.print(lock.get_state(), | |
"CODE: "); | |
} | |
// wait_center waits until the joystick is in the middle | |
// before returning. | |
void wait_center() { | |
while (true) { | |
if (joystick.read_pos() == Joystick::Position::MIDDLE) { | |
return; | |
} | |
} | |
} | |
// setup performs some initialization before the loop | |
// function is repeatedly called. | |
void setup() { | |
lock.attach(); | |
lcd.attach(); | |
print_query(); | |
} | |
// loop is repeatedly called after setup returns. | |
// It contains the code that makes all our declared objects | |
// work together and create a fully functioning keyless lock. | |
void loop() { | |
Joystick::Position pos = joystick.read_pos(); | |
if (pos == Joystick::Position::VAGUE || pos == Joystick::Position::MIDDLE) { | |
// If the joystick is not up, right, down or left we have nothing to do. | |
return; | |
} | |
// Allow the user to see the position they just entered. | |
lcd.print(pos); | |
// Play some auditory feedback for the user. | |
// We are not using different frequencies for different | |
// positions to prevent a side channel attack. | |
buzzer.buzz(250, 200); | |
// Ask the guard what we need to do next now that the user | |
// has inputted another position. | |
switch (guard.ask(pos)) { | |
case Guard::Response::MORE: | |
// Guard wants more input. We will wait until the joystick is back | |
// in the middle to avoid reading duplicate positions. | |
// We could use a delay, but this is far more robust. | |
wait_center(); | |
return; | |
case Guard::Response::SUCCESS: | |
// The guard says we can toggle the lock. | |
lock.toggle(); | |
// Some auditory feedback. | |
buzzer.buzz(1500, 200); | |
// Allows the user to see last position entered. | |
delay(200); | |
break; | |
case Guard::Response::FAILURE: { | |
// Guard says that the user failed to enter the correct code. | |
// Allow the user to see last position entered. | |
delay(200); | |
// Print a message requesting the user try again. | |
// Proper grammar is important. | |
lcd.print("TRY AGAIN", | |
guard.get_tries_left() == 2 | |
? "2 TRIES LEFT" | |
: "1 TRY LEFT"); | |
// These buzzes with delays give the user some nice feedback and | |
// allow him or her to read the message before we print the query. | |
buzzer.buzz(700); | |
delay(500); | |
buzzer.buzz(100, 600); | |
delay(600); | |
break; | |
} | |
case Guard::Response::LOCKDOWN: | |
// The guard wants us to enter lockdown mode. | |
// Allow the user to see last position entered. | |
delay(200); | |
lockdown(); | |
// Unreachable as lockdown should never return. | |
} | |
// We need to refresh the query on the LCD because the state | |
// was changed or the try again message was printed. | |
print_query(); | |
// We will wait until the joystick is in the center to avoid | |
// reading duplicate positions. | |
wait_center(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment