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.

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) {
    
    //LOGI(String::format("start 0x0%x",Addr));
    Wire.beginTransmission(Addr);
    uint8_t MagReset[] = {0xf0};                 // 'reset the device
    Wire.write(MagReset,sizeof(MagReset));
    //LOGI(String::format("Wrote %i bytes",Wire.write(MagReset,sizeof(MagReset))));
    I2CStatus = Wire.endTransmission(false);    // end transmission but w/ restart
    //LOGI(String::format("EOT= %i",I2CStatus));
    if(I2CStatus == 0) {
        if(Wire.requestFrom(Addr, (uint8_t)1) == 0) {
            LOGI("no data returned");
            I2CStatus = 6;
            Wire.endTransmission(true); // make sure we release the bus
        } else {
            Wire.read();
            //LOGI(String::format("read %i",Wire.read()));
            I2CStatus = Wire.endTransmission(true);
        }
    }
     
    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));
    //LOGI(String::format("Wrote %i bytes",Wire.write(MagConfig,sizeof(MagConfig))));
    I2CStatus = Wire.endTransmission(false);    // end transmission but w/ restart
    //LOGI(String::format("EOT= %i",I2CStatus));
    if(I2CStatus == 0) {
        if(Wire.requestFrom(Addr, (uint8_t)1) == 0) {
            //LOGI("no data returned");
            I2CStatus = 6;
            Wire.endTransmission(true); // make sure we release the bus
        } else {
            Wire.read();
            //LOGI(String::format("read %i",Wire.read()));
            I2CStatus = Wire.endTransmission(true);
        }
    }
    if(I2CStatus != 0)SayI2CError(); else FirstI2CError = true;
    //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) {
   
   //LOGI("start");
   xVal = yVal = zVal = 0;
   tVal = 0.0;
   
   // start a conversion
    Wire.beginTransmission(Addr);
    Wire.write(0x3f);                            // start a conversion, all 4 vars
    I2CStatus = Wire.endTransmission(false);     // end transmission but w/ restart
    //LOGI(String::format("EOT= %i",I2CStatus));
    if(I2CStatus == 0) {
        if(Wire.requestFrom(Addr, (uint8_t)1) == 0) {
            I2CStatus = 6;
            Wire.endTransmission(true);          // make sure we release the bus
        } else {
            Wire.read();                         // read status, but I don't care
            I2CStatus = Wire.endTransmission(true);
            //LOGI(String::format("EOT= %i",I2CStatus));
        }
    }
    //LOGI(String::format("I2CStatus= %i",I2CStatus));
    
    // wait for conversion to complete, ie the status byte becomes 3
    if(I2CStatus == 0) {
        uint8_t b;
        int ii = 0;
        do {
            Wire.beginTransmission(Addr);
            Wire.write(0x00);                               // do a noop to get the status of the convert
            I2CStatus = Wire.endTransmission(false);        // end transmission but w/ restart
            //LOGI(String::format("EOT= %i",I2CStatus));
            if(I2CStatus == 0) {
                if(Wire.requestFrom(Addr, (uint8_t)1) == 0) {    // 1 byte status
                    I2CStatus = 6;
                    Wire.endTransmission(true);             // make sure we release the bus
                } else {
                    b = Wire.read();                        // status byte
                    //LOGI(String::format("b= 0x%x",b));
                    I2CStatus = Wire.endTransmission(true);
                    if(b != 3)delay((unsigned long)50);     // wait if status indicates not done yet
                    //LOGI(String::format("EOT= %i",I2CStatus));
                }
            }
            ii++;                                           // keep track of how long we waited
        } while ((b != 3) && (I2CStatus == 0) && (ii < 30)); // keep going until we get the right status, an I2C error or we exceed the max # of attempts
    }
    //LOGI(String::format("I2CStatus= %i",I2CStatus));
    
    // now read back the conversion data
    if(I2CStatus == 0) {
        Wire.beginTransmission(Addr);
        Wire.write(0x4f);                               // read result all 4 vars
        I2CStatus = Wire.endTransmission(false);        // end transmission but w/ restart
        //LOGI(String::format("EOT= %i",I2CStatus));
        if(I2CStatus == 0) {
            if(Wire.requestFrom(Addr, (uint8_t)9) == 0) {    // 4 2byte vars plus byte status
                I2CStatus = 6;
                Wire.endTransmission(); // make sure we release the bus
            } else {
                uint8_t b;
                b = Wire.read();                // get status
                //LOGI(String::format("b= 0x%x",b));
                if(b == 3){                    // status is good
                    tVal = (((((Wire.read() * 256.0 + Wire.read()) - 46244) / 45.2) + 25.0) * 1.8) + 32.0;  // convert temp to Deg F
                    //LOGI(String::format("t= %f",tVal));
                    xVal = (int)Wire.read() * 256 + (int)Wire.read();  // convert to signed int
                    if(xVal > 32768) xVal = xVal - 65535;
                    //LOGI(String::format("x= %i",xVal));
                    yVal = (int)Wire.read() * 256 + (int)Wire.read();
                    if(yVal > 32768) yVal = yVal - 65535;
                    //LOGI(String::format("y= %i",yVal));
                    zVal = (int)Wire.read() * 256 + (int)Wire.read();
                    if(zVal > 32768) zVal = zVal - 65535;
                    //LOGI(String::format("z= %i",zVal));
                    I2CStatus = Wire.endTransmission();
                    //LOGI(String::format("EOT= %i",I2CStatus));
                } else {
                    I2CStatus = 7;
                    Wire.endTransmission(); // make sure we release the bus 
                }
            }
        }
    }
    if(I2CStatus != 0)SayI2CError(); else FirstI2CError = true;
    //LOGI(String::format("done %i",I2CStatus));
}
//
// 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 hysteresis, the result stays the same
    ReadCarSensor(a,x,y,z,t);
    if(CurVal == 0) {                // curval is 0 (inside small box) so one point must be outside large box to move to 1
        if( ((x < llx[c]) || (x > hhx[c])) ||
            ((y < lly[c]) || (y > hhy[c])) ||
            ((z < llz[c]) || (z > hhz[c])) ) {
                result = 1;         // above or below, above or below, above or below, then 1 is car present
                //LOGI(String::format("x,y,z= %i,%i,%i res=%i",x,y,z,result));  // only log if changed
        }
    } else {                        // curval is 1 (car present), so all points must be inside smaller box to move to 0 (no car present)
        if( ((x > lhx[c]) && (x < hlx[c])) &&  
            ((y > lhy[c]) && (x < hly[c])) &&  
            ((z > lhz[c]) && (z < hlz[c])) ){
                result = 0;          // above and below, above and below, above and below so no car present
                //LOGI(String::format("x,y,z= %i,%i,%i res=%i",x,y,z,result));
        }
    }
    return(result);
}