The motion sensor is attached to analog input 0 of the Arduino controller. The output of the sensor is digital, so you get a very low or a very high value on this input.

According to the specifications, when the sensor gets power, it needs up to 30 seconds to stabilize. While this time it can give multiple signals. To solve this problem, I use an idle time of 20 seconds. The sensor has to be “silent” for 20 seconds until I consider it ready to use. The same is true after the sensor detected a motion until I put the controller back to idle state.

The Interface

The interface starts with an enum which defines all states of the sensor. There is Uninitialized until the loop() method was called once. In the state WaitStabilize the controller waits until the sensor is in a stable low state. The state Idle means, the sensor is working but did not detect any motion. If there is any motion, the controller changes into the state Alarm, until it is idle again.

There is also a typedef for the callback method, which takes the current time as unsigned long and the new status. This callback is called every time when the status changes.

The constant MOTION_SENSOR defines on which (analog) pin the motion sensor is connected. It’s not a flexible solution, e.g. for the case if you like to run multiple sensors. In this case you should add the pin as member variable and add the pin as argument for the constructor.

The second constant IDLE_TIME defines the time in seconds, how long the sensor has to be in low state until the controller goes (back) into idle state. The value of 20 seconds seems to be a good value. Again, not a flexible solution for multiple sensors.

class MotionSensor
{
public:
    /// The status of the motion sensor.
    enum Status : uint8_t {
        Uninitialized, ///< Loop was never called before.
        WaitStablilize, ///< Wait for the sensor to stabilize.
        Idle, ///< The motion sensor is working.
        Alarm, ///< The motion sensor registered motion.
    };

    /// The callback.
    ///
    /// The first parameter is the current time, and the
    /// The second parameter is the new status.
    ///
    typedef void (*Callback)(const unsigned long, Status);

private:
    /// The analog pin of the motion sensor.
    ///
    const int MOTION_SENSOR = 0;

    /// The idle time in seconds.
    /// This is the time which the sensor has to be in LOW state, before another event
    /// is triggered. It is also the initialization time which is required until
    /// the sensor is considered as ready.
    ///
    const int IDLE_TIME = 20;

The setup() method has to be called in the setup() method of the sketch. It initializes this controller.

You can set a callback using the setCallback() method. This has to be done before loop() is called the first time.

The loop() method has to be called in the loop() method of your sketch using the current time of the loop.

The status() method returns the current status of the motion sensor.

public:
    /// Call this method in setup()
    ///
    void setup();
    
    /// Set a callback if the status changes.
    ///
    void setCallback(Callback callback);
    
    /// Call this method in loop();
    ///
    void loop(const unsigned long currentTime);
    
    /// Get the current status of the motion sensor.
    ///
    inline Status status() const { return _status; }

The Implementation

The setup method is empty, because there is nothing to be done in the setup stage. This method merely exists to have a place to extend this controller with an initialization is required.

void MotionSensor::setup()
{
}

The loop() method behaves differently depending on the state of the sensor. Called in the Uninitialized state, it will read the state of the sensor and switch to the WaitStablilize state.

In the WaitStablilize state it will wait for a sensor change. If the sensor changes its state, the time since the last event is reset (_lastEvent.start(currentTime);). If there is no change, and the signal from the sensor is low, and the elapsed time is larger or equal the defines idle time — the controller will change into the Idle state.

In the Idle state, the controller waits for the sensor to go to high state. If it does, the controller changes its state into the Alarm state.

In Alarm state, the controller waits until the sensor stays in low state for equal or more than the defined idle time.

void MotionSensor::loop(const unsigned long currentTime)
{
    if (_status == Uninitialized) {
        _lastState = currentSensorState();
        setStatus(WaitStablilize, currentTime);
    } else if (_status == WaitStablilize) {
        // Check the sensor state.
        const bool sensorState = currentSensorState();
        if (sensorState != _lastState) {
            _lastEvent.start(currentTime);
            _lastState = sensorState;           
        } else if (sensorState == false) {
            if ((_lastEvent.elapsed(currentTime)/1000) >= IDLE_TIME) {
                // Great, we can process in working state.
                setStatus(Idle, currentTime);
            }
        }
    } else if (_status == Idle) {
        const bool sensorState = currentSensorState();
        if (sensorState) { // Alarm!
            _lastState = true;
            _lastEvent.start(currentTime);
            setStatus(Alarm, currentTime);
        }       
    } else if (_status == Alarm) {
        // Wait until the sensor goes back to idle state.
        const bool sensorState = currentSensorState();
        if (sensorState != _lastState) {
            _lastEvent.start(currentTime);
            _lastState = sensorState;           
        } else if (sensorState == false) {
            if ((_lastEvent.elapsed(currentTime)/1000) >= IDLE_TIME) {
                // Great, we can process in working state.
                setStatus(Idle, currentTime);
            }
        }
    }
}

The currentSensorState() reads the analog input and converts this input into a boolean value.

bool MotionSensor::currentSensorState() const
{
    const int sensorValue = analogRead(MOTION_SENSOR);
    const bool sensorState = (sensorValue > 200);
    return sensorState;
}

Every time the sensor changes its state, the setStatus checks for a callback and calls this callback with the current time and the new state.

void MotionSensor::setStatus(Status status, const unsigned long currentTime)
{
    if (_status != status) {
        _status = status;
        if (_callback != 0) {
            _callback(currentTime, status);
        }
    }
}

Conclusion

The MotionSensor class hides all the complexity of the sensor timing in a simple to use component. You get simple code using the callback which just has to contain the responses for a status change.

Continue here: Combine Everything