Overview

This device helps the user develop habits by allowing the user to track the strength of habits via RFID tags. Each time the user performs an activity, they can scan the associated RFID tag/key card on the device as a tally of how many times they’ve performed the activity. When this happens a certain amount of time, the activity is considered a developed habit.

Front view of the device with two types of compatible RFID-embedded objects.

Usage Demo

Details

A fully “developed” habit will trigger the light ring embedded within the dial to display a rainbow.

Front view of device showing the size of the dial and scanning surface

The interface of the device is a cylindrical dial with an embedded button and LED ring, as well as a RFID reader underneath the flat surface.

Process

The first decision point  came from an aesthetic standpoint and occurred early on in the process. My original idea was using a potentiometer to adjust the strength of a habit, and have an LED strip near the RFID scanner to display the strength.

Original proof of concept with a potentiometer, linear Neopixel strip, and separate button.

However, I decided to move to a ring and encoder setup because the LED also needs to be responsive to dial adjustments. By using a ring, the motion of the rotation is coupled with the “motion” of the LED lights. Furthermore, using an encoder allows the user to turn the dial freely, but figuring out what the rotations are translated to is relegated to software.

Though this appeared to be a minor change, the encoder required major restructure of the main program loop. Originally, I wanted to check if a RFID tag is detected by the scanner every cycle. But the time this takes interferes with the encoder’s ability to detect rotation. This led me to introduce a “time out” system for setting a new tag’s strength. Pushing down the encoder while rotating will set the strength, and after releasing the encoder, the user has three seconds to flash a new tag. Otherwise, the nano goes back to waiting to read a tag.

Converting to Neopixel ring and rotary encoder with built-in button.

One feature I wanted to add was making time a factor in the calculation of how strong a strength is. However, I decided against implementing it. Because of the long-running nature of this device, I would need to incorporate timing in the form of an extra component to connect to the arduino. Furthermore, this information would need to be encoded onto the RFID tags. Though this would allow me to differentiate habits I infrequently follow from the ones that I do abide by daily, the amount of technical complications was too much for the time frame of this project.

Instead, after implementing the functions I initially planned on, I decided to focus on designing the appearance of the final product.

Process Highlights

Moving components to a protoboard to slim down the form factor.

Initial sketch of the enclosure

Modeled enclosure with built-in supports for the components.

Printed enclosure with placeholder acrylic dial.

Final enclosure with printed lid and dial.

Discussion

Responses

“A time interval could make it even better.”

The most common points raised during the critique was a lack of a time element. The “strength” of a habit should account for the frequency in which an activity is performed, but my device currently only tracks the amount of times it has been scanned. This is definitely a feature I would like to incorporate, including a way to set the target frequency (e.g. X times/ week). One concern for adding this feature is that it might require a more complicated interface. The consensus seems to be that my current press-and-turn interface is “simple and intuitive.” The simplicity may come from the fact that there is only one thing to control right now: the maximum number of times the tag is scanned to fill up the light ring. If there are two degrees of freedom involved, I would need to think of a way for one dial to set two parameters.

Another way I could maintain the simplicity of the interface is to set a constant number/duration for which a tag must be scanned. However, the dial will then only set the desired frequency, using this and the actual frequency of scanning to calculate “strength”.

“I wonder if there could be the opposite, where it punishes you for having bad habits.”

If this time feature is implemented, I could introduce a decay factor into my habit. This way, the strength of the habit could decrease if the scanning becomes too infrequent. Although this is not necessarily a real punishment, it does mean that I will be further from getting to see the rainbow lights spin around.

Another question raised during the critique was how I will remember to scan the tags related to an activity when I am not immediately next to my desk, which is where this device would be. An example I provided is putting a tag on my gym bag, so when I come home from the gym I will naturally scan it. However, not every activity may have a physical object associated with it, which would make remembering more difficult. The hope would then be that seeing this tracker on my desk can also serve as a reminder for me to perform tasks as well as remember to scan tags associated with them. In that sense, remembering to use this device is also a habit I would then need to develop.

Self Critique and Takeaways

