Machine


Well – here’s the code for my “open the blinds when it’s daylight” project. It features time slicing – while the motor is winding the blinds, you can hit a button at any time to stop it.

Inputs:
PIN_UP, PIN_DN, PIN_ARM – buttons on my little control stick.
PIN_LDR – light sensor.

Outputs:
pin 13, of course. And pins A, A_, B and B_ which are the coils of the stepper.

A down on any button stops the motor.

A hold on PIN_UP/PIN_DN moves the motor until you release the button.

A hold on PIN_ARM arms and disarms the device.

A double-click on any button moves the blinds to the preset position for that button.

A triple-click sets the preset position for that button. This means that when setting, the motor will briefly activate (as if you had double-clicked), but meh.

Finally, the daylight sensor. If the device is armed, and A0 goes over the threshhold and stays over the threshold for a given time, then the motor moves the blinds to the position set on the ARM button.

The basic design is that each object has a loop() method which is called each time the main loop() runs. A boolean is returned to indicate if anything interesting happened. If nothing interesting happens for a while, then it would be reasonable to put the board to sleep, but meh – haven’t done this bit.

To set up:
Click and hold one of the UP/DN buttons to close the blinds. Triple-click to remember the closed position.
Click and hold the other UP/DN button to open the blinds. Triple-click to remember the open position, and also triple-click the ARM button.

At bedtime:
double-click the ‘close the blinds’ button to block out the light from that damn streetlight right outside my bedroom window. (If it isn’t already closed, which it will be due to privacy.)
click and hold the ‘arm’ button. The LED will flash once.

At dawn(ish), the blinds will open to let in the glorious morning sun and hopefully my circadian rhythm will finally get the message that it’s ok to wake up in the morning.

const byte INFO_LED = 13; // blue wire - also dives on-board LED

const byte A = 2;
const byte A_ = 3;
const byte B = 4;
const byte B_ = 5;

#define STEPPER_PINS(v) for(int v=2;v<=5;v++)
#define STEPPER_OFF STEPPER_PINS(stepperOut) digitalWrite(stepperOut, LOW);

const byte PIN_UP = 12; // red or black wire
const byte PIN_DN = 11; // red or black wire
const byte PIN_ARM = 10; // white wire

const byte  PIN_LDR = 0; // analog 0

const int LDR_THRESHHOLD = 300; // seems to be about right.
const int DAYLIGHT_TIME_MS = 1000 * 60 * 10; // 10 minutes

//#define DEBUG true

#ifdef DEBUG
#define info(x) _info(x)
#else
#define info(x)
#endif


// first - library code (in effect)

// An object that knows how to listen for button presses and releases
// and to invoke callbacks in response
// call loop() to make it go.

class Button {
  protected:

    byte pin;

#ifdef DEBUG
    void _info(const char *s) {
      Serial.print(name);
      Serial.print(": ");
      Serial.println(s);
    }
#endif

    virtual void dn() {
      info("DOWN");
      if (dn_callback) {
        dn_callback();
      }
    }

    virtual void up() {
      info("UP");
      if (up_callback) {
        up_callback();
      }
    }

  public:

    void (*dn_callback)() = NULL;
    void (*up_callback)() = NULL;
    char *name = "A Button";

    boolean buttonDown = false;

    void setup(byte _pin) {
      pin = _pin;
      pinMode(pin, INPUT_PULLUP);
      buttonDown = !digitalRead(pin);
    }

    boolean loop() {
      boolean newState = !digitalRead(pin);
      if (newState == buttonDown) return false;
      buttonDown = newState;
      if (buttonDown) dn(); else up();
      return true;
    }

};

// An object that knows how to listen for clicks, double-clicks, and holds
// and to invoke callbacks in response
// call loop() to make it go.

class Clicker: public Button {
  public:

    const int DOUBLE_CLICK_MS = 1000;
    const int HOLD_MS = 500;

    void (*click_callback)(int);
    void (*startHold_callback)(int);
    void (*hold_callback)();
    void (*endHold_callback)();

    boolean loop() {
      Button::loop();

      if (buttonDown) {
        if (holding) {
          hold();
        }
        else if (timeExceeded(HOLD_MS)) {
          startHold(clickCount);
          holding = true;
          clickCount = 0;
          hold(); // an initial hold to produce consistent behaviour
        }
        return true;
      }
      else {
        if (clickCount > 0 && timeExceeded(DOUBLE_CLICK_MS)) {
          clickCount = 0;
        }

        // if clickcount is greater than zero, we are in a possible double-click situation
        // so we want the loop to be running fast.
        return clickCount > 0;
      }
    }

