Notifications
Clear all

Percent Composition Puzzle

5 Posts
2 Users
1 Reactions
1,339 Views
(@chemicalarts)
Member
Joined: 4 years ago
Posts: 3
Topic starter  

I mentioned in my introduction post that I've made a puzzle that I plan on using for my chemistry classes. This puzzle requires the student to determine the empirical formula from the percent composition of each element. The puzzle is in the form of a box with a sliding door, a control panel, and an On/Off switch. Insider are an Arduino Nano, a buck converter (with 9V battery), an LCD screen, a momentary switch button, a rotary encoder, a servo motor, and a RC522 RFID reader. I designed the box in Fusion 360 and printed out the parts on my Prusa Mk3 printer.

2021 10 30 17 15 57

When the device is turned on, a default puzzle with an (relatively) easy solution is given. This on has a solution of C2H5NO.

2021 10 30 17 30 31

The rotary encoder (the adjust knob) is used to change the value of each element. Pushing the rotary encoder button toggles between each element. Once the student is confident that they have the answer, they use the test button to attempt to open the lock (a 9g servo motor). A correct answer gives them an affirmative message and a satisfying swish sound from the servo motor. They can then slide the door on top open and see what's inside.

2021 10 30 17 39 16

 The cool part is that you can update the puzzle with a new problem by scanning a programed RFID card, which changes the screen and the variables in memory for the correct answer. The programming of this took a while and was very educational for me. Here's a card with a much more challenging problem programmed into the RFID memory.

2021 10 30 17 46 27

 The data is easy to program onto the cards. There's even an Android app you can use to copy the data onto the card. Here's what the ASCII data on the card looks like.

Screenshot 20211030 174810 MIFARE Classic Tool

I'm pleased with how well it worked out and learned how to integrate several of the components thanks to DroneBot Workshop videos. I'm going to post the code for the Arduino (if anyone wants to see what I did) as a reply to this post because it is quite long.


   
jker reacted
Quote
(@chemicalarts)
Member
Joined: 4 years ago
Posts: 3
Topic starter  

Here's the Arduino code for those interested in such things. I'm not much of an expert at coding, but I posted lots of comment code to explain each library, variable, pin assignment, and instance.

// Libraries included
#include <LiquidCrystal_I2C.h> //Includes the I2C liquid crystal library
#include <string.h> // Includes the string library
#include <Servo.h> // Includes the servo library
#include <SPI.h> // Includes the Serial Peripheral Interface library
#include <MFRC522.h> // Includes the MFRC522 RFID library

// pin inputs and outputs
#define inputCLK 2 // Rotary encoder CLK input pin
#define inputDT 5 // Rotary encoder DT input pin
#define switchPin 3 // Rotary encoder SW (switch) input pin. The remaining pins on the rotary encoder are to ground and VCC.
#define testPin 4 // Test button pin. Other side is connected to ground.
#define servoPin 6 // Servo motor data pin (usually the third pin that isn't ground or VCC)
#define SS_Pin 10 // MFRC522 SPI SS (SDA) pin
#define RST_Pin 9 // MFRC522 SPI Reset (RST) pin
// The remaining pins on the MFRC522 are defined by the library: SPI MOSI D11, SPI MISO D12, SPI SCK D13 


int currentStateCLK; // initializes the variable for the current state of the rotary encoder
int previousStateCLK; // initializes the variable for the previous state of the rotary encoder
int element; // initializes the variable for the element (C=0, H=1, N=2, O=3). Activated in updateElement function using button of rotary encoder
int CarboNum; // initializes the variable for the user input number of carbons
int HydroNum; // initializes the variable for the user input number of hydrogens
int NitroNum; // initializes the variable for the user input number of nitrogens
int OxyNum; // initializes the variable for the user input number of oxygens
int CarboCount; // initializes the variable for the count sent from the rotary encoder for carbon
int HydroCount; // initializes the variable for the count sent from the rotary encoder for hydrogen
int NitroCount; // initializes the variable for the count sent from the rotary encoder for nitrogen
int OxyCount; // initializes the variable for the count sent from the rotary encoder for oxygen
int period = 1000; // initializes the variable for the wait period in milliseconds during the loop
String line0 = "C: 40.66% H: 8.55%"; // initializes default value for line 0 of LCD display
String line1 = "N: 23.71% O: 27.08%"; // initializes default value for line 1 of LCD display
String line2 = ""; // initializes default value for line 2 of LCD display
String line3 = ""; // initializes default value for line 3 of LCD display
int CHNOsub[5] = {2, 5, 1, 1}; // initializes variable array with correct subscripts for C, H, N, and O. Default values will be changed via RFID
unsigned long time_now = 0; // initializes current time to 0 upon startup or reset
unsigned long LastElemDebounceTime = 0; // initializes comparative value for debounce of updateElement subroutine
unsigned long debounceDelay = 500; // initializes the debounce time for updateElement subroutine. Increase if elements are getting skipped when rotary encoder button is pushed.