Looking back, I think that this project was quite ambitious given that it was an individual project over the course of two weeks. I am happy with the way that my project turned out, but I also think I could have been more ambitious. I decided to focus on the physical design of my project over the software because I wanted to use this assignment as a way to explore that field. Because of this, I settled with a conceptually simple (but still challenging to implement) software side.

One of the potential issues on the software side I decided to overlook was the sequential nature of the LED animations. Because each animation routine was a separate function that used delay() for timing, no RFID tags will be scanned when an animation is being played. This also means that tags must be scanned one at a time, i.e. I would not be able to bulk scan a lot of tags (e.g. at the end of the say when I get home). However, an advantage of this is that I would be able to see the status of each tag I scan, which would can serve as a way for me to check in with myself about a habit and give enough time for all of them.

I wanted to learn how to design and 3D print for this project, and I was able to accomplish that goal. Translating something from my mind to the whiteboard, then to a modeling software, and finally materialized in physical space was a very exciting process for me, and I learned to consider a lot of different factors in my design, especially the space required to fit components. For example, the wiring for the neopixel needed to be routed through the dial to the protoboard, so I needed to make sure that there was enough clearance between the encoder stand and the protoboard stand to route these wires. While designing the internal components, I also had a general idea of how I wanted the external appearance would turn out, and this also helped the final design come together.

Nonetheless, there were also some issues with my 3D printed enclosure, which is understandable for my first time designing. The stands for my protoboard were too small, so I needed to sand down the edges of the board for it to fit. On the other hand, the stand for the encoder was too wide for the component to sit snugly in it. I used tape make sure it stayed in its place during operation. Moreover, I made the dial a bit too flimsy because the sides of it were too thin, and this made turning the dial feel less stable than anticipated. Another detail I overlooked was the gap between the dial and the body of the enclosure. The gap is inevitable, as there needs to be room for the dial to be pressed. However, this exposed the internals originally, and I improvised by wrapping the central LED ring support with white tape.

I learn a lot about my own process for designing, which involved quite a bit of improvising. After all, this is only the first iteration. I wanted to make sure that my internal components worked before I began to think too much about the final “look” of the product. I seem to allow the functionalities and components to dictate the form, instead of the other way around. I also designed the base before I designed the dial and top cover because I needed a tangible product to test fit the internals. Some of the considerations about the base only arose when the dial and lid needed to be designed, but it was too late. To summarize, I think I take on a very impatient but incremental approach to design, which caused some unfortunately silly mistakes.

What’s Next?

In the next iteration, I would address some of the issues with the external enclosure (i.e. make the dial sturdier, adjust the supports for components). Since this device would be sitting on my desk, I could afford to make it larger to fit in a real time clock module, which would allow me to implement the time-based features discussed above.

Another potential addition could be a way to report the development of a habit over time, like a line chart. This could be done via IoT method (most likely an AWS or Google Cloud micro service), so I could have a web dashboard that keeps track of my habit development. Another more analog way could be using a dot matrix printer. It could be a slow process where, after each day, a single line is printed, with each dot corresponding to a habit. There are a lot of directions this project could take, and I am excited to see what direction I end up choosing.

Technical Information

Schematic

Code

/*
   RFID Habit Tracker
   This is the code for the Arduino Nano Rev3 board within the habit tracker.
   Each time an RFID chip is scanned by the MFRC522 unit,
   a byte stored within the chip (buffer[0]) is incremented
   in its memory block (buffer) to represent the number of times a habit is reinforced.

   The maximum limit to increment up until is set by turning the encoder while
   pressing it down, and "flashing" a new tag with buffer[1] set as the
   max count.

   The status of a habit, when incremented, is displayed on the Neopixel ring with an animation.
   When turning the rotary encoder, the strength is also represented on the ring.
   When a tag has been scanned its "maximum" times, the ring fills up and rotates.

   The Adafruit Neopixel API was consulted to implement the LED animations.
   (https://learn.adafruit.com/adafruit-neopixel-uberguide/arduino-library-use)

   The Rotary encoder code is based on the example found here:
   (https://github.com/PaulStoffregen/Encoder)

   The MFRC522 scanner uses the library in the link below.
   (https://github.com/miguelbalboa/rfid)

   MFRC522 Wiring
   -------------------------------
               MFRC522     Arduino
               Reader/PCD  Nano v3
   Signal      Pin         Pin
   -------------------------------
   RST/Reset   RST         D9
   SPI SS      SDA(SS)     D10
   SPI MOSI    MOSI        D11
   SPI MISO    MISO        D12
   SPI SCK     SCK         D13

   Other connections
   -----------------------------------
   Device      Dev. Pin    Arduino Pin
   -----------------------------------
   Neopixel    Digital_in  D3
   Encoder     CLK         D6
   Encoder     DT          D5
   Encoder     SW          D4

*/

