Schrödinger's Hourglass

Scott Cooper and Khanh Nguyen

Overview

My secret santa, from work, got me a cool 6 inch hourglass. So my buddy Khanh and I tore it apart and put it on a robot to monitor its accuracy over time. It is probably better to say we measured the hourglasses percision, but whatever.  Every three and a half minutes a photo resistor/LED pair sees that there is no more sand coming through the neck, records the elapsed time, and flips it over. We are using an Arduino on a bread board for street cred. There is also a 16Mb AT45DB161D flash ROM on-board to record the trial number, elapsed milliseconds, and average light levels when empty and full.

Because the stepper motor is disabled except for the very brief times during the hourglass flips, it uses very little power.  The code saves every trial to a small flash ROM and can be restarted without losing much data if the input power fails.  A green LED was selected because I found that the cadimium sulphide photoresister is more sensitive to the blue/green spectrum than the reds and yellows.

 

Shrodingers Hourglass

Shrodingers Hourglass

Shrodingers Hourglass

Shrodingers Hourglass

Shrodingers Hourglass

Results

Nothing about this experiment worked as expected.  The first two graphs show the time it took for the sand to move between chambers.  There is about a two second difference between the two chambers.  Each chamber empties at a different rate.  This is likely because of different neck angles on each chamber.  The top graph also shows the occasional "Impossibly Short Elapsed Times".  This is weird.  Some of these trials ended 45 seconds too early.  Can't explain.

The second graph is just a close-up of the data.  I was able to complete about 400 trials per day.  Some of the little blips in the data appear to spike twice a day.  Lunar tidal forces acting on the sand?  Wishful thinking, but that would be cool.

Then there are the weird drops around trial 2400 and 5600, about 8 days apart.  What is that?  I'm not a statistician but it also looks like it is taking about 0.5 seconds longer to complete that when the trails were first started.

The next set of graphs show the light levels recorded during the trials.  There was a significant and gradual lowering of the LED light levels.  My LEDs were likely affected by dust, temperature and age, which I did not take into account.  This drop in light level forced me to stop the experiment early, because I hard coded everything to spectific light levels.  Again the two chambers show different light levels when the upper chamber is full of sand.  This is caused by my placement of the LED and photoresistor.  The values between 440 and 490 are from the occasional neck clog.  The values from 490 to 520 are probably just the last grains of sand getting stuck. Again we see a slow worble of the closeup data.  LED temperature?

 

Raw Data

You can find about 24 days of raw data here:

Below are the results of the first 25 trials:  This took approximately 1 hour 23 minutes to collect.

0,213549,382,570
1,215756,415,562
2,212987,389,568
3,215263,415,568
4,213492,388,567
5,215264,418,567
6,212912,388,567
7,215428,416,568
8,250000,481,480
9,1315,569,569
10,212614,387,567
11,214800,414,567
12,213498,387,564
13,215431,412,563
14,190880,388,567
15,214744,414,566
16,213093,388,564
17,215184,414,567
18,212721,387,564
19,1313,555,555
20,1313,562,562
21,214649,413,564
22,213396,387,566
23,215483,415,564
24,213624,388,566
25,215423,413,566

The columns are:

  1. Trial number
  2. Elapsed time in milliseconds
  3. The average light level recorded one second after flipping the hourglass
  4. The average light level recorded one second after all the sand got to the bottom chamber

On trial 8 & 9 we see a common neck clog.

  1. Trial 7 is ordinary
  2. The hourglass was flipped starting trial 8
  3. The sand immediantly gets clogged in the neck
  4. One second later an odd average light level of 481 was recorded (Sand on top but not going thru neck)
  5. The light level never goes above 520 so the watchdog timer expired at 250000 milliseconds
  6. After one more second the odd average light level of 480 was recorded (Sand still on top but not going thru neck)
  7. The hourglass was flipped starting trial 9
  8. The sand is now already on the bottom so trial 9 sees a empty neck (above 520) and immediately flips again
  9. Trial 10 is ordinary

Light levels

I do not know what is causing this. It might be the LED failing, the hot glue becoming more opace, or the neck becoming scratched and dusty.

Neck Clogs

I expected a few clogs and coded a watch dot timer to deal with them, but the clogs I have gotten seem to be getting further apart.

Back to Back Flips

Impossibly Short Elapsed Times

WTF... How is this possible? It surely must be a mistake in my code, but what? Am I failing to get the correct times in and back out of the flash device? What the hell...

Update:

It has come to my attention that using a Arduinos millis() function might not be very accurate for this type of measurement.  I will need to re-run this experiment using better controls.

Battery backup
Use DS1307 real time clock or something similar
Record date and time, not just elapsed time
Temperature and humidity would also be nice
Better usage of ADC voltage range.  Might involve an op-amp.

Code

