// Author : Quentin BENETHUILLERE // Date of creation : 2021/05/01 // Last modification : 2021/05/31 /* * Program function(s): * This program is the MAIN program that controls the puzzle box that I designed for the FabAcademy program 2021. * It handles the following functions : * - Communication with a LCD using i2c protocol. * - Communication with 2 RFID MFR522 card readers using SPI protocol. * - Communication with a BT module HC-06 through UART, that allows to take control of the puzzle box through a phone application. * - Control of 2 separate solenoid lock mechanisms. * - Control of a servo motor that acts also as a lock mechanism. * - Detection of user inputs on 4 or 5 push buttons. * * Detailed explanations and expected scenario: * - 1: Players solve puzzle 1 (handled by another PCB) => Pin puzzle 1 goes HIGH and thus solenoid 1 gets energized (opens). Pin puzzle 1 goes back to LOW after a few seconds. * - 2: Players solve puzzle 2 (handled by another PCB) => Pin puzzle 2 goes HIGH and thus solenoid 2 gets energized (opens). Pin puzzle 2 goes back to LOW after a few seconds. * - 3: Players solve a mechanical puzzle that allow to go further in the game. * - 4: At this step only, players have in their hands the 2 RFID tags to solve next puzzle. * - If a correct RFID tag is detected by one of the reader, RGB LED blinks (blue). * - If a wrong RFID tag is detected by one of the reader, the RGB LED turns ON red for a few seconds. * - If both RFID tags are presented successively within a certain amount of time to their corresponding readers, RGB LED turns ON green for a few seconds. * - The sequence is reset after a wrong RFID identification or following a certain time without any RFID tag detected. * - 5: Once the puzzle described in the previous step is solved, LCD is ON and lets user solve the last puzzle which corresponds to a DNA sequence to enter using a dedicated keypad with 4 buttons : A/C/T/G. * - Each user input on the keypad is displayed on the LCD. * - If the correct sequence is entered, LCD displays an access authorization message, and the servomotor rotates to allow players to open final door. * - If a wrong sequence is entered, LCD displays an access denied message, and the servomotor remains in its position, holding the door locked. * */ //* ----------- //* LIBRAIRIES //* ----------- #include // Library for debouncing push buttons. #include // Library for LCD using i2c protocol. #include // Library for i2c communication. #include // Library for servo motor. #include // Library for SPI protocol. #include // Library for RFID modules. /*Notes: - PICC (Proximity Integrated Circuit Card) defines the rfid card. - PCD (Proximity Coupling Device) defines the MFRC522 card reader. */ // ------------------------- // DECLARATION OF DEFINES // ------------------------- // Defining colors association for the RGB LED : #define RGB_LED_OFF 0 #define RED 1 #define GREEN 2 #define BLUE 3 //#define COMMON_ANODE // //uncomment this line ONLY if using a Common Anode RGB LED. // ------------------------- // DECLARATION OF VARIABLES // ------------------------- // ------------------------------------------------ // Adjustable variables (program main parameters) // ------------------------------------------------ // DNA puzzle : const int DNA_code_length = 10; //char DNA_code[DNA_code_length+1]="CTGGATACCAAGTACG"; // DNA sequence code to be found. [17] because character '\0' added at the end of a string input, and this avoid confusions when string comparison will be performed later. Note : +1 to account for the '\0' end character. //char DNA_code[DNA_code_length+1]="AAAAAAAAAAAAAAAA"; // For testing purposes ONLY. Note : +1 to account for the '\0' end character. char DNA_code[DNA_code_length+1]="ATGCGAATAG"; // For testing purposes ONLY. Note : +1 to account for the '\0' end character. // Warning : Do not set the "DNA_code" variable as "const" otherwise the minicore ATmega328 returns an error at compilation. Which is not the case when using "Arduino Pro Mini" board... const int debounce_delay = 5; // Delay used to debounce buttons (if button state change has been observed for this consecutive delay at least, then the state change is confirmed). // RFID puzzle : const int TIMER_clearing_uid = 10000; // Duration (ms) during which the uid of a card/tag is saved in the memory before being cleared. const int nb_tags_authorized = 1; // Number of tags/cards authorized. // Warning : necesseray to declare this object as "const int" otherwise an error message is raised. byte authorized_tags_1[nb_tags_authorized][4]={{0x8A, 0x37, 0xF2, 0x80}}; // Array of authorized uid for reader 1 (note : 1 uid = 4 bytes given in HEX). Syntax for several uid : {{0x77, 0x6F, 0xB3, 0x85},{0x9A, 0x27, 0x0A, 0x86}} byte authorized_tags_2[nb_tags_authorized][4]={{0x9A, 0x27, 0x0A, 0x86}}; // Array of authorized uid for reader 2 (note : 1 uid = 4 bytes given in HEX). Syntax for several uid : {{0x77, 0x6F, 0xB3, 0x85},{0x9A, 0x27, 0x0A, 0x86}} // Global : const int TIMER_scrolling_text = 250; // Duration between each step of LCD text scrolling. const int TIMER_scrolling_text_start = 1500; // Duration between each step of LCD text scrolling. const int TIMER_fixed_text = 3000; // Duration for which a help message is displayed if its length is <16. const int max_message_length = 100; const byte i2c_daughter_board_address = 9; // i2c address given to the daughter board. const byte nb_bytes_requested = 2; // numbers of bytes requested to the daughter board. const int TIMER_solenoids_open = 6000; // Duration (ms) during which solenoids are energized (open) when puzzles are solved. const int TIMER_solenoids_open_application = 3000; // Duration (ms) during which solenoids are energized (open) when controlled through the phone application. const int TIMER_ERROR = 3000; // Timer for which red LED is turned on following an error. const int TIMER_SUCCESS = 3000; // Timer for which green LED is turned on once puzzle is completed. const int TIMER_BLINK = 600; // Timer for which LED blinks (when reinitialization takes place, or for each touch/connection performed). // ------------------------------------------------ // PIN connections // ------------------------------------------------ // Digital const byte PIN_countdown = 4; // Pin to communicate with the countdown board. const byte PIN_button_A = 20; // Pin corresponding to button "A" of the DNA keypad. const byte PIN_button_T = 21; // Pin corresponding to button "T" of the DNA keypad. const byte PIN_button_C = 7; // Pin corresponding to button "C" of the DNA keypad. const byte PIN_button_G = 8; // Pin corresponding to button "G" of the DNA keypad. const byte PIN_button_H = 10; // Pin corresponding either to a "Erase" button or to a "Help" button to be integrated later on. const byte PIN_buttons[4]={PIN_button_A,PIN_button_C,PIN_button_T,PIN_button_G}; // array of buttons pins for faciliating programming. const byte PIN_puzzle_1 = 17; // Pin corresponding to the feedback from another PCB related to puzzle 1 status. const byte PIN_puzzle_2 = 2; // Pin corresponding to the feedback from another PCB related to puzzle 2 status. //* Analogic PIN : // N/A // SPI : const byte PIN_MOSI = 11; const byte PIN_MISO = 12; const byte PIN_SCK = 13; const byte PIN_SS_1 = 14; // Pin for Slave Selection of RFID 1. //const byte PIN_SS_2 = 15; // Pin for Slave Selection of RFID 2. const byte PIN_RST = 16; // I2C : const byte PIN_SDA = 17; const byte PIN_SCL = 18; // RX/TX : const byte RX = 0; const byte TX = 1; // PWM : const byte PIN_servo = 3; // Pin associated to the servo motor const byte PIN_RGB_LED_red = 5; // Pin associated to the red leg of the RGB LED. const byte PIN_RGB_LED_green = 6; // Pin associated to the green leg of the RGB LED. const byte PIN_RGB_LED_blue = 9; // Pin associated to the blue leg of the RGB LED. // ------------------------------------------------ // Other variables // ------------------------------------------------ // DNA puzzle: const char buttons_letters[5]={'A','C','T','G','_'}; // array of the letters associated to each push button input on the DNA keypad. char lcd_line_0[17] = "Access Code?"; // Message to be displayed on the first line of the LCD. //char lcd_line_1[DNA_code_length+1] = "________"; // Message to be displayed on the second line of the LCD. char lcd_line_1[DNA_code_length+1]; byte button_A_state = 0; // variable to know whether button A is pushed (0) or not (1). byte button_C_state = 0; // variable to know whether button C is pushed (0) or not (1). byte button_T_state = 0; // variable to know whether button T is pushed (0) or not (1). byte button_G_state = 0; // variable to know whether button G is pushed (0) or not (1). byte input_position = 0; // variable to know the position of next user input in the DNA sequence (corresponds basically to the column position the next input will be displayed in the LCD). // RFID puzzle : byte rfid_uid_1[4]; // Table containing the uid of the rfid tag/card read by the first card reader (table of 4 bytes). byte rfid_uid_2[4]; // Table containing the uid of the rfid tag/card read by the second card reader (table of 4 bytes). byte access_authorized=0; // Variable that determines whether the access is authorized (1) or denied (0). int card_detected_1 = 0; // Variable to determine whether a tag/card has been detected in the proximity of first reader. int card_detected_2 = 0; // Variable to determine whether a tag/card has been detected in the proximity of second reader. int card_read_1 = 0; // Variable to determine whether a tag/card has been identified and selected by the first reader. int card_read_2 = 0; // Variable to determine whether a tag/card has been identified and selected by the second reader. long t_card_read_1=0; // Variable to determine the last time when a card/tag has been selected by the first reader. This variable will be used to clear from the memory the uid read after a certain time. long t_card_read_2=0; // Variable to determine the last time when a card/tag has been selected by the second reader. This variable will be used to clear from the memory the uid read after a certain time. byte uid_erased_1=0; byte uid_erased_2=0; // Global : byte button_H_state = 0; // variable to know whether button Erase is pushed (0) or not (1). byte i2c_address_LCD = 0x27; // address to communicate with the LCD over i2c (Note : "0x27" by default, "0x26" if A0 and A1 are connected on the LCD i2c module) byte servo_locked = 140; // angle that defines the servo motor position to lock the door. byte servo_open = 60; // angle that defines the servo motor position to open the door. byte game_progress = 1; // variable to determine at which step of the game players are. This may be needed to provide different help messages for each step of the game. byte previous_game_progress = 1; // variable to detect changes in game progress. int last_time_game_progress_sent = 0; // variable used to regularly send the game progress to the phone application. byte end_game = 5; // variable to determine for which value of "game_progress" the game is completed. byte puzzles_status = 0; // puzzles status given by daughter board. 0 = both puzzles 1 and 2 KO, 1 = puzzle 1 OK, 2 = puzzle 2 OK. long last_help_time = 0; // time when help was requested for the last time (used to control when a help message must be stopped). byte lcd_cursor_scrolling = 0; // Variable to control scrolling text on the LCD. char received_data; // Variable corresponding to the characters received from the BT module via UART communication. char msg[max_message_length]; // variable used to display messages on the LCD. char message_custom[max_message_length]; // variable used to display on the LCD custom messages sent from the phone application. byte character_read; // variable to store the last character read in the serial communication. // WARNING : It seems necessary to have those pointers at the end of the declarations, otherwise one of the message get corrupted. It was not the case in the previous version of this program. const char *message_1 = {"Remember the Covid-19 pandemic phases we have been through."}; //{"Help message for puzzle 1"}; const char *message_2 = {"Reconstitute Covid-19 spreading around the world."}; //{"Help message for puzzle 2"}; const char *message_3 = {"Analyzing the composition of the virus is the key."}; //{"Help message for puzzle 3"}; const char *message_4 = {"Identify the common DNA sequence between the 2 variants you have discovered so far."}; //{"Help message for puzzle 4"}; // ------------------------- // DECLARATION OF OBJECTS // ------------------------- LiquidCrystal_I2C lcd(i2c_address_LCD, 16, 2);// Declaration of the i2c LCD (16 columns, 2 rows). Bounce2::Button button_A; // Declaration of push button A. Bounce2::Button button_C; // Declaration of push button C. Bounce2::Button button_T; // Declaration of push button T. Bounce2::Button button_G; // Declaration of push button G. Bounce2::Button buttons[4]={button_A,button_C,button_T,button_G}; // Declaration of an array of buttons for code optimization. Servo servo; // Declaration of the servo motor. MFRC522 rfid_1(PIN_SS_1, PIN_RST); // Declaration of a MFRC522 RFID card reader 1. //MFRC522 rfid_2(PIN_SS_2, PIN_RST); // Declaration of a MFRC522 RFID card reader 2. // ------------------------- // INITIALIZATION // ------------------------- // Setup code. Run once at power up, or each time the RESET button is pressed: void setup() { // ------------------------------------------------ // Communication // ------------------------------------------------ Serial.begin(9600); // Establishement of Serial communication, necessary for communication with the BT module. SPI.begin(); // Establishment of SPI (Serial Peripheral Inteface) communication : Wire.begin(); // Initiate the i2c communication. // ------------------------------------------------ // Objects // ------------------------------------------------ // Initialization of the LCD object (turned off in the beginning) : lcd.init(); lcd.noBacklight(); lcd.clear(); // Initialization of push buttons objects, with debounce delay and state when pressed : for (unsigned char i=0; i<4; i++){ buttons[i].attach(PIN_buttons[i]); buttons[i].interval(debounce_delay); buttons[i].setPressedState(LOW); } // Initialization of servo motor object : servo.attach(PIN_servo); servo.write(servo_locked); // setting servo motor so that door is locked : // Initialization of RFID readers: rfid_1.PCD_Init(); // Initialization of the MFRC522 object : //rfid_2.PCD_Init(); // Initialization of the MFRC522 object : // ------------------------------------------------ // Input / Output // ------------------------------------------------ pinMode(PIN_countdown, OUTPUT); pinMode(PIN_SS_1, OUTPUT); //pinMode(PIN_SS_2, OUTPUT); pinMode(PIN_RGB_LED_red, OUTPUT); pinMode(PIN_RGB_LED_green, OUTPUT); pinMode(PIN_RGB_LED_blue, OUTPUT); pinMode(PIN_puzzle_1, OUTPUT); // HW pull-down resistor is present in the PCB (but no impact anymore because this PIN is finally set as an output rather than an input as planned originally). pinMode(PIN_puzzle_2, OUTPUT); // HW pull-down resistor is present in the PCB (but no impact anymore because this PIN is finally set as an output rather than an input as planned originally). for (byte i=0; i<=4; i++){ pinMode(PIN_buttons[i],INPUT_PULLUP); } // ------------------------------------------------ // PIN initial states // ------------------------------------------------ digitalWrite(PIN_countdown,HIGH); // Coutdown should not start immediately at initialization. LOW = countdown running / HIGH = countdown not running. analogWrite(PIN_RGB_LED_red,0); // RGB LED OFF. analogWrite(PIN_RGB_LED_green,0); // RGB LED OFF. analogWrite(PIN_RGB_LED_blue,0); // RGB LED OFF. digitalWrite(PIN_puzzle_1,LOW); // Solenoid lock mechanism 1 closed. digitalWrite(PIN_puzzle_2,LOW); // Solenoid lock mechanism 2 closed. for (int i=0; i= TIMER_clearing_uid) && uid_erased_1==0){ for (byte i=0;i<4;i++){ rfid_uid_1[i]=0; } // end for uid_erased_1 = 1; //Serial.println("uid from reader 1 erased"); } // end if card_detected_1 = rfid_1.PICC_IsNewCardPresent(); card_read_1 = rfid_1.PICC_ReadCardSerial(); // If no card/tag is detected around the card reader, the program does not go any further and the whole loop is repeated. if (!(card_detected_1)){ //if (!rfid_1.PICC_IsNewCardPresent()){ //lcd.backlight(); //lcd.setCursor(0,1); //lcd.print("No card"); //Serial.println("No card/tag detected"); return; } // end if // If no card/tag has been selected after the authentication process, the program does not go any further and the whole loop is repeated. if (!(card_read_1)){ //if (!rfid_1.PICC_ReadCardSerial()){ //Serial.println("No card/tag selected"); //lcd.backlight(); //lcd.setCursor(0,1); //lcd.print("No selection"); //delay(1000); return; } // end if // If card identified and selected by the reader : //lcd.setCursor(0,1); //lcd.print("Reader 1"); // Reading the uid of the selected tag/card (composed of 4 bytes) : for (byte i = 0; i < 4; i++){ rfid_uid_1[i] = rfid_1.uid.uidByte[i]; } // end for // Displaying the card/tag uid on the Serial line : //display_uid(rfid_uid_1,1); uid_erased_1=0; // Reinitializing the RFID : rfid_1.PICC_HaltA(); // Halting PICC (making it go from "Selected" mode to "Halt" mode). rfid_1.PCD_StopCrypto1(); // Stopping encryption on PCD. // Saving the authentication time from reader 1: t_card_read_1=millis(); // Controlling Access : access_authorized=RFID_access_control_one_reader(authorized_tags_1,rfid_uid_1); // if the access is denied (a card/tag non authorized by a specific reader has been identified) : if (access_authorized==0){ set_color_by_name(RED); //Serial.println("Access denied !"); delay(TIMER_ERROR); set_color_by_name(RGB_LED_OFF); } // end if // if the access is authorized (both cards/tags have been identified simultaneously by the corresponding readers) : else if (access_authorized==1){ set_color_by_name(GREEN); //Serial.println("Access authorized !"); delay(TIMER_SUCCESS); set_color_by_name(RGB_LED_OFF); if (game_progress==3){ game_progress = game_progress+1; } // end if } // end else if // if only one of the cards/tags has been identified by the appropriate reader, access is not authorized yet, but the orange LED indicates that something is happening : else if (access_authorized==2){ set_color_by_name(BLUE); //Serial.println("One tag/card identified, present the second tag simultaneously for access authorization !"); delay(TIMER_BLINK); set_color_by_name(RGB_LED_OFF); } // end else if } // end "RFID_puzzle" function byte RFID_access_control_one_reader(byte tags_list_1[nb_tags_authorized][4], byte uid_controlled_1[4]){ // This functions determines whether access authorization can be given or not, based on the status of the 2 card readers. byte rfid_access_1=0; int debug_variable=0; byte access=0; //access authorized (1), access denied (0) for (byte i=0;i= TIMER_clearing_uid) && uid_erased_1==0){ for (byte i=0;i<4;i++){ rfid_uid_1[i]=0; } // end for uid_erased_1 = 1; //Serial.println("uid from reader 1 erased"); } // end if if (((millis()-t_card_read_2) >= TIMER_clearing_uid) && uid_erased_2==0){ for (byte i=0;i<4;i++){ rfid_uid_2[i]=0; } // end for //Serial.println("uid from reader 2 erased"); uid_erased_2 = 1; } // end if card_detected_1 = rfid_1.PICC_IsNewCardPresent(); card_detected_2 = rfid_2.PICC_IsNewCardPresent(); card_read_1 = rfid_1.PICC_ReadCardSerial(); card_read_2 = rfid_2.PICC_ReadCardSerial(); // If no card/tag is detected around the card reader, the program does not go any further and the whole loop is repeated. if (!(card_detected_1 || card_detected_2)){ lcd.setCursor(0,1); lcd.print("No card"); //Serial.println("No card/tag detected"); return; } // end if // If no card/tag has been selected after the authentication process, the program does not go any further and the whole loop is repeated. if (!(card_read_1 || card_read_2)){ //Serial.println("No card/tag selected"); lcd.setCursor(0,1); lcd.print("No selection"); return; } // end if // If card identified and selected by the first reader : if (card_read_1){ lcd.setCursor(0,1); lcd.print("Reader 1"); // Reading the uid of the selected tag/card (composed of 4 bytes) : for (byte i = 0; i < 4; i++){ rfid_uid_1[i] = rfid_1.uid.uidByte[i]; } // end for // Displaying the card/tag uid on the Serial line : //display_uid(rfid_uid_1,1); uid_erased_1=0; // Reinitializing the RFID : rfid_1.PICC_HaltA(); // Halting PICC (making it go from "Selected" mode to "Halt" mode). rfid_1.PCD_StopCrypto1(); // Stopping encryption on PCD. // Saving the authentication time from reader 1: t_card_read_1=millis(); } // end if // If card identified and selected by the second reader : if (card_read_2){ lcd.setCursor(0,1); lcd.print("Reader 2"); // Reading the uid of the selected tag/card (composed of 4 bytes) : for (byte i = 0; i < 4; i++){ rfid_uid_2[i] = rfid_2.uid.uidByte[i]; } // end for // Displaying the card/tag uid on the Serial line : //display_uid(rfid_uid_2,2); uid_erased_2=0; // Reinitializing the RFID : rfid_2.PICC_HaltA(); // Halting PICC (making it go from "Selected" mode to "Halt" mode). rfid_2.PCD_StopCrypto1(); // Stopping encryption on PCD. // Saving the authentication time from reader 2: t_card_read_2=millis(); } // end if // Controlling Access : access_authorized=RFID_access_control_two_readers(authorized_tags_1,authorized_tags_2,rfid_uid_1,rfid_uid_2); // if the access is denied (a card/tag non authorized by a specific reader has been identified) : if (access_authorized==0){ set_color_by_name(RED); //Serial.println("Access denied !"); delay(TIMER_ERROR); set_color_by_name(RGB_LED_OFF); } // end if // if the access is authorized (both cards/tags have been identified simultaneously by the corresponding readers) : else if (access_authorized==1){ set_color_by_name(GREEN); //Serial.println("Access authorized !"); delay(TIMER_SUCCESS); set_color_by_name(RGB_LED_OFF); if (game_progress==3){ game_progress = game_progress+1; } // end if } // end else if // if only one of the cards/tags has been identified by the appropriate reader, access is not authorized yet, but the orange LED indicates that something is happening : else if (access_authorized==2){ set_color_by_name(BLUE); //Serial.println("One tag/card identified, present the second tag simultaneously for access authorization !"); delay(TIMER_BLINK); set_color_by_name(RGB_LED_OFF); } // end else if } // end "RFID_puzzle_two_readers" function byte RFID_access_control_two_readers(byte tags_list_1[nb_tags_authorized][4], byte tags_list_2[nb_tags_authorized][4], byte uid_controlled_1[4],byte uid_controlled_2[4]) { // This functions determines whether access authorization can be given or not, based on the status of the 2 card readers. byte rfid_access_1=0; byte rfid_access_2=0; byte access=0; //access authorized (1), access denied (0) for (byte i=0;i 0 ){ received_data = Serial.read(); switch(received_data){ // Turning ON RGB LED in red : case 'r': //Serial.println("Red light"); set_color_by_name(RED); break; // Turning ON RGB LED in green : case 'g': //Serial.println("Green light"); set_color_by_name(GREEN); break; // Turning ON RGB LED in blue : case 'b': //Serial.println("Blue light"); set_color_by_name(BLUE); break; // Turning OFF RGB LED : case 'o': //Serial.println("Light OFF"); set_color_by_name(RGB_LED_OFF); break; // Pausing countdown : case 'p': //Serial.println("Pause countdown"); digitalWrite(PIN_countdown,HIGH); break; // Starting/Restarting countdown : case 's': //Serial.println("Start/Restart countdown"); digitalWrite(PIN_countdown,LOW); break; // Displaying help message : case 'h': //Serial.println("Help message displayed on LCD"); help_message(game_progress); break; // Going to puzzle 1 : case '1': //Serial.println("Going to puzzle 1"); game_progress = 1; break; // Going to puzzle 2 : case '2': //Serial.println("Going to puzzle 2"); game_progress = 2; break; // Going to puzzle 3 : case '3': //Serial.println("Going to puzzle 3"); game_progress = 3; break; // Going to puzzle 4 : case '4': //Serial.println("Going to puzzle 4"); game_progress = 4; break; // Opening solenoid 1 : case 'u': //Serial.println("Opening solenoid 1 for a few seconds"); digitalWrite(PIN_puzzle_1,HIGH); delay(TIMER_solenoids_open_application); digitalWrite(PIN_puzzle_1,LOW); break; // Opening solenoid 2 : case 'v': //Serial.println("Opening solenoid 2 for a few seconds"); digitalWrite(PIN_puzzle_2,HIGH); delay(TIMER_solenoids_open_application); digitalWrite(PIN_puzzle_2,LOW); break; // Controlling servo motor to open final door : case 'w': //Serial.println("Opening final door"); servo.write(servo_open); // setting servo motor so that door is locked : break; // Controlling servo motor to close final door : case 'c': //Serial.println("Closing final door"); servo.write(servo_locked); // setting servo motor so that door is locked : break; case 'm': t=0; delay(100); // delay necessary to let the phone application send the custom text message through the serial line. while (Serial.available()>0){ character_read = Serial.read(); if (character_read != '$'){ message_custom[t]= character_read; t=t+1; } // end if } // end while help_message(10); for (byte i=0;i