top of page

Custom RFID laundry scanner

Writer: Jon PeroutkaJon Peroutka

Updated: Jan 18




Problem / Opportunity


Laundry is not my favorite activity. Fortunately for me, my amazing girlfriend has been gracious enough to take on the chore of our laundry (thanks hun!). While this has been super helpful, we ran into a bit of an issue. Many of my clothing articles don’t go into the dryer in order to avoid the items shrinking and no longer fitting. But by combining our laundry together and washing them together, some items which needed to be pulled out of the load before going into the dryer were missed amongst the other items which were meant to go into the dryer. These missed items went through the dryer cycle and ended up shrinking - and I couldn’t wear them anymore.


The simple solution would be to sort the laundry beforehand into the items which were meant for the dryer and items which were not. However, even with this process, there were still some items missed if I wasn’t careful about inspecting the laundry. So, I wanted a solution which would be able to scan a load of laundry which had both categories of items in it (dryer-friendly as well as dryer-unfriendly) and be able to tell the user if there were any dryer-unfriendly items in the laundry load, how many items, and what they looked like so that the user could find them more easily. Then the user/laundry washer could pull those items out of the load and ensure nothing inadvertently goes into the dryer.



Research


After looking on the internet for solutions which could fit my need, the closest ones I could find were commercial-grade RFID-based inventory management scanners. These are scanners which are meant for business such as hotels who wish to track their linen inventory. The idea is that each linen would have an RFID tag sewn into it, and the tag can be scanned to identify if it had been washed and with which products, as well as identify any theft which has taken place on their linens.

This was exactly the solution I needed. However, these devices were priced much higher than I thought was reasonable for my use case, around $1000 or more - yikes! I couldn’t find anything close to the price range which I wanted, so I decided to build my own.



Solutioning


I learned that I had to use an ultra high RFID protocol due to the distance which would be required for sensing RFID tags (1-3 feet). During my research, I came across an arduino-based, ultra high-frequency RFID reader shield (below). I kept this in my back pocket, and I eventually concluded that this was going to be my best bet for my solution.

I started by creating a rough “logical” sketch of what I thought the solution could look like, to gauge what I would need to buy, how much it would cost (knowing that there would likely be some variance once I started in the project), and what the 3D printer parts might look like. (did I mention this was rough?)



Parts and Prices:

  • UHF RFID Shield - $240

  • Arduino Uno - $20

  • Battery - $10

  • Screen - $15

  • Button(s) - $2

  • External Antenna - ~$30

  • 3D Printed Parts - ~$10

  • Miscellaneous Wires, screws, etc - ~$10

  • Laundry-friendly RFID tags - $1.25 per tag


Total cost would be around $330 (at least $600 less than purchasing an existing product), not including the cost of the tags.


Now, this was the first arduino project I had ever thought about building. I had never coded in C++ before, had no knowledge of the arduino ecosystem, and had extremely limited electronics engineering and 3D modeling experience. [sidenote: my dad is an electronics engineer, so I told myself that I would reach out to him if I encountered any blockers on that front. He was definitely a help for this. Thanks, Dad!]


I took some time to do some research on what arduino is, how to get started, how components get linked to each other, etc. I watched youtube videos, searched google, read articles, and finally read various forum threads on C++, existing arduino libraries I could use, and electronics engineering concepts. Eventually I got the feeling that I may be able to make this work, so I took the plunge and purchased all the components I needed.



Build


Once I received the parts, I started by confirming that the RFID shield connected to the Uno was working for me. I followed the sparkfun.com’s hookup guide instructions closely. I was able to confirm that the RFID reader was successfully reading the tags with the uno connected to the computer via the Universal Reader Assistant software found in the guide.


With that confirmed, I went on to confirm that I could read the RFID tags via the arduino serial monitor in the arduino IDE, utilizing some of the available libraries called out in the guide.





With that successfully built, I moved next to building the ability to display the RFID tag’s EPC (the tag’s unique ID) in the display.




