Return to Garage Door Minder – Photon IoT

Use a MLX90393 Magnetometer as a Car Detector

Initially my Particle Photon garage door minder used IR sensors to detect both the presence of each of two cars and to detect that the garage door opening was blocked.  The problem with this was cross-talk between the 3 different IR sensors.  Thus my need to find some alternative method for detecting the presence of the cars.

What’s a car?  A big hunk of metal.  So how do to detect metal?  Maybe a Magnetometer!

I found that SparkFun has an MLX90393 Magnetometer in a nice breakout board with an I2C interface.  I was already using an I2C interface on the Photon garage door minder (for interfacing with a temperature sensor) so sticking with an I2C interface device seemed wise.

Sparkfun MLX90393 breakout board

SparkFun MLX90393 breakout board

The main idea for using a magnetometer to detect a car was that the presence of a large chunk of metal (the car) would change the local natural magnetic field when the car was present.  The MLX90393 is a 3 axis magnetometer so the ‘current magnetic field’ is represented by a set of X, Y, and Z values, but in reality due to changing local conditions, the ‘no car present’ state actually is a 3D bounding box representing how the X, Y, and Z values fluctuate over time.  The same is true for the ‘car present’ state, as it is represented by another 3D bounding box containing the normal fluctuation of X, Y, Z values when a car is present.

I wrote some quick code to read and display X,Y,Z values which I ran for several minutes when a car not present.  The min/max range of the X, Y, Z values then represented the “no car present” bounding box.  I then thought that I could say that a car was present when any one of the X, Y, Z points was outside that bounding box.  Turned out still too much variation across the day and when the car was leaving the driveway the X, Y, Z values again bounced around a lot.  So yeah; no.  I had to rethink a bit.

Too much boundary condition.  I figured the algorithm needed some hysteresis  as well.

Now the algorithm became a two bounding box algorithm,  so that if the car was currently absent, it would be parked present if at least one point was outside of the larger bounding box and if the car was currently present, then all 3 points had to be inside a smaller bounding box.  This gives a dead-band between the two bounding boxes where the state of the car present/absent won’t change so we’ve got some hysteresis now in the calculation.

The small/large bounding boxes were calculated by taking the 2 minute max/min making the resulting bounding box 2x larger and for the large bounding box, making it 4x larger.  This seems to put enough difference between the car present values and the car absent values so that we get a nice reliable signal.

Side note, one thing I found is that you have to generate the 2 minute bounding box, in the exact location where the sensor will be placed.  Stray magnetic fields and metal (even the rebar in the concrete) will affect the values.

One thing I did with the Sparkfun breakout board was to encase the whole thing in 5 minute epoxy so that it could withstand the dirt and water that would certainly eventually turn up on the garage floor.  Encased in epoxy, the board looks a little funny, but certainly is more robust.

the SparkFun breakout board encased with 5 minute epoxy

Then once I had the whole thing installed, I covered it with tape to further protect it from the elements.  Here’s a pic, but really all you can see is the tape on the floor!  Next to it (with red tape) is the emitter for the previous beam-break IR sensor.  I like the whale tape much better.

 

Update

After running the sensor for several months, several realizations.  The X and Y values don’t make a lot of difference.  Worse, all 3 values have some amount of temperature drift and the small changes between present and absent car in the X and Y values were overwhelmed by the temperature drift.  So I decided to drop the X and Y, and only use the Z value. The Z value is the one perpendicular to the garage floor and it’s got enough change that even the temperature drift doesn’t impact the measurement.

Here’s what the Z value looks like over a couple of days

The other update is that going only with the Z, I was able to optimize the sensor code to only request and return the Z value, saving some in the read time.  I also cleaned up some of the I2C code to do some better error handling and remove some extra writes.

The code required to support this is three parts.  First, the MLX90393 has to be initialized.  Second I wrote a calibration routine that would run for several minutes and generate the initial Min/Max values and store those in non-volatile memory.  Third, was the routine to read the X, Y, Z values and determine if the car was present or absent.  (these routines are set up to eventually support two sensors, one for each car)

The initialization routine:  This routine initializes the MXL90393 to do conversion on demand and sets the digital filtering as high as I could get it.  Note the LOGI call is for testing with the psyslog module.  Most are commented out by default, but some are left in for error conditions.