// functions other than setup and loop
void updateElement(); // function to update which element is adjusted using rotary encoder
void AdjustCounter(); // function to update counter of an element
void test(); // function to test the results currently in memory and on the LCD screen
void updatePuzzle(); // function to update puzzle with RFID card

// Create instances of hardware processes
LiquidCrystal_I2C lcd(0x27, 20, 4); //Instantiates the LiquidCrystal_I2C for address 0x27 and 20 chars, 4 lines
MFRC522 mfrc522(SS_Pin, RST_Pin);   // Instantiates MFRC522 with pin assignments
Servo myservo;  // Instantiates a Servo object named "myservo" to control a servo motor

void setup() {
  // put your setup code here, to run once:
  
  // Set encoder pins as inputs
  pinMode (inputCLK,INPUT);
  pinMode (inputDT,INPUT);
  pinMode (switchPin,INPUT_PULLUP);
  
  // Set Test button as input
  pinMode (testPin,INPUT_PULLUP); 
  
  // Setup Serial Monitor
  Serial.begin (9600);

  // Initialize SPI bus
  SPI.begin();

  // Initialize MFRC522 card
  mfrc522.PCD_Init();

  // Set up servo
  myservo.attach(servoPin);
  myservo.write(90);  

  // Setup LCD screen
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0,0);
  lcd.print(line0);
  lcd.setCursor(0,1);
  lcd.print(line1);

  // Read the initial state of inputCLK
  // Assign to previousStateCLK variable
  previousStateCLK = digitalRead(inputCLK);
  
  element = 0;
  CarboNum = 0;
  HydroNum = 0;
  NitroNum = 0;
  OxyNum = 0;
  CarboCount = 0;
  HydroCount = 0;
  NitroCount = 0;
  OxyCount = 0;

  attachInterrupt(digitalPinToInterrupt(switchPin), updateElement, CHANGE); // establishes rotary encoder switch pin as an interrupt
  attachInterrupt(digitalPinToInterrupt(inputCLK), AdjustCounter, CHANGE); // establishes rotary encoder dial as an interrupt

}

void loop() {
  // put your main code here, to run repeatedly:
  if(millis() >= time_now + period) {
    time_now += period;
    Serial.print("C:");
    Serial.print(CarboNum);
    Serial.print("  H:");
    Serial.print(HydroNum);
    Serial.print("  N:");
    Serial.print(NitroNum);
    Serial.print("  O:");
    Serial.println(OxyNum);

    String CNum = String(CarboNum);
    String HNum = String(HydroNum);
    String NNum = String(NitroNum);
    String ONum = String(OxyNum);
    String line2 = String("C: " + CNum + "  H: " + HNum + "   ");
    String line3 = String("N: " + NNum + "  O: " + ONum + "   ");
    lcd.setCursor(0,2);
    lcd.print(line2);
    lcd.setCursor(0,3);
    lcd.print(line3);
   }   

  CarboNum=CarboCount/2;
  HydroNum=HydroCount/2;
  NitroNum=NitroCount/2;
  OxyNum=OxyCount/2;

  test();
  updatePuzzle();
  }
  
void test() {
  int testVal = digitalRead(testPin);
  if (testVal == LOW) {
    if (CarboNum == CHNOsub[0] && HydroNum == CHNOsub[1] && NitroNum == CHNOsub[2] && OxyNum == CHNOsub[3]) {
      Serial.print("SUCCESS!!!");
      lcd.setCursor(0,2);
      lcd.print("Answer is correct!");
      lcd.setCursor(0,3);
      lcd.print("                    ");
      myservo.write(0);
      delay(10000);
      lcd.setCursor(0,2);
      lcd.print("                    ");
      lcd.setCursor(0,3);
      lcd.print("                    ");
      
    } else {
      Serial.print("FAILURE");
      lcd.setCursor(0,2);
      lcd.print("Answer is incorrect!");
      lcd.setCursor(0,3);
      lcd.print("Please try again.");
      myservo.write(90);
      delay(2000);
      lcd.setCursor(0,2);
      lcd.print("                    ");
      lcd.setCursor(0,3);
      lcd.print("                    ");
      
    }
  }
}

void updateElement() {
  int sensorVal = digitalRead(switchPin);
  if (sensorVal == LOW) {
    if ((millis() - LastElemDebounceTime) > debounceDelay) {
      element++;
      LastElemDebounceTime = millis();
      if (element >3) {
        element = 0;
      }
    }
  }
}

