Source
Escape_Game_Final_Floppydisc.ino
/**
* Floppy Disc Console (c) 2022 Alastair Aitchison
*
* A device to simulate uploading/downloading data from a floppy disc,
* such as those (allegedly) used to store the U.S. military nuclear launch codes.
*
* Players must find three floppy discs, insert them into the drive, and select the
* correct file from each to upload. Once all three items of data have been loaded,
* a message can be displayed, a relay triggered etc. etc.
*/
// INCLUDES
// The LCD display uses an I2C interface provided by Arduino's "Wire" library
#include <Wire.h>
// LCD display module with I2C interface.See https://github.com/duinoWitchery/hd44780
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>
// RFID reader library. See https://github.com/miguelbalboa/rfid
#include <MFRC522.h>
// STRUCTURES
// This data structure encapsulates all the properties associated with an individual file
struct file {
char fileName[16]; // The name for this file as will appear on the display
unsigned int fileSize; // From 0-65,535
unsigned long fileDate; // Timestamp
};
// This data structure contains info relating to each "disk" (RFID card) that is scanned
struct card {
byte rfidUID[4]; // Unique identifier of the RFID tag
file contents[4]; // The array of files this card contains
byte correctFileID; // The file that needs uploading from this disc
};
// CONSTANTS
// The number of different cards players can insert
const int numTags = 3;
// The number of files contained on each card
const byte numFiles = 4;
// Define the data held on each "disk"
//A37708DA, 43702BDA, 5C 7B E4 AF,8E 92 AF 9F
// nav is card A, guid is card Y, traj is card F
const card cardList[numTags] = {
{ {0x43,0x70, 0x2B, 0xDA}, { {"ORION.nav", 100, 1}, {"HERCULES.nav", 200, 1}, {"LEO.nav", 100, 1}, {"BOOTES.nav", 100, 1}, }, 3 },
{ {0x5C,0x7B, 0xE4, 0xAF}, { {"Alpha_6.guid", 300, 1}, {"Alpha_9.guid", 200, 1}, {"Alpha_3.guid", 100, 1}, {"Alpha_8.guid", 300, 1}, }, 1},
{ {0x8E,0x92, 0xAF, 0x9F}, { {"07685.traj", 400, 1}, {"52301.traj", 200, 1}, {"91254.traj", 100, 1}, {"82316.traj", 200, 1}, }, 2},
};
// GPIO pins attached to each component
// Rotary encoder/button
const byte encDataPin = 6; //sw
const byte encClockPin = 7; //dio
const byte buttonPin = 8; //clk
// Green LED that indicates floppy drive is active
const byte floppyLedPin = A0;
// Chip select line to the RFID reader
const byte rfidSS = 10; ///sck
// Blue LEDs that indicate which data has been loaded
const byte ledPins[] = {2, 3, 4};
// GLOBALS
// Initialise the LCD display (the HD44780 library auto-detects I2C address)
hd44780_I2Cexp lcd;
// Create an instance of the MFRC522 class, using the specified chip select pin
// We won't use a RST line, so set that to UINT8_MAX
MFRC522 rfid(rfidSS, UINT8_MAX);
// Is a disc currently inserted?
bool isDiscPresent = false;
// The index of the currently inserted disc
int8_t currentCard = -1;
// The file selected on the currently inserted disc
uint8_t selectedFile = 0;
// Timestamp at which we last checked whether disc was changed
unsigned long timeSinceLastScan;
// The cards whose data has been successfully loaded
bool dataLoaded[] = {false, false, false};
void setup() {
// Serial connection is initialised first, as used to print
// debugging information to Arduino serial monitor
Serial.begin(115200);
Serial.println(__FILE__ __DATE__);
Serial.print(F("Setting pin modes..."));
// Encoder
pinMode(buttonPin, INPUT_PULLUP);
pinMode(encClockPin, INPUT_PULLUP);
pinMode(encDataPin, INPUT_PULLUP);
// LEDs
pinMode(floppyLedPin, OUTPUT);
digitalWrite(floppyLedPin, LOW);
for(int i=0; i<3; i++){
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW);
}
Serial.println(F("done."));
Serial.print(F("Initialising I2C interface..."));
// Initialise the I2C interface
Wire.begin();
Serial.println(F("done."));
// LCD
Serial.print(F("Initialising LCD display..."));
// Initialise the display with specified number of columns/rows
lcd.begin(20,4);
lcd.setBacklight(255);
// Create custom characters to draw the bargraph on the LCD display
// Up to 8 custom chars can be saved, which can be drawn by their index, e.g. lcd.write(byte(0));
// (Here we use 7 custom chars, so there's one spare left)
// For a graphical tool to help create this data, see https://www.quinapalus.com/hd44780udg.html
uint8_t customChars[][8] = {
{B01111, B11000, B10000, B10000, B10000, B10000, B11000, B01111}, // Start of bar, empty
{B01111, B11000, B10011, B10111, B10111, B10011, B11000, B01111}, // Start of bar, filled
{B11111, B00000, B00000, B00000, B00000, B00000, B00000, B11111}, // Middle of bar, empty
{B11111, B00000, B11000, B11000, B11000, B11000, B00000, B11111}, // Middle of bar, half-filled
{B11111, B00000, B11011, B11011, B11011, B11011, B00000, B11111}, // Middle of bar, filled
{B11110, B00011, B00001, B00001, B00001, B00001, B00011, B11110}, // End of bar, empty
{B11110, B00011, B11001, B11101, B11101, B11001, B00011, B11110}, // End of bar, filled
};
// Copy the custom characters to the LCD memory
for (int i=0; i<7; i++){
lcd.createChar(i, customChars[i]);
}
lcd.clear();
lcd.home();
lcd.print("Launch Console");
lcd.setCursor(0, 2);
lcd.print("Insert disc to");
lcd.setCursor(0, 3);
lcd.print("upload file data");
Serial.println(F("done."));
Serial.print(F("Initialising RFID reader..."));
// Initialise the SPI bus
SPI.begin();
// Initialise the RFID reader
rfid.PCD_Init();
// Wait for stabilisation
delay(100);
rfid.PCD_DumpVersionToSerial();
Serial.println(F("done."));
}
void loop() {
// Check every frame whether the encoder has been turned (even if not required)
int8_t knobChange = readRotary();
// Has sufficient time elapsed since last checking the disc?
if(millis() - timeSinceLastScan > 1000){
timeSinceLastScan = millis();
// Check if any tag within range can be read
if (PICC_IsAnyCardPresent() && rfid.PICC_ReadCardSerial()) {
// Light up the floppy disc active LED
digitalWrite(floppyLedPin, HIGH);
// If there wasn't previously a card present
if(!isDiscPresent) {
isDiscPresent = true;
// Create a string of the tag ID (8 char ID + 1 byte terminator)
char buffer[9];
sprintf(buffer, "%02X%02X%02X%02X", rfid.uid.uidByte[0], rfid.uid.uidByte[1], rfid.uid.uidByte[2], rfid.uid.uidByte[3]);
// Debug output to serial
Serial.print(F("Card detected! "));
Serial.println(buffer);
// Update LCD screen
lcd.clear();
lcd.home();
lcd.print("Disk ID:");
lcd.print(buffer);
lcd.setCursor(0,2);
lcd.print("Accessing Files");
// Draw progress bar
for(int percent=0; percent<101; percent++){
showProgressBar(percent, 0, 3, 20);
delay(10);
}
// Find out what disk has been inserted
currentCard = -1;
for(int i=0; i<numTags; i++) {
// If the ID of the disc current inserted matches one of those in the cardList array
if(memcmp(cardList[i].rfidUID, rfid.uid.uidByte, sizeof cardList[i].rfidUID) == 0) {
// Note the index of which card is inserted
currentCard = i;
// Print the disk contents on the LCD display
lcd.clear();
showFileList(currentCard);
showCursor(selectedFile);
break;
}
}
// If we're *not* able to find a matching ID in the array of know tags
if(currentCard == -1) {
// This section of code will only be reached if a card is scanned that is not recognised
// i.e. it shouldn't happen!
lcd.setCursor(0,2);
lcd.print(F("Disc Not Recognised"));
lcd.setCursor(0,3);
for(int n=0; n<20; n++) {
lcd.print(" ");
}
}
}
}
// We used to have a card inserted, but can no longer find one
else if(isDiscPresent){
Serial.println(F("Card Removed!"));
lcd.clear();
lcd.home();
lcd.print("Launch Console");
lcd.setCursor(0, 2);
lcd.print("Insert disc to");
lcd.setCursor(0, 3);
lcd.print("upload file data");
isDiscPresent = false;
digitalWrite(floppyLedPin, LOW);
}
// Tell PICC to halt
rfid.PICC_HaltA();
// Stop the PCD
rfid.PCD_StopCrypto1();
}
// We only scroll through files when a disc has been inserted!
if(isDiscPresent){
// And it has been recognised
if(currentCard != -1) {
// If scroll wheel has moved
if(knobChange){
// Update the selected file
selectedFile = constrain(selectedFile + knobChange, 0, numFiles-1);
Serial.print(selectedFile);
showCursor(selectedFile);
}
// If the button is clicked
if(digitalRead(buttonPin) == LOW){
// Upload the currently selected file
upload();
// Wait for the button to be released
while(digitalRead(buttonPin) == LOW){
delay(10);
}
}
}
}
// If the disc is *not* present
else {
// Nothing to see here...
}
// Check whether all data has been uploaded...
bool allDataLoaded = true;
for(int i=0; i<3; i++) {
if(dataLoaded[i] == false) {
allDataLoaded = false;
break;
}
}
// ...if so
if(allDataLoaded){
// Update the display
lcd.clear();
lcd.setCursor(1, 0);
lcd.print(F("DATA LOAD COMPLETE"));
lcd.setCursor(2, 2);
lcd.print(F("READY FOR LAUNCH"));
lcd.setCursor(3,2);
lcd.print(F("573"));
// Insert any desired logic here to, e.g. trigger a relay etc.
while(true) {
delay(100);
}
}
}
/**
* Display the set of files contained on a disc
*/
void showFileList(uint8_t cardNum){
if(cardNum < 0 || cardNum >= numTags) {
Serial.print("ERROR");
return;
}
Serial.print("Printing contents of disc #");
Serial.println(cardNum);
for(int i=0; i<numFiles; i++){
// Choose what info to display about each file - using fileName, fileSize, fileDate
char line[19];
memset(line, 0, sizeof line);
snprintf(line, 19, "%s", cardList[currentCard].contents[i].fileName);
// Debug
Serial.print(" -");
Serial.println(line);
// Output to LCD display
// Start at first column in to leave room for cursor
lcd.setCursor(1,i);
lcd.print(line);
}
}
/**
* Display the cursor that highlights the currently selected file
*/
void showCursor(uint8_t lineNum) {
// Show the cursor
for(int i=0; i<numFiles; i++){
lcd.setCursor(0,i);
lcd.print( i==lineNum ? ">" : " ");
}
}
/**
* Simulates the action of "uploading" the currently selected file,
* displaying a progress bar and then reporting if successful
*/
void upload() {
// Update the LCD display
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Uploading"));
lcd.setCursor(0, 2);
lcd.print(cardList[currentCard].contents[selectedFile].fileName);
// Progress bar percentage value
static byte percent = 0;
for(percent=0; percent<=100; percent++){
// Display numeric percent value on the top line
lcd.setCursor(20-4, 0);
char msg[4];
// This can look confusing - %3d is a padded 3-digit number, whereas trailing %% prints a literal percent sign
sprintf(msg, "%3d%%", percent);
lcd.print(msg);
// Draw bar 20 units wide starting at (0,3)
showProgressBar(percent, 0, 3, 20 );
delay(10);
}
// If the file uploaded is the correct file for the currently inserted disc...
if(selectedFile == cardList[currentCard].correctFileID){
// Update the array
dataLoaded[currentCard] = true;
// Light up the LED corresponding to this card
digitalWrite(ledPins[currentCard], HIGH);
// Update the LCD display
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Success:"));
lcd.setCursor(0, 2);
lcd.print(F("Upload Complete"));
}
// ... the wrong file has been uploaded
else {
// Update the array
dataLoaded[currentCard] = false;
// Turn off the corresponding LED
digitalWrite(ledPins[currentCard], LOW);
// Update the LCD display
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Error:"));
lcd.setCursor(0, 2);
lcd.print(F("Data Corrupt"));
}
// Pause a little
delay(3000);
lcd.clear();
// Return to the file listing of the currently inserted disc
showFileList(currentCard);
showCursor(selectedFile);
}
/**
* Draw a progress bar on LCD display using custom characters
* Based on code by Fabien: https://www.carnetdumaker.net/articles/faire-une-barre-de-progression-avec-arduino-et-liquidcrystal/
*/
void showProgressBar(byte percent, int x, int y, int width) {
// Move the cursor to the specified start point
lcd.setCursor(x, y);
// Map value from 0-100 percentage range to width of the bar
// Each character displays 2 vertical bars, but the first and last character display only one.
// nb_columns is the number of columns that still need to be drawn
byte nb_columns = map(percent, 0, 100, 0 , width * 2 - 2);
// Draw each segment of the line
for (byte i=0; i < width; ++i) {
// First segment
if (i==0 ) {
if(nb_columns > 0 ) {
lcd.write(1); // Filled left-end bar
nb_columns -= 1 ;
} else {
lcd.write((byte) 0 ); // Empty left-end bar
}
}
// Last segment
else if (i == width -1 ) {
// Filled end segment
if (nb_columns > 0 ) {
lcd.write(6);
}
// Empty end segment
else {
lcd.write(5);
}
}
// Every middle segment
else {
if (nb_columns >= 2) {
// Filled
lcd.write(4);
nb_columns -= 2;
} else if(nb_columns == 1 ) {
// Half-filled
lcd.write(3);
nb_columns -= 1;
}
else {
// Empty
lcd.write(2);
}
}
}
}
/**
* Reads output from rotary encoder
* A valid CW move returns 1, CCW returns -1, invalid returns 0.
* See https://www.best-microcontroller-projects.com/rotary-encoder.html
*/
int8_t readRotary() {
// Enumeration of whether pairs of "previous state, next state" values are valid
// e.g. 00->00 is invalid, so 0
// 00->01 is valid, so 1
// 00->10 is valid, so 1
// 00->11 is invalid, so 0 etc., etc.
static int8_t rot_enc_table[] = {0,1,1,0, 1,0,0,1, 1,0,0,1, 0,1,1,0};
// Used to debounce knob reading
static uint8_t prevNextCode = 0;
static uint16_t store=0;
// Create the 4bit PS-NS code for the current state
prevNextCode <<= 2;
if (digitalRead(encDataPin)) prevNextCode |= 0x02;
if (digitalRead(encClockPin)) prevNextCode |= 0x01;
prevNextCode &= 0x0f;
// Check if state is valid
if (rot_enc_table[prevNextCode] ) {
// Store latest state
store <<= 4;
store |= prevNextCode;
if ((store&0xff)==0x2b) return 1;
if ((store&0xff)==0x17) return -1;
}
return 0;
}
/**
* Returns true if a PICC responds to PICC_CMD_WUPA.
* All cards in state IDLE or HALT are invited.
*
* @return bool
*/
bool PICC_IsAnyCardPresent() {
byte bufferATQA[2];
byte bufferSize = sizeof(bufferATQA);
// Reset baud rates
rfid.PCD_WriteRegister(rfid.TxModeReg, 0x00);
rfid.PCD_WriteRegister(rfid.RxModeReg, 0x00);
// Reset ModWidthReg
rfid.PCD_WriteRegister(rfid.ModWidthReg, 0x26);
MFRC522::StatusCode result = rfid.PICC_WakeupA(bufferATQA, &bufferSize);
return (result == MFRC522::STATUS_OK || result == MFRC522::STATUS_COLLISION);
} // End PICC_IsAnyCardPresent()