Skip to content

Instantly share code, notes, and snippets.

@nhooyr
Last active April 16, 2017 17:51
Show Gist options
  • Save nhooyr/5e6e11318ee2505922c2ef89f0ad5a7e to your computer and use it in GitHub Desktop.
Save nhooyr/5e6e11318ee2505922c2ef89f0ad5a7e to your computer and use it in GitHub Desktop.
#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