void AdjustCounter() {
   
  // Read the current state of inputCLK
  currentStateCLK = digitalRead(inputCLK);
  

  // If the previous and the current state of the inputCLK are different then a pulse has occured
  if (currentStateCLK != previousStateCLK) {

    // Nested loop ** If the inputDT state is different than the inputCLK state then the encoder is rotating counterclockwise
    if (digitalRead(inputDT) != currentStateCLK) {
      switch(element){
        case 0:
          CarboCount--;
          if (CarboCount<0) {
          CarboCount=0;
          } 
          break;
        case 1:
          HydroCount--;
          if (HydroCount<0) {
          HydroCount=0;
          }
          break;
        case 2:
          NitroCount--;
          if (NitroCount<0) {
          NitroCount=0;
          } 
          break;
        case 3:
          OxyCount--;
          if (OxyCount<0) {
          OxyCount=0;
          } 
          break;

      }
      
    } else {
      // Otherwise the encoder must be rotating clockwise
      switch(element){
        case 0:
          CarboCount++;
          if (CarboCount>199) {
          CarboCount=199;
          }
          break;
        case 1:
          HydroCount++;
          if (HydroCount>199) {
          HydroCount=199;
          }
          break;
        case 2:
          NitroCount++;
          if (NitroCount>199) {
          NitroCount=199;
          }
          break;
        case 3:
          OxyCount++;
          if (OxyCount>199) {
          OxyCount=199;
          }
          break;

      }
    }
   
  }
  
  // Update previousStateCLK with the current state
  previousStateCLK = currentStateCLK;
}

void updatePuzzle(){
  // Prepare key - all keys are set to FFFFFFFFFFFFh at chip delivery from the factory.
  MFRC522::MIFARE_Key key;
  for (byte i = 0; i < 6; i++) key.keyByte[i] = 0xFF;

  //some variables we need
  byte block;
  byte len;
  MFRC522::StatusCode status;

  //-------------------------------------------

  // Reset the loop if no new card present on the sensor/reader. This saves the entire process when idle.
  if ( ! mfrc522.PICC_IsNewCardPresent()) {
    return;
  }

  // Select one of the cards
  if ( ! mfrc522.PICC_ReadCardSerial()) {
    return;
  }

  Serial.println(F("**Card Detected:**"));

  //-------------------------------------------

  mfrc522.PICC_DumpDetailsToSerial(&(mfrc522.uid)); //dump some details about the card

  //mfrc522.PICC_DumpToSerial(&(mfrc522.uid));      //uncomment this to see all blocks in hex

  //-------------------------------------------

  byte carbonPer[18];
  line0 = "";
  line1 = "";

  block = 1;
  len = 18;

  //------------------------------------------- GET Carbon Percentage
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 1, &key, &(mfrc522.uid)); //line 834 of MFRC522.cpp file
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, carbonPer, &len);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  //PRINT Carbon Percentage
  
  Serial.print(F("Carbon: "));
  
  for (uint8_t i = 0; i < 10; i++)
  {
    Serial.write(carbonPer[i]);
    line0 += (char)carbonPer[i];
  }
  Serial.println(" ");