//
// initialize the magnetic car sensor
// Addr = 0x0c, 7 bit i2c address
//
void InitCarSensor(uint8_t Addr) {
    
    Wire.beginTransmission(Addr);
    uint8_t MagReset2[] = {0x80};  // 'exit
    Wire.write(MagReset2,sizeof(MagReset2));
    I2CStatus = Wire.endTransmission(false);    // end transmission but w/ restart
    //LOGI(String::format("EOT= %i",I2CStatus));  //<== returns 0
    if(I2CStatus == 0) {
        if(Wire.requestFrom(Addr, (uint8_t)1,true) == 0) {
            I2CStatus = 6;
            LOGI(String::format("EOT= %i",I2CStatus)); 
        } else {
            Wire.read();
            //LOGI(String::format("read %i",Wire.read()));  //<== returns a 5
        }
    } else {
        I2CStatus = Wire.endTransmission(true);
        LOGI(String::format("EOT= %i",I2CStatus));     
    }
    
    delay((unsigned long)10);
    
    Wire.beginTransmission(Addr);
    uint8_t MagReset[] = {0xf0};  // 'reset
    Wire.write(MagReset,sizeof(MagReset));
    I2CStatus = Wire.endTransmission(false);    // end transmission but w/ restart
    //LOGI(String::format("EOT= %i",I2CStatus));  //<== returns 0
    if(I2CStatus == 0) {
        if(Wire.requestFrom(Addr, (uint8_t)1,true) == 0) {
            I2CStatus = 6;
            LOGI(String::format("EOT= %i",I2CStatus)); 
        } else {
            Wire.read();
            //LOGI(String::format("read %i",Wire.read()));  //<== returns a 5
        }
    } else {
        I2CStatus = Wire.endTransmission(true);
        LOGI(String::format("EOT= %i",I2CStatus)); 
    }
    
    delay((unsigned long)10);

    Wire.beginTransmission(Addr);
    uint8_t MagConfig[] = {0x60,0x00,0x1f,0x08};  // 'set the filtering in reg 2 Dig_Filt and OSR = 2, res = 1
    Wire.write(MagConfig,sizeof(MagConfig));
    I2CStatus = Wire.endTransmission(false);    // end transmission but w/ restart
    if(I2CStatus == 0) {
        if(Wire.requestFrom(Addr, (uint8_t)1,true) == 0) {
            I2CStatus = 6;
            LOGI(String::format("EOT= %i",I2CStatus)); 
        } else {
            Wire.read();
            //LOGI(String::format("read %i",Wire.read()));  //<== return 3
        }
    } else {
        I2CStatus = Wire.endTransmission(true);
        LOGI(String::format("EOT= %i",I2CStatus)); 
    }
    if(I2CStatus != 0){
        SayI2CError(); 
    } else {
        FirstI2CError = true;  // got through successfully so reset I2C error flag
    }
    //LOGI(String::format("done %i",I2CStatus));
    return;
}

The calibration routine:  This routine samples the magnetometer for a bit and saves the resulting max/min X/Y/Z values in non-volatile memory.