  protected:

#ifdef DEBUG
    void _info(const char *s) {
      Serial.print(name);
      Serial.print(": ");
      Serial.println(s);
    }
#endif

    unsigned long dnMillis = 0L;
    boolean holding = false;
    int clickCount = 0;

    boolean timeExceeded(int t) {
      // millis may wrap, but the math works out
      return (millis() - dnMillis) >= t;
    }

    void dn() {
      Button::dn();
      dnMillis = millis();
      clickCount ++;
    }

    void up() {
      Button::up();
      if (holding) {
        holding = false;
        endHold();
      }
      else {
        click(clickCount);
      }
    }

    virtual void click(int clickCount) {
      info("CLICK");
      if (click_callback) {
        click_callback(clickCount);
      }
    }

    virtual void startHold(int clickCount) {
      info("HOLD ON");
      if (startHold_callback) {
        startHold_callback(clickCount);
      }
    }

    virtual void hold() {
      if (hold_callback) {
        hold_callback();
      }
    }

    virtual void endHold() {
      info("HOLD OFF");
      if (endHold_callback) {
        endHold_callback();
      }
    }
};

// A stepper motor that keeps track of location. It can move the motor to a target, or just start and stop.
// it will only do 5 minutes max of movement before stopping unconditionally
// call loop() to make it go.

class MyStepper {
  public:

    int location = 0;
    char *name = "Stepper";

    void setup() {
      STEPPER_PINS(v) pinMode(v, OUTPUT);
    }

    boolean loop() {
      // it's ok to leave lastMove uninitialized, because it's unsigned

      // 4ms = 250/s = 10/s turns = 600rpm, which is about the maximum

      if (millis() - lastMove < 5) return movingUp || movingDown || moveToTarget;

      lastMove = millis();

      if (movingUp) {
        location++;
      }
      else if (movingDown) {
        location--;
      }
      else if (moveToTarget) {
        if (location == target) {
          stopMoving();
          return true;
        }

        location += target > location ? 1 : -1;
      }
      else {
        return false;
      }

      if (tripFailsafe()) return true;

      // this code may cause both A and A_ to be turned on at the same time, but meh -
      // it's only for a microsecond or so. It isn't going to break anything.
      digitalWrite(A,   (location & 2) ? HIGH : LOW);
      digitalWrite(A_, !(location & 2) ? HIGH : LOW);
      digitalWrite(B,   ((location ^ (location >> 1)) & 1) ? HIGH : LOW);
      digitalWrite(B_, !((location ^ (location >> 1)) & 1) ? HIGH : LOW);

      return true;
    }

    void moveUp() {
      info("moveUp()");
      stopMoving();
      setFailSafe();
      movingUp = true;
    }

    void moveDown() {
      info("moveDown()");
      stopMoving();
      setFailSafe();
      movingDown = true;
    }

    void moveTo(int _target) {
      info("moveTo(_)");
      stopMoving();
      setFailSafe();
      moveToTarget = true;
      target = _target;
    }

    void stopMoving() {
      info("stopMoving()");
      movingUp = false;
      movingDown = false;
      moveToTarget = false;
      STEPPER_OFF;
    }


  protected:
    const unsigned long FAILSAFE_MS = 1000 * 60 * 5; // 5 minutes max time running the motor

    unsigned long failsafe = 0;
    unsigned long lastMove = 0;
    boolean movingUp = false;
    boolean movingDown = false;
    boolean moveToTarget = false;
    int target = 0;

#ifdef DEBUG
    void _info(const char *s) {
      Serial.print(name);
      Serial.print(": ");
      Serial.println(s);
    }
#endif

    void setFailSafe() {
      failsafe = millis();
    }

    boolean tripFailsafe() {
      if (millis() - failsafe < FAILSAFE_MS) return false;
      if (!movingUp && !movingDown && !moveToTarget) return false;
      stopMoving();
      return true;
    }

};

// the control objects and their event handlers.
// the control schema is:
// if the 'set' button is on, then a click on up/down/arm will set the corresponding setpoint
// it the 'set' button is not down
// buttons up and dn will move the motor on a hold and move to the setpunt on a dclick
// single-click does nothing. It's just an 'oops'.
// arm button moves to the arm setpoint on a dclick, and arms/disarms on a hold.
// on an arm, the onboard led is flashed once. on a disarm, it's flashed three times.

MyStepper stepper;
Clicker buttonUp;
Clicker buttonDn;
Clicker buttonArm;