///////////////////////////////////////////////////////////////////////////////////////////////////////
// Schrodinger's Hourglass v1.0 
// Dullbits.com
// By Scott Cooper and Khanh Nguyen
// 
// Known problems:
//     1.  Millis() will overflow after aprox 50 days resulting in one bad trial run.
//         Not worth the trouble to correct.  I'm hard core that way.  Shut up, I am too.
//         Although the nozzle/clog watch dog timer will also fail.  So really this code is
//         bullet proof as long as nobody shoots any bullets.
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Trip points:
//
//    Orientation   Empty        Full        Trip
//    -------------------------------------------
//    Dot Up        587-588      431-448     520
//    Dot Down      587-588      407-412     520
///////////////////////////////////////////////////////////////////////////////////////////////////////

#include 
  
unsigned long flash_record;
unsigned long flash_page;
unsigned long flash_current_page = 999999;
unsigned long flash_record_offset;
const unsigned long flash_page_size = 528;
const unsigned long flash_max_pages = 4096;
const unsigned long flash_records_per_page = flash_page_size / 8;
const unsigned long flash_max_offset = flash_page_size - 8;
const unsigned long flash_max_records = flash_max_pages * flash_records_per_page;

unsigned long time_start;
unsigned long time_end;
unsigned long time_last;
unsigned long time_elapsed;
unsigned long time_current;
unsigned long time_abort;

unsigned long time;
unsigned int light1;
unsigned int light2;

Dataflash dflash; 

#define USB_SLEEP_PIN 2
#define DIR_PIN 6
#define STEP_PIN 7
#define ENABLE_PIN 8
#define BLINKY_PIN A5
#define PHOTORES_PIN A3
#define LED_PIN A2