//
// run the specified magnetometer for about a minute
// and remember the max/min values. save those in NVRAM for later use
//
int CalibrateMag(String Sel){
    int c,x,y,z;
    int lx,hx,ly,hy,lz,hz;
    float t;
    int ii;
    uint8_t a;
    
    sscanf(Sel,"%d",&c);     // which magnetometer? 0 or 1
    if(c == 0){
        a = 0x0c;            // I2C address of 1st magnetometer
    } else {
        a = 0x0d;            // I2C address of 2nd magnetometer
    }
    
    ReadCarSensor(a,x,y,z,t);  // start with all values the same
    hx = lx = x;               // then run for a bit to see what the range looks like
    hy = ly = y;
    hz = lz = z;
    ii = 0;
    do {
        ReadCarSensor(a,x,y,z,t);   // go read current x,y,z and temp (just 'cause it's there)
        hx = max(x,hx);             // a new max X
        lx = min(x,lx);             // a new min X
        hy = max(y,hy);
        ly = min(y,ly);
        hz = max(z,hz);
        lz = min(z,lz);
        delay((unsigned long)100); // wait a bit and do it again
        Particle.process(); 
    } while(ii++ < 200);           // repeat for 200 * 100ms
    
    // store it in eeprom
    EEPROM.put(8+0+(c*24),lx);     // plan for 2 magnetometers, each in
    EEPROM.put(8+4+(c*24),hx);     // in a separate 24 bytes of nvram.
    EEPROM.put(8+8+(c*24),ly);
    EEPROM.put(8+12+(c*24),hy);
    EEPROM.put(8+16+(c*24),lz);
    EEPROM.put(8+20+(c*24),hz);
    
    FetchCarCalib();              // normally done on boot, read back eeprom
                                  // and calculate the small and large bounding boxes
    
    return(0);
}
//
// grab the stored max/min values and calculate the
// small and large bounding boxes to determine
// car present / absent
//
void FetchCarCalib() {
        
    // Get the stored magnetometer box for each of the 2 magnetometers
    for (int c = 0; c < 2; c++){
        int ll,hh;
        EEPROM.get(8+0+(c*24),ll);          //  low x
        EEPROM.get(8+4+(c*24),hh);          //  high x
        llx[c] = ll - 4*(hh-ll);            // grow the large bounding box low side by 4x
        lhx[c] = ll - (hh-ll)*2;            // grow the small bounding box low side by 2x
        hlx[c] = hh + (hh-ll)*2;            // grow the small bounding box high side by 2x
        hhx[c] = hh + 4*(hh-ll);            // grow the large bounding box high side by 4x
        
        
        EEPROM.get(8+8+(c*24),ll);          // repeat above for Y
        EEPROM.get(8+12+(c*24),hh);
        lly[c] = ll - 4*(hh-ll);
        lhy[c] = ll - (hh-ll)*2;
        hly[c] = hh + (hh-ll)*2;
        hhy[c] = hh + 4*(hh-ll);

        EEPROM.get(8+16+(c*24),ll);        // repeat above for Z
        EEPROM.get(8+20+(c*24),hh);
        llz[c] = ll - 4*(hh-ll);
        lhz[c] = ll - (hh-ll)*2;
        hlz[c] = hh + (hh-ll)*2;
        hhz[c] = hh + 4*(hh-ll);
    }
    
LOGI("== new box =="); 
LOGI(String::format("x= %i %i %i %i",llx[0],lhx[0],hlx[0],hhx[0]));
delay((unsigned long)50);
LOGI(String::format("y= %i %i %i %i",lly[0],lhy[0],hly[0],hhy[0]));
delay((unsigned long)50);
LOGI(String::format("z= %i %i %i %i",llz[0],lhz[0],hlz[0],hhz[0]));
delay((unsigned long)50);
LOGI("== new box =="); 
    
}

Routines to read sensor values and determine if a car is present or not.