boolean armed = false;
int upSetpoint;
int dnSetpoint;
int armSetpoint;
boolean daylight = false;
unsigned long daylight_t = 0;


#ifdef DEBUG
void _info(const char *s) {
  Serial.print(": ");
  Serial.println(s);
}
#endif

void buttonUp_click(int clickCount) {
  info("click");
  if (clickCount == 2) {
    info("2 clicks. moving up");
    stepper.moveTo(upSetpoint);
  }
  else if (clickCount == 3) {
    info("3 clicks. Setting up");
    upSetpoint = stepper.location;
  }
}

void buttonUp_startHold(int clickCount) {
  stepper.moveUp();
}

void buttonUp_endHold() {
  info("stop moving the stepper");
  stepper.stopMoving();
}

void buttonDn_click(int clickCount) {
  if (clickCount == 2) {
    stepper.moveTo(dnSetpoint);
    info("2 clicks. movto dn.");
  }
  else if (clickCount == 3) {
    dnSetpoint = stepper.location;
    info("3 clicks. Setting dn");
  }
}

void buttonDn_startHold(int clickCount) {
  stepper.moveDown();
}

void buttonDn_endHold() {
  stepper.stopMoving();
}

void buttonArm_click(int clickCount) {
  if (clickCount == 2) {
    stepper.moveTo(armSetpoint);
  }
  else if (clickCount == 3) {
    armSetpoint = stepper.location;
  }
}

void buttonArm_endHold() {
  armed = !armed;
  info_flash(armed ? 1 : 3);
}

void stopStepper() {
  stepper.stopMoving();
}

void setup() {
  pinMode(INFO_LED, OUTPUT);

#ifdef DEBUG
  Serial.begin(9600);
#endif


  buttonUp.setup(PIN_UP);
  buttonDn.setup(PIN_DN);
  buttonArm.setup(PIN_ARM);
  stepper.setup();

  buttonUp.name = "BtnUp";
  buttonUp.click_callback = buttonUp_click;
  buttonUp.startHold_callback = buttonUp_startHold;
  buttonUp.endHold_callback = buttonUp_endHold;

  buttonDn.name = "BtnDn";
  buttonDn.click_callback = buttonDn_click;
  buttonDn.startHold_callback = buttonDn_startHold;
  buttonDn.endHold_callback = buttonDn_endHold;

  buttonArm.name = "BtnArm";
  buttonArm.click_callback = buttonArm_click;
  buttonArm.endHold_callback = buttonArm_endHold;

  // all buttons, irrespective of anything else they do, stop
  // the stepper when they are hit.

  buttonUp.dn_callback = stopStepper;
  buttonDn.dn_callback = stopStepper;
  buttonArm.dn_callback = stopStepper;

}

// don't need a class to encapsulate this: it's pretty simple
boolean checkLightSensor() {
  if (!armed) return false; // nothing to see, here.

  boolean _daylight = analogRead(PIN_LDR) > LDR_THRESHHOLD;

  if (!_daylight) {
    if (!daylight) {
      // still naptime
      return false;
    }
    else {
      info("Just a passing car");
      // just a passing car.
      daylight = false;
      return true;
    }
  }
  else {
    // ok. it's daylight now, and we are armed.

    if (!daylight) {
      info("Daylight sensed");
      // this was the first sensing
      daylight = true;
      daylight_t = millis();
      return true;
    }
    else if (millis() - daylight_t < DAYLIGHT_TIME_MS ) {
      // waiting to see if the sun remains out for a bit
      return false;
    }
    else {
      info("It's daytime. Moving.");
      // we are armed, it's daylight, and it's been daylight for some time
      info_flash(10);
      armed = false;
      stepper.moveTo(armSetpoint);
      return true;
    }
  }

}

void loop() {
  // put your main code here, to run repeatedly:

  boolean somethingHappened = false;

  somethingHappened = buttonUp.loop() || somethingHappened;
  somethingHappened = buttonDn.loop() || somethingHappened;
  somethingHappened = buttonArm.loop() || somethingHappened;
  somethingHappened = stepper.loop() || somethingHappened;
  somethingHappened = checkLightSensor() || somethingHappened;

  if (!somethingHappened) {
    // TODO! Put the arduino to sleep, wake on interrupt.
    // for now - meh. It's ok to run a tight loop.
  }

}

void info_flash(int n) {
  while (n--) {
    digitalWrite(INFO_LED, HIGH);
    delay(200);
    digitalWrite(INFO_LED, LOW);
    delay(200);
  }
  delay(200); // to demarcate the phrase
}

Leave a comment