With the screen working, I had the primary functionality successfully built. The next big hurdle to undertake was the 3D modeling. This was easily my most ambitious 3D modeling challenge to date. I broke up the whole design into 5 major components: the main bay which holds the uno & RFID shield, the screen compartment, the compartment for the battery, the lid to cover the battery, and the handle.


Below is the main bay I designed (which was the most complex part):










Next, was assembly and wiring.





Along the way, I wired up an on/off button, clear button (to clear the screen of scanned tags at the user's discretion), and the usb battery bank. I added an external antenna as it added considerable range and ensured the best detection rate. I also added a temperature/humidity sensor as I was concerned that heat and moisture from using the device inside a warm dryer could cause damage.


The final code I built is below. Note, I can almost guarantee that the code has flaws and does not follow best coding principles. This was my first real coding project (let alone my first C++ project), and I haven't gone back to refactor it. So, take the code as it is. It worked for my use case, but it is by no means perfect.


#include "SparkFun_Si7021_Breakout_Library.h"
#include <Wire.h>
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library for ST7735
#include <Adafruit_ST7789.h> // Hardware-specific library for ST7789
#include <SPI.h>

#include <SoftwareSerial.h> //Used for transmitting to the device
using namespace std;

SoftwareSerial softSerial(2, 3); //RX, TX

#include "SparkFun_UHF_RFID_Reader.h" //Library for controlling the M6E Nano module
RFID nano; //Create instance

#define BUZZER1 9
//#define BUZZER1 0 //For testing quietly
#define BUZZER2 10

#if defined(ARDUINO_FEATHER_ESP32) // Feather Huzzah32
  #define TFT_CS         14
  #define TFT_RST        15
  #define TFT_DC         32

#elif defined(ESP8266)
  #define TFT_CS         4
  #define TFT_RST        16                                            
  #define TFT_DC         5

#else
  // For the breakout board, you can use any 2 or 3 pins.
  // These pins will also work for the 1.8" TFT shield.
  #define TFT_CS        10
  #define TFT_RST        9 // Or set to -1 and connect to Arduino RESET pin
  #define TFT_DC         8
#endif

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

const int buttonPin = 4;     // the number of the pushbutton pin
const int ledPin =  12;      // the number of the LED pin

float humidity = 0;
float tempf = 0;
String temp;
int power = A3; //set pwer for temp sensor
int GND = A2; //set ground for temp sensor

//Create Instance of HTU21D or SI7021 temp and humidity sensor and MPL3115A2 barrometric sensor
Weather sensor;
    
void setup(void)
{  
    // initialize the LED pin as an output:
    pinMode(ledPin, OUTPUT);
    // initialize the pushbutton pin as an input:
    pinMode(buttonPin, INPUT);
    
    Serial.begin(115200);
  
    pinMode(BUZZER1, OUTPUT);
    pinMode(BUZZER2, OUTPUT);

    pinMode(power, OUTPUT); //for temp sensor
    pinMode(GND, OUTPUT); //for temp sensor

    digitalWrite(power, HIGH);
    digitalWrite(GND, LOW);
    sensor.begin(); //start I2C interface for temp sensor
  
    digitalWrite(BUZZER2, LOW); //Pull half the buzzer to ground and drive the other half.
  
    while (!Serial);
    Serial.println();
    Serial.println("Welcome! Starting RFID Reader");
    Serial.println("Initializing RFID Module...");
  
    if (setupNano(38400) == false) //Configure nano to run at 38400bps
    {
      Serial.println("Module failed to respond. Please check wiring.");
      while (1); //Freeze!
    }
  
    nano.setRegion(REGION_NORTHAMERICA); //Set to North America
  
    nano.setReadPower(2200); //5.00 dBm. Higher values may cause USB port to brown out
    //Max Read TX Power is 27.00 dBm and may cause temperature-limit throttling
   
    nano.startReading(); //Begin scanning for tags
    Serial.println("Initialized RFID Module");
  
    //Initialize screen
    Serial.println("Initializing screen...");
    tft.initR(INITR_144GREENTAB);
    delay(1000);
    tft.fillScreen(ST77XX_BLACK);
    Serial.println(F("Screen Initialized."));
    testdrawtext("Screen Initialized", ST77XX_WHITE, 0, 0);

}


void loop()
{

  char listArray[15][12] {
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
      {00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00},
  };
  char testEPC[12];
  char readEPC[12];
  boolean test = false;
  int buttonState = 0;         // variable for reading the pushbutton status

  delay(500);
  tft.fillScreen(ST77XX_BLACK);
  getWeather();
  temp = ("T:"+String(tempf)+"F, "+"H:"+String(humidity)+"%");
  testdrawstring(temp, ST77XX_GREEN, 0, 0);
  checkWeather();
  testdrawtext("Please scan test tag", ST77XX_WHITE, 0, 8);

  while(test == false){
    if (nano.check() == true) //Check to see if any new data has come in from module
    {
      byte responseType = nano.parseResponse(); //Break response into tag ID, RSSI, frequency, and timestamp
  
      if (responseType == RESPONSE_IS_KEEPALIVE)
      {
        Serial.println(F("Looking for test tag..."));
      }
      else if (responseType == RESPONSE_IS_TAGFOUND)
      {
        byte tagEPCBytes = nano.getTagEPCBytes();
        
        //Read the EPC data from the tag and store it in readEPC
        for (byte x = 0 ; x < tagEPCBytes ; x++)
        {
          testEPC[x] = static_cast<char>(nano.msg[31 + x]);
        }
        Serial.print(F("Test EPC Read is: "));
        for (byte y = 0; y < sizeof(testEPC); y++){
          Serial.print(testEPC[y]);
        }

        //if it found the test tag, turn boolean 'test' to true; note that the numbers correspond to the ascii "test        " https://www.rapidtables.com/convert/number/hex-to-ascii.html
        if (testEPC[0] == 116 && 
            testEPC[1] == 101 &&
            testEPC[2] == 115 &&
            testEPC[3] == 116 &&
            testEPC[4] == 32 &&
            testEPC[5] == 32 &&
            testEPC[6] == 32 &&
            testEPC[7] == 32 &&
            testEPC[8] == 32 &&
            testEPC[9] == 32 &&
            testEPC[10] == 32 &&
            testEPC[11] == 32){
            Serial.println(F("Test passed."));
            tone(BUZZER1, 2349, 150); //D
            delay(150);
            tone(BUZZER1, 2637, 150); //E
            delay(150);
            test=true;
            }
      }
    }
  }
  
  tft.fillScreen(ST77XX_BLACK);
  getWeather();
  temp = ("T:"+String(tempf)+"F, "+"H:"+String(humidity)+"%");
  testdrawstring(temp, ST77XX_GREEN, 0, 0);
  checkWeather();
  testdrawtext("Ok! Scan laundry..", ST77XX_WHITE, 0, 8);

  while(true){
      checkWeather();
      buttonState = digitalRead(buttonPin);
      if (buttonState == HIGH) {
          // turn LED on:
          digitalWrite(ledPin, HIGH);
          tft.fillScreen(ST77XX_BLACK);
          testdrawtext("Clearing...", ST77XX_WHITE, 0, 0);
          for (byte x = 0 ; x < 15 ; x++)
          {
              for (byte y = 0 ; y < 12 ; y++)
                listArray[x][y] = 00;
          }
          delay(800);
          tft.fillScreen(ST77XX_BLACK);
          getWeather();
          temp = ("T:"+String(tempf)+"F, "+"H:"+String(humidity)+"%");
          testdrawstring(temp, ST77XX_GREEN, 0, 0);
          checkWeather();
          testdrawtext("Continue Scanning...", ST77XX_WHITE, 0, 8);
          digitalWrite(ledPin, LOW);
      } 
      else {
          if (nano.check() == true) //Check to see if any new data has come in from module
          {
          byte responseType = nano.parseResponse(); //Break response into tag ID, RSSI, frequency, and timestamp
      
          if (responseType == RESPONSE_IS_KEEPALIVE)
          {
            Serial.println(F("Scanning..."));
          }
          else if (responseType == RESPONSE_IS_TAGFOUND) 
          {
            byte tagEPCBytes = nano.getTagEPCBytes();
            
            //Read the EPC data from the tag and store it in readEPC
            for (byte x = 0 ; x < tagEPCBytes ; x++)
            {
              readEPC[x] = static_cast<char>(nano.msg[31 + x]);
            }
            Serial.print(F("EPC Read is: "));
            for (byte y = 0; y < tagEPCBytes; y++){
              Serial.print(readEPC[y]);
            }
      
            int match = 0;
        
            //looks to see if the EPC code already exists in the list before trying to add it in a new slot
            for (int x = 0; x < 10; x++){
                //if it found the EPC in the array already, increment match to prevent it being added to the array
                if (listArray[x][0] == readEPC[0] && 
                    listArray[x][1] == readEPC[1] &&
                    listArray[x][2] == readEPC[2] &&
                    listArray[x][3] == readEPC[3] &&
                    listArray[x][4] == readEPC[4] &&
                    listArray[x][5] == readEPC[5] &&
                    listArray[x][6] == readEPC[6] &&
                    listArray[x][7] == readEPC[7] &&
                    listArray[x][8] == readEPC[8] &&
                    listArray[x][9] == readEPC[9] &&
                    listArray[x][10] == readEPC[10] &&
                    listArray[x][11] == readEPC[11]){
                      match++;
                }
            }
    
            //if it found the test tag, increment match to prevent it being added to the array 
            if (readEPC[0] == 116 && 
                readEPC[1] == 101 &&
                readEPC[2] == 115 &&
                readEPC[3] == 116 &&
                readEPC[4] == 32 &&
                readEPC[5] == 32 &&
                readEPC[6] == 32 &&
                readEPC[7] == 32 &&
                readEPC[8] == 32 &&
                readEPC[9] == 32 &&
                readEPC[10] == 32 &&
                readEPC[11] == 32){
                  match++;
              }
    
            
            //if the EPC isn't already in the array, add it first, then count it, and then display it
            if (match == 0){
                 //read each array in listArray to put an EPC in
                for (int x = 0; x < 15; x++){
                    //look to see if there is an empty array entry to put the EPC code in, else it skips it
                    if (listArray[x][0] == 00){
                        for (int i=0; i<sizeof(readEPC); i++)
                        {
                          listArray[x][i] = readEPC[i];
                        }
                        break;
                    }
                }


                //count how many EPCs exist
                int count = 0;
                String countString = "";
                for (int x = 0; x < 15; x++){
                    //look to see if there is content in that array
                    if (listArray[x][0] != 00){
                        count++;
                    }
                }
                countString = "Tags Found: "+static_cast<String>(count)+"\n"; 

                //clear screen
                tft.fillScreen(ST77XX_BLACK);
                
                //get temps
                getWeather();
                temp = ("T:"+String(tempf)+"F, "+"H:"+String(humidity)+"%");
                testdrawstring(temp, ST77XX_GREEN, 0, 0);
                checkWeather();
                
                //iterate through the 2-d array to see which ones aren't empty and display them   
                for (int z = 0; z < 15; z++) {
                  if (listArray[z][0] != 00){
                    String myEPCString = "";
                    for (int zz = 0; zz < sizeof(readEPC); zz++){
                       myEPCString += static_cast<char>(listArray[z][zz]); //this converts the char to a string and adds it to the string variable              
                    }
                    myEPCString += "\n";
                    countString += myEPCString; //adds the count line to the EPC lines and then prints everything
                    testdrawstring(countString, ST77XX_WHITE, 0, 8);
                  }
                }
                //delay(500);
                tone(BUZZER1, 2093, 150); //C
                delay(150);
                tone(BUZZER1, 2349, 150); //D
                delay(150);
                tone(BUZZER1, 2637, 150); //E
                delay(150);
                
            }
    
    
    
            for (int x = 0; x < 15; x++){
              Serial.print("EPC Array :");
              for (byte y = 0; y < 12; y++){
                Serial.print(listArray[x][y]);
              }
              Serial.print("\n");
            }
        }
        else if (responseType == ERROR_CORRUPT_RESPONSE)
        {
          Serial.println("Bad CRC");
        }
        else
        {
          //Unknown response
          Serial.print("Unknown error");
        }
      }
    }
  }
}

//Gracefully handles a reader that is already configured and already reading continuously
//Because Stream does not have a .begin() we have to do this outside the library
boolean setupNano(long baudRate)
{
  nano.begin(softSerial); //Tell the library to communicate over software serial port

  //Test to see if we are already connected to a module
  //This would be the case if the Arduino has been reprogrammed and the module has stayed powered
  softSerial.begin(baudRate); //For this test, assume module is already at our desired baud rate
  while(!softSerial); //Wait for port to open

  //About 200ms from power on the module will send its firmware version at 115200. We need to ignore this.
  while(softSerial.available()) softSerial.read();
  
  nano.getVersion();

  if (nano.msg[0] == ERROR_WRONG_OPCODE_RESPONSE)
  {
    //This happens if the baud rate is correct but the module is doing a ccontinuous read
    nano.stopReading();

    Serial.println(F("Module continuously reading. Asking it to stop..."));

    delay(1500);
  }
  else
  {
    //The module did not respond so assume it's just been powered on and communicating at 115200bps
    softSerial.begin(115200); //Start software serial at 115200

    nano.setBaud(baudRate); //Tell the module to go to the chosen baud rate. Ignore the response msg

    softSerial.begin(baudRate); //Start the software serial port, this time at user's chosen baud rate
  }

  //Test the connection
  nano.getVersion();
  if (nano.msg[0] != ALL_GOOD) return (false); //Something is not right

  //The M6E has these settings no matter what
  nano.setTagProtocol(); //Set protocol to GEN2

  nano.setAntennaPort(); //Set TX/RX antenna ports to 1

  return (true); //We are ready to rock
}

void testdrawtext(char *text, uint16_t color, int cur1, int cur2) {
  tft.setCursor(cur1, cur2);
  tft.setTextColor(color);
  tft.setTextWrap(true);
  tft.print(text);
}

void testdrawstring(String text, uint16_t color, int cur1, int cur2) {
  tft.setCursor(cur1, cur2);
  tft.setTextColor(color);
  tft.setTextWrap(true);
  tft.print(text);
}


void getWeather()
{
  // Measure Relative Humidity from the HTU21D or Si7021
  humidity = sensor.getRH();

  // Measure Temperature from the HTU21D or Si7021
  tempf = sensor.getTempF();
  // Temperature is measured every time RH is requested.
  // It is faster, therefore, to read it from previous RH
  // measurement with getTemp() instead with readTemp()
}

void checkWeather() {
  while((tempf >= 95) || (humidity >= 65)){
      tone(BUZZER1, 2637, 150); //E
      temp = ("T:"+String(tempf)+"F, "+"H:"+String(humidity)+"%");
      tft.fillScreen(ST77XX_BLACK);
      testdrawstring(temp, ST77XX_GREEN, 0, 0); 
      testdrawtext("SCANNER TOO HOT OR HUMID!", ST77XX_RED, 0, 8);
      getWeather();
  }
}



Final Product


At last, the final product!











Below is an example of an RFID tag affixed to one of my hoodies. It's a specific type of RFID tag intended for linens and has a soft cloth-like touch to it. With it affixed to the main tag on my clothes, it's soft enough to not be uncomfortable on my skin.





The solution has proven very helpful and eliminated all items from inadvertently going through the dryer cycle. It's been a great success and a fantastic learning experience. I very much enjoyed the project and had a great sense of accomplishment from it.



 
 
 

Comments


bottom of page