//
// read the magnetic car sensor, values in x,y,z,t
//
void ReadCarSensor(uint8_t Addr, int &xVal, int &yVal, int &zVal, float &tVal) {
   
   xVal = yVal = zVal = 0;
   tVal = 0.0;
   
   // start a conversion
    Wire.beginTransmission(Addr);
    //Wire.write(0x3f);                            // start a conversion, all 4 vars, XYZT
    Wire.write(0x39);                              // only want z and t
    I2CStatus = Wire.endTransmission(false);       // end transmission but w/ restart

    if(I2CStatus == 0) {
        if(Wire.requestFrom(Addr, (uint8_t)1,true) == 0) {
            I2CStatus = 6;
            LOGI(String::format("EOT= %i",I2CStatus));
        } else {
            Wire.read();
            //LOGI(String::format("read %i",Wire.read()));  //<== return 3   ? read 35 = 100011 = SM_mode and D1/D0
        }
    } else {
        LOGI(String::format("EOT= %i",I2CStatus));  // I2C Status that caused the ELSE
        I2CStatus = Wire.endTransmission(true); 
    }
    
    // wait for conversion to complete, ie the status byte becomes 3 or 1
    int ii;
    if(I2CStatus == 0) {
        uint8_t b;
        ii = 0;
        do {
            Wire.beginTransmission(Addr);
            Wire.write(0x00);                               // do a noop to get the status of the covert
            I2CStatus = Wire.endTransmission(false);        // end transmission but w/ restart
            if(I2CStatus == 0) {
                if(Wire.requestFrom(Addr, (uint8_t)1,true) == 0) {    // 1 byte status
                    I2CStatus = 6;
                    LOGI(String::format("EOT= %i",I2CStatus));
                } else {
                    b = Wire.read();        // result byte
                    //if(b != 3)delay((unsigned long)10);  // XYZT
                    if(b != 1)delay((unsigned long)10);    //ZT only
                }
            } else { 
                LOGI(String::format("EOT= %i",I2CStatus));
                I2CStatus = Wire.endTransmission(true);
            }
            ii++;
        //} while ((b != 3) && (I2CStatus == 0) && (ii < 30)); // XYZT
        } while ((b != 1) && (I2CStatus == 0) && (ii < 30));  // ZT only
    }
    
    // now read back the conversion data
    if(I2CStatus == 0) {
        Wire.beginTransmission(Addr);
        //Wire.write(0x4f);                            // read result all 4 vars, // XYZT
        Wire.write(0x49);                            // read result all 2 vars, ZT only
        I2CStatus = Wire.endTransmission(false);    // end transmission but w/ restart

        if(I2CStatus == 0) {
            //if(Wire.requestFrom(Addr, (uint8_t)9,true) == 0) {    // 4 2byte vars plus byte status, // XYZT
            if(Wire.requestFrom(Addr, (uint8_t)5,true) == 0) {    // 2 2byte vars plus byte status, ZT only
                I2CStatus = 6;
                LOGI(String::format("EOT= %i",I2CStatus));
            } else {
                uint8_t b;
                b = Wire.read();                // get status, and number of bytes.  when done just = # of bytes
                //if(b == 3){ // XYZT
                if(b == 1){ //ZT only
                    tVal = (((((Wire.read() * 256.0 + Wire.read()) - 46244) / 45.2) + 25.0) * 1.8) + 32.0;
//                  xVal = (int)Wire.read() * 256 + (int)Wire.read();
//                  if(xVal > 32768) xVal = xVal - 65535;
//                  yVal = (int)Wire.read() * 256 + (int)Wire.read();
//                  if(yVal > 32768) yVal = yVal - 65535;
                    zVal = (int)Wire.read() * 256 + (int)Wire.read();
                    if(zVal > 32768) zVal = zVal - 65535;
                    Mag = String::format("%i,%i,%i,%f",xVal,yVal,zVal,tVal);
                } else {
                    I2CStatus = 7;
                    LOGI(String::format("EOT= %i",I2CStatus)); 
                }
            }
        }  else {
            LOGI(String::format("EOT= %i",I2CStatus)); 
            I2CStatus = Wire.endTransmission(true); 
        }
    } else {
        LOGI(String::format("EOT= %i",I2CStatus)); 
    }
    if(I2CStatus != 0){
        SayI2CError(); 
    } else {
        FirstI2CError = true;
    }
}
//
// check for car present, car 0 or 1
// sensor must have run calibrate at some point
// without a car present to have a base line
// assumes that baseline has already been read from
// eeprom into l/h array
//
int CheckCar(int c, int CurVal){
    uint8_t a;
    int x,y,z,result;
    float t;
    if(c == 0){
        a = 0x0c;
    } else {
        a = 0x0d;
    }
    
    result = CurVal;                // unless we're outside the hysterisis, it stays the same
    ReadCarSensor(a,x,y,z,t);
    // simplify to only looking at Z where the strongest coorlation is
    if(CurVal == 0) {                // curval is 0 (inside box) so one point must be outside largest box to move to 1
        if( (z < llz[c]) || (z > hhz[c]) ) {
                result = 1;
                snprintf(TempChars,128,"Havecar x,y,z= %i,%i,%i res=%i",x,y,z,result);
                LOGI(TempChars);  // only log if changed
        }
    } else {                        // curval is 1, so all points must be inside smaller box to move to 0
        if( (z > lhz[c]) && (z < hlz[c]) ){
                result = 0;
                snprintf(TempChars,128,"No car x,y,z= %i,%i,%i res=%i",x,y,z,result);
                LOGI(TempChars);
        }
    }
    if( c==0 ){
        FloorTemp1 = t;
    } else {
    }
    MagX = x;
    MagY = y;
    MagZ = z;
}

I2C – How far can you go?

One interesting note is that using the MLX90393 as a car sensor means I need to run an I2C signal from the roof of the garage (where the Photon is located) all the way down to the middle of the garage floor where the car sits.  That’s a distance of about 30 feet.  if you …