///////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() { 
  Serial.begin(115200);
  Serial.println("Schrodinger's Hourglass v1.0, initializing");
  pinMode(PHOTORES_PIN, INPUT );
  pinMode(USB_SLEEP_PIN, INPUT);
  pinMode(DIR_PIN, OUTPUT);
  pinMode(STEP_PIN, OUTPUT);
  pinMode(ENABLE_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BLINKY_PIN, OUTPUT);
  dflash.init();
  digitalWrite(ENABLE_PIN, HIGH);

  for(long k=7; k>0; k--) {
    Serial.println(k);
    delay(1000);
  }
  Serial.print("Ambiant light level: "); 
  Serial.println(avg_light_level());
  
  digitalWrite(BLINKY_PIN, HIGH);
  digitalWrite(LED_PIN, HIGH);
  
  //wipe_flash();
  flash_record = find_last_record();
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
void loop(){
  dump_all_records();
  rotate(400, 600);
  time_trial(520 * 6);  
  
  dump_all_records();
  rotate(-400, 600);
  time_trial(520 * 6);  
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
void time_trial(int trip_point){ 
  long vsum = 0;
  unsigned int v1, v2, v3, v4, v5, v6;
  v1 = v2 = v3 = v4 = v5 = v6 = 0;
  time_start = millis()-200;
  time_abort = time_start + 250000;
  light1 = avg_light_level();

  do {
    v6 = v5;
    v5 = v4;
    v4 = v3;
    v3 = v2;
    v2 = v1;
    v1 = analogRead(PHOTORES_PIN);
    vsum = v1 + v2 + v3 + v4 + v5 + v6;

    time_current = millis();
    
    // Its been too long, the nozzle/neck might be clogged.  Abort this trial
    if (time_current >= time_abort) { vsum = 65535; }
    
    // Status update for the carbon based lifeforms
    if (time_current > time_last) {
      time_last = time_current + 10000;
      Serial.print((float)(time_current - time_start) / 2130 );
      Serial.print("%    ");
      Serial.println(vsum / 6);
    }
  } while ( vsum < trip_point);

  time_end = millis();
  light2 = avg_light_level();

  Serial.print("Writing record: ");
  Serial.print(flash_record);
  Serial.print("    Elapsed time: ");
  Serial.print((time_end - time_start) / 1000.0);
  Serial.print("    Light level full: "); 
  Serial.print(light1); 
  Serial.print("    Light level empty: "); 
  Serial.print(light2); 
  Serial.println();

  write_record(time_end - time_start, light1, light2);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
void rotate(int steps, long speed){ 
  //rotate a specific number of microsteps (4 microsteps per step) - (negitive for reverse movement)
  //speed is any number 500 or bigger, bigger is slower
  int dir = (steps > 0)? HIGH:LOW;
  steps = abs(steps);

  digitalWrite(DIR_PIN, dir); 
  digitalWrite(ENABLE_PIN, LOW);
  delay(200);
  for(int i=0; i < steps; i++){ 
    digitalWrite(STEP_PIN, HIGH);
    delayMicroseconds(speed);
    digitalWrite(STEP_PIN, LOW); 
    delayMicroseconds(speed); 
  } 
  delay(200);
  digitalWrite(ENABLE_PIN, HIGH);
} 

///////////////////////////////////////////////////////////////////////////////////////////////////////
unsigned int avg_light_level(){
  long vsum = 0;
  
  delay(1000);
  for (int k=0; k<1000; k++) {
    vsum += analogRead(PHOTORES_PIN);
  }
  return vsum / 1000;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
void write_record(unsigned long time, unsigned int light1, unsigned int light2){
  flash_page = flash_record / flash_records_per_page;
  flash_record_offset = (flash_record % flash_records_per_page) * 8;

  //Get the old one first before adding to it, slow but safe
  dflash.Page_To_Buffer(flash_page, 1);

  dflash.Buffer_Write_Byte(1, flash_record_offset + 0, (time >> 24)       );
  dflash.Buffer_Write_Byte(1, flash_record_offset + 1, (time >> 16) & 0xFF);
  dflash.Buffer_Write_Byte(1, flash_record_offset + 2, (time >>  8) & 0xFF);
  dflash.Buffer_Write_Byte(1, flash_record_offset + 3, (time      ) & 0xFF);

  dflash.Buffer_Write_Byte(1, flash_record_offset + 4, light1 >> 8);
  dflash.Buffer_Write_Byte(1, flash_record_offset + 5, light1 & 0xFF);
  dflash.Buffer_Write_Byte(1, flash_record_offset + 6, light2 >> 8);
  dflash.Buffer_Write_Byte(1, flash_record_offset + 7, light2 & 0xFF);
  
  dflash.Buffer_To_Page(1, flash_page);
  flash_record++;
  
  // Invalidate the current buffer used by read_record
  flash_current_page = 999999;

  /*
  Serial.print("Writing Buffer,  record:");
  Serial.print(flash_record);
  Serial.print("  page:");
  Serial.print(flash_page);
  Serial.print("  record_offset:");
  Serial.print(flash_record_offset);
  Serial.print("    Time: "); 
  Serial.print(time); 
  Serial.print("    Light1: "); 
  Serial.print(light1); 
  Serial.print("    Light2: "); 
  Serial.print(light2); 
  Serial.println();
  */
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
void read_record(unsigned long flash_record){
  flash_page = flash_record / flash_records_per_page;
  flash_record_offset = (flash_record % flash_records_per_page) * 8;

  if (flash_current_page != flash_page) {
    dflash.Page_To_Buffer(flash_page, 1);
    flash_current_page = flash_page;
  }
  
  time    = (long)dflash.Buffer_Read_Byte(1, flash_record_offset + 0) << 24;
  time   |= (long)dflash.Buffer_Read_Byte(1, flash_record_offset + 1) << 16;
  time   |= (long)dflash.Buffer_Read_Byte(1, flash_record_offset + 2) <<  8;
  time   |= (long)dflash.Buffer_Read_Byte(1, flash_record_offset + 3);
  light1  = dflash.Buffer_Read_Byte(1, flash_record_offset + 4) << 8;
  light1 |= dflash.Buffer_Read_Byte(1, flash_record_offset + 5);
  light2  = dflash.Buffer_Read_Byte(1, flash_record_offset + 6) << 8;
  light2 |= dflash.Buffer_Read_Byte(1, flash_record_offset + 7);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
void wipe_flash(){
  Serial.println("Wiping the flash..."); 
  for(unsigned long k=0; k<=flash_max_pages; k++) {
    for(unsigned long j=0; j<=flash_page_size; j++) {
      dflash.Buffer_Write_Byte(1, j, 0);
    }
    dflash.Buffer_To_Page(1, k);
  }
  flash_record=0;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
unsigned long find_last_record(){
  Serial.println("Finding last record on flash...");
  for(unsigned long k=0; k<=flash_max_records; k++) {
    read_record(k);
    if (time == 0 && light1 == 0 && light2 == 0) {
      Serial.print("Last record: ");
      Serial.println(k);
      return k;  
    }
  }
  Serial.print("Something is wrong, I'm not even kidding...");
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
void dump_all_records(){
  if (digitalRead(USB_SLEEP_PIN) == HIGH) {
    Serial.println("Dumping all records...");
    for(unsigned long k=0; k<=flash_max_records; k++) {
      read_record(k);
      if (time == 0 && light1 == 0 && light2 == 0) {
        return;  
      }
      Serial.print(k); 
      Serial.print(","); 
      Serial.print(time); 
      Serial.print(","); 
      Serial.print(light1); 
      Serial.print(","); 
      Serial.print(light2); 
      Serial.println();
    }
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////////////



created: Dec. 1, 2013, 1:01 a.m.
modified: April 14, 2019, 1 a.m.

Dullbits.com