//  lcd.printstr(carbonPer);
  

  //---------------------------------------- GET Hydrogen Percentage

  byte hydrogenPer[18];
  block = 2;

  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 2, &key, &(mfrc522.uid)); //line 834
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, hydrogenPer, &len);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  //PRINT Hydrogen Percentage
  
  Serial.print(F("Hydrogen: "));
  
  for (uint8_t i = 0; i < 10; i++ ) {
    Serial.write(hydrogenPer[i] );
    line0 += (char)hydrogenPer[i];
  }
  Serial.println(" ");

  
  lcd.setCursor(0,0);
  lcd.print(line0);

  //---------------------------------------- GET Nitrogen Percentage

  byte nitrogenPer[18];
  block = 4;

  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 4, &key, &(mfrc522.uid)); //line 834
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, nitrogenPer, &len);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  //PRINT Nitrogen Percentage
  
  Serial.print(F("Nitrogen: "));
  
  for (uint8_t i = 0; i < 10; i++) {
    Serial.write(nitrogenPer[i] );
    line1 += (char)nitrogenPer[i];
  }
  Serial.println(" ");

  //---------------------------------------- GET Oxygen Percentage

  byte oxyPer[18];
  block = 5;

  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 5, &key, &(mfrc522.uid)); //line 834
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, oxyPer, &len);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  //PRINT Oxygen Percentage
  
  Serial.print(F("Oxygen: "));
  
  for (uint8_t i = 0; i < 10; i++) {
    Serial.write(oxyPer[i] );
    line1 += (char)oxyPer[i];

  }
  Serial.println(" ");

  lcd.setCursor(0,1);
  lcd.print(line1);
  
  //----------------------------------------

  byte CSub[18];
  String CSubAsc = "";
 
  block = 8;
  len = 18;

  //------------------------------------------- GET Carbon Subscript
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 8, &key, &(mfrc522.uid)); //line 834 of MFRC522.cpp file
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, CSub, &len);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }
    
  //PRINT Carbon Number
  
  Serial.print(F("C: "));
  
  for (uint8_t i = 0; i < 2; i++)
  {
    Serial.write(CSub[i]);
    CSubAsc += (char)CSub[i];
  }
  Serial.println(" ");

  CHNOsub[0] = CSubAsc.toInt();
  Serial.println(CSubAsc);
  Serial.println(CHNOsub[0]);

 
  //----------------------------------------

  byte HSub[18];
  String HSubAsc = "";
 
  block = 9;
  len = 18;

  //------------------------------------------- GET Hydrogen Subscript
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 9, &key, &(mfrc522.uid)); //line 834 of MFRC522.cpp file
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, HSub, &len);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }
    
  //PRINT Hydrogen Number
  
  Serial.print(F("H: "));
  
  for (uint8_t i = 0; i < 2; i++)
  {
    Serial.write(HSub[i]);
    HSubAsc += (char)HSub[i];
  }
  Serial.println(" ");

  CHNOsub[1] = HSubAsc.toInt();
  Serial.println(HSubAsc);
  Serial.println(CHNOsub[1]);

  //----------------------------------------

  byte NSub[18];
  String NSubAsc = "";
 
  block = 10;
  len = 18;

  //------------------------------------------- GET Nitrogen Subscript
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 10, &key, &(mfrc522.uid)); //line 834 of MFRC522.cpp file
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, NSub, &len);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }
    
  //PRINT Nitrogen Number
  
  Serial.print(F("N: "));
  
  for (uint8_t i = 0; i < 2; i++)
  {
    Serial.write(NSub[i]);
    NSubAsc += (char)NSub[i];
  }
  Serial.println(" ");

  CHNOsub[2] = NSubAsc.toInt();
  Serial.println(NSubAsc);
  Serial.println(CHNOsub[2]);

  //----------------------------------------

  byte OSub[18];
  String OSubAsc = "";
 
  block = 12;
  len = 18;

  //------------------------------------------- GET Oxygen Subscript
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 12, &key, &(mfrc522.uid)); //line 834 of MFRC522.cpp file
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, OSub, &len);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }
    
  //PRINT Oxygen Number
  
  Serial.print(F("O: "));
  
  for (uint8_t i = 0; i < 2; i++)
  {
    Serial.write(OSub[i]);
    OSubAsc += (char)OSub[i];
  }
  Serial.println(" ");

  CHNOsub[3] = OSubAsc.toInt();
  Serial.println(OSubAsc);
  Serial.println(CHNOsub[3]);


  Serial.println(F("\n**End Reading**\n"));


  delay(1000); //change value if you want to read cards faster

  mfrc522.PICC_HaltA();
  mfrc522.PCD_StopCrypto1();
}
//*****************************************************************************************//

   
ReplyQuote
Will
 Will
(@will)
Member
Joined: 4 years ago
Posts: 2603
 
Posted by: @chemicalarts

 

2021 10 30 17 39 16

 The cool part is that you can update the puzzle with a new problem by scanning a programed RFID card, which changes the screen and the variables in memory for the correct answer. The programming of this took a while and was very educational for me. Here's a card with a much more challenging problem programmed into the RFID memory.

2021 10 30 17 46 27

 The data is easy to program onto the cards. There's even an Android app you can use to copy the data onto the card. Here's what the ASCII data on the card looks like.

I was wondering why you didn't make up an SD card with all of the data and eliminate the physical RFID cards. You could then just use a random number to select the next problem and keep working.

Was it because you wanted to phase in harder problems as the student went on ?

Anything seems possible when you don't know what you're talking about.


   
ReplyQuote
(@chemicalarts)
Member
Joined: 4 years ago
Posts: 3
Topic starter  

@will The simple answer is that I had bought a pack of RFID readers and was looking for something to do with them. This was as much a self-learning exercise as it was a project for developing teaching tools.


   
ReplyQuote
Will
 Will
(@will)
Member
Joined: 4 years ago
Posts: 2603
 
Posted by: @chemicalarts

@will The simple answer is that I had bought a pack of RFID readers and was looking for something to do with them.

LOL, I understand completely 🙂

This was as much a self-learning exercise as it was a project for developing teaching tools.

Well it turned out great. It's a really good mix of 3D design and printing, the Arduino and a nice assortment of useful components well integrated into a practical and operational end product. And it looks good too.

Well done !

Anything seems possible when you don't know what you're talking about.


   
ReplyQuote