//#define ENCODER_DO_NOT_USE_INTERRUPTS
#include <SPI.h>
#include <MFRC522.h>
#include <Adafruit_NeoPixel.h>
#include <Encoder.h>

#ifdef __AVR__
#include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif


const int RST_PIN     = 9;
const int SS_PIN      = 10;
const int LED_PIN     = 3;
const int BUTTON_PIN  = 4;

// milliseconds of no button press until we stop waiting to "flash"
const int TIMEOUT     = 3000;

//Max val the encoder can take on
const int MAX_ENCODER = 100;
//Max val that buffer[1] could be
const int MAX_SCANS = 30

                      const int LED_COUNT   = 16;
const int BRIGHTNESS  = 100;
const int FILLUP_SPEED = 10;

//Location of bytes in the RFID tag to write to
const byte BLK_ADDR      = 4;
const byte TRAILER_BLK   = 7;

float last = millis();
long pos = 0;
bool increment;

// Declare NeoPixel strip object:
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

// Create MFRC522 instance.
MFRC522 mfrc522(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key key;

Encoder myEnc(6, 5);

void dump_byte_array(byte *buffer, byte bufferSize);


void setup() {
  Serial.begin(115200); // Initialize serial communications with the PC
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  strip.begin();           // INITIALIZE NeoPixel strip object (REQUIRED)
  strip.show();            // Turn OFF all pixels ASAP
  strip.setBrightness(BRIGHTNESS); // Set BRIGHTNESS to about 1/5 (max = 255)

  SPI.begin();        // Init SPI bus
  mfrc522.PCD_Init(); // Init MFRC522 card

  // Prepare the key (used both as key A and as key B)
  for (byte i = 0; i < 6; i++) {
    key.keyByte[i] = 0xFF;
  }
}

/*
   Main loop.
*/
void loop() {
  bool button = !digitalRead(BUTTON_PIN);
  bool flashed = false;
  if (button) {
    increment = false;
    last = millis();
    pos = min(max(0, myEnc.read()), MAX_ENCODER);
    myEnc.write(pos);
    display(pos, MAX_ENCODER);
    return;
  } else {
    // "flash" routine timed out, go back to waiting to
    // read and increment RFID tag
    if (millis() - last > TIMEOUT) {
      fadeout(2);
      increment = true;
      myEnc.write(0);
    }
  }

  // Reset the loop if no new card present on the sensor/reader.
  // We only check rfid tags when the button is not pressed
  // This prevents encoder lagging
  if ( ! mfrc522.PICC_IsNewCardPresent()) {
    return;
  }
  // Select one of the cards
  if ( ! mfrc522.PICC_ReadCardSerial())
    return;

  //reset encoder position
  myEnc.write(0);

  //declare buffer for data to be read into
  MFRC522::StatusCode status;
  byte buffer[18];
  byte size = sizeof(buffer);

  // Authenticate using key A,B
  status = (MFRC522::StatusCode) mfrc522.PCD_Authenticate(
             MFRC522::PICC_CMD_MF_AUTH_KEY_A, TRAILER_BLK, &key, &(mfrc522.uid));
  status = (MFRC522::StatusCode) mfrc522.PCD_Authenticate(
             MFRC522::PICC_CMD_MF_AUTH_KEY_B, TRAILER_BLK, &key, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("PCD_Authenticate() failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  // Read to buffer
  mfrc522.MIFARE_Read(BLK_ADDR, buffer, &size);

  //Update buffer or reinitialize buffer
  if (increment) {
    buffer[0] = min(buffer[0] + 1, buffer[1]);
  } else {
    increment = true;
    flashed = true;
    buffer[0] = 0;
    buffer[1] = map(pos, 0, MAX_ENCODER, 0, MAX_SCANS);
  }

  // Write data to the block
  status = (MFRC522::StatusCode) mfrc522.MIFARE_Write(BLK_ADDR, buffer, 16);
  if (status != MFRC522::STATUS_OK) {
    Serial.print(F("MIFARE_Write() failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
  }

  // Halt PICC
  mfrc522.PICC_HaltA();
  // Stop encryption on PCD
  mfrc522.PCD_StopCrypto1();

  // fill up ring and fade out to indicate newly flashed tag
  if (flashed) {
    for (int i = pos; i < MAX_ENCODER; i++) {
      display(i, MAX_ENCODER);
      delay(3);
    }
    fadeout(1);
  }

  //always display current strength animation
  fill_up_to(int(buffer[0]), int(buffer[1]));

  //Habit full strength, display lights
  if (buffer[0] == buffer[1])
  {
    rainbow_cycle();
  }
  last = millis();
}

/**
   Helper routine to dump a byte array as hex values to Serial.
*/
void dump_byte_array(byte *buffer, byte bufferSize) {
  for (byte i = 0; i < bufferSize; i++) {
    Serial.print(buffer[i] < 0x10 ? " 0" : " ");
    Serial.print(buffer[i], HEX);
  }
}

//display encoder value on ring with m being full
void display(int cur, int m) {
  int remapped = map(cur, 0, m, 0, BRIGHTNESS * LED_COUNT);

  int full_bright = remapped / BRIGHTNESS;
  int rem = (remapped) % BRIGHTNESS / 10 * 10;

  strip.clear();
  if (full_bright)
    strip.fill(strip.ColorHSV(49152, 255, BRIGHTNESS),
               LED_COUNT - full_bright, full_bright);
  strip.setPixelColor(LED_COUNT - 1 - full_bright,
                      strip.ColorHSV(49152, 255, rem));
  strip.show();
}

//fade out ring light at given speed
void fadeout(int speed) {
  for (int i = BRIGHTNESS; i >= 0; i -= speed) {
    strip.setBrightness(i);
    strip.show();
  }
  strip.clear();
  strip.setBrightness(BRIGHTNESS);
  strip.show();
}

//animate the ring filling up to an arc based on
//value (cur) and what would be a full circle (m)
void fill_up_to(int cur, int m) {
  int remapped = map(cur, 0, m, 0, BRIGHTNESS * LED_COUNT);

  int full_bright = remapped / BRIGHTNESS;
  int rem = (remapped) % BRIGHTNESS / 10 * 10;

  strip.clear();
  strip.show();
  for (uint16_t i = 0; i < full_bright; i++) {
    uint16_t hue = 0 - i * (65536 / LED_COUNT);
    for (int b = 0; b < 256; b += FILLUP_SPEED) {
      strip.setPixelColor(LED_COUNT - 1 - i,
                          strip.ColorHSV(hue, 255, b));
      delay(1);
      strip.show();
    }
  }

  uint16_t hue = 0 - full_bright * (65536 / LED_COUNT);
  strip.setPixelColor(LED_COUNT - 1 - full_bright,
                      strip.ColorHSV(hue, 255, rem));

  strip.show();
}

//rotate rainbow light around ring and fade out
void rainbow_cycle() {
  strip.clear();
  uint16_t prev = 0;
  for (uint16_t offset = 0;
       offset >= prev;
       offset += 65536 / LED_COUNT / 20) {
    
    prev = offset;
    for (uint16_t i = 0; i < LED_COUNT; i++) {
      uint16_t hue = offset - i * (65536 / LED_COUNT);
      strip.setPixelColor(LED_COUNT - 1 - i,
                          strip.ColorHSV(hue, 255, BRIGHTNESS));
    }
    delay(2);
    strip.show();
  }
  fadeout(1);
}