Skip to content

Secondary Control - Clockwork Application Code

Application control for the clockwork component is fundamentally about:

  • Controlling the stepper motors to position the clock hands
  • Communication to receive application instructions

Application control for the clockwork mechanism was created using Arduino toolchains and workflows - the Arduino IDE with C/C++ programming and libraries.

Research

In selecting approaches for stepper motor control, I considered different potential options. For communication, I selected BLE based on my earlier Fab Academy assignment experience.

My Earlier Fab Academy Projects

As part of Mechanical Design, Machine Design week, we worked with 28BYJ-48 stepper motors for a basic 3D printed drawing machine. The initial exploration used the Arduino Stepper library, which was a consideration.

For communication, I had previously worked with Bluetooth® Low Energy (BLE) in Embedded Networking & Communications, which was a consideration.

Prior Projects

For stepper motor control, I reviewed prior projects that had used stepper motors, to consider options. There were two primary options - direct motor control and use of the AccelStepper library.

Project Stepper Control Approach
Modern Weasley Clock Direct Coded Control
Where'sLy Clock Project AccelStepper Library
Magic-Clock AccelStepper Library

Reference

In working with the 28BYJ-48 stepper motors, I found the reference of Control 28BYJ-48 Stepper Motor with ULN2003 Driver & Arduino to be very helpful. This covered use of both the Arduino Stepper library and the AccelStepper library.

Selection

Based on my research, for controlling stepper motors, the main choice was between the Arduino Stepper library and the AccelStepper library. I had used the Arduino Stepper library as part of previous Fab Academy assignments and as the enabling library for the clockwork testing code. The AccelStepper library, however, had a key advantage in being able to control multiple motors simultaneously.

In conducted some basic control testing with each library. The AccelStepper library seems to have some internal overhead and is not able to match the fastest revolutions per minute available with the Arduino Stepper library for the 28BYJ-48 motors. But the AccelStepper library provided reasonable speed for the clock hand application and it enabled multiple motor control. So, I decided to move forward with the AccelStepper library.

For communication, I had a good previous experince with the Arduino Bluetooth® Low Energy (BLE) in Embedded Networking & Communications, so I moved foward with BLE communication.


Locus Pocus - Secondary Clockwork Control Design

For the Locus Pocus project, I selected the AccelStepper library for motor control and the ArduinoBLE library for Bluetooth® Low Energy communication.

In developing Locus Pocus, I used previous approaches as points of reference, but I created all of the clockwork control code myself.

I adopted an object-oriented approach, breaking clock functionality down into:

  • Clock Motor - containing motor detail for specific motor types
  • Clock Hand - responsible for motor control to drive the hands
  • Clock Face - responsible for managing available face positions
  • Clock - bringing together and managing component functionality

Application control for the clockwork mechanism was created using Arduino toolchains and workflows - the Arduino IDE with C/C++ programming and libraries.


Clock Motor

While fairly straightforward, I separated out a clock motor type to act as a container for key motor characteristics. Currently this is only the number of steps for the motor, but could be used to refine motor use, or use different types of motors.

ClockMotor.h
#ifndef ClockMotor_H
#define ClockMotor_H

struct motor_t
{
  int stepsPerRevolution;
};

const motor_t MOTOR_28BYJ_48 = { 2048 };

#endif

Clock Hand

The clock hand class encapsulates functionality for the clock hands. Effectively, this is about motor control for turning the clock hands. It inherits from the AccelStepper class provided by the library, so is a stepper motor controller wrapped with additional, clock-specific functions for moving to specific positions.

The main functionality is:

  • moveTo() - move to a specific stepper position in terms of steps
  • moveToDegree() - move to a degree position on a 360 degree circle - this considers the top of the circle to be 0
  • calibrate() - supports manual calibration for this hand - will make 2 complete revolutions - if data is entered, it marks that point as the new home (zero point)
ClockHand.cpp
#include "ClockHand.h"

ClockHand::ClockHand(std::string name, motor_t motor, bool motorOrientation, uint8_t pin1, uint8_t pin2, uint8_t pin3, uint8_t pin4)
  : AccelStepper(AccelStepper::FULL4WIRE, pin1, pin2, pin3, pin4) {
  this->setMaxSpeed(1000.0);
  this->setAcceleration(100.0);
  this->setSpeed(1000);
  this->name = name;
  _motor = motor;
  _motorOrientation = motorOrientation;
}

void ClockHand::moveTo(long motorPositionInSteps) {
  AccelStepper::moveTo((_motorOrientation ? 1 : -1) * motorPositionInSteps);
}

void ClockHand::moveToDegree(int positionInDegrees) {
  double degreeToStepScale = static_cast<double>(positionInDegrees) / 360.0;
  double motorPositionInSteps = degreeToStepScale * _motor.stepsPerRevolution;
  this->moveTo(static_cast<long>(motorPositionInSteps));  
}

void ClockHand::calibrate(Stream& io) {
  io.print("Calibrating: ");
  io.print(this->name.c_str());
  io.println(" [Enter] to set base position (max 2 rotations to try)...");
  io.flush();
  delay(1000);
  this->moveTo(AccelStepper::currentPosition() + 2*_motor.stepsPerRevolution);
  while (AccelStepper::distanceToGo() != 0)
  {
    AccelStepper::run();
    if (io.available())
    {
      AccelStepper::setCurrentPosition(0);
      while (io.available()) {
        Serial.read();
      }
    }
  }
}

Clock Face

The clock face class encapsulates functionality for managing the defined positions on a clock face.

The main functionality is:

  • getPosition() - given a string naming a hand position, return the degree position (on a 360 degree circle, where 0 is at the top) or -1 if there is no such named hand position on this face
  • getPositionRandom() - returns a random position from the clock face
ClockFace.cpp
#include "ClockFace.h"

ClockFace::ClockFace (std::vector<ClockFacePosition> positions)
{
  _cfp = positions;
  std::srand(std::time({})); // use current time as seed for random generator
}

int ClockFace::getPosition(std::string name)
{
  for (ClockFacePosition pos : _cfp)
  {
    if (pos.name == name)
    {
       return pos.degree;
    }
  }
  return -1;
}

int ClockFace::getPositionRandom()
{
  int numFacePositions = _cfp.size();
  if (numFacePositions == 0)
  {
    return rand() % 361;
  }
  else
  {
    int position = rand() % numFacePositions;
    return _cfp[position].degree;
  }
}

Clock

The clock class encapsulates functionality for the clock as a whole. It is constructed with a specific clock face and set of clock hands.

Clock commands are given in a "target:action" format, where the target represents the name associated with a hand (or the "clock" itself), and the action represents the goal for the named hand/clock. Typically the action is the name of a clock face position. For example, "h1:home" would indicate that the hand named "h1" should move to the "home" position on the clock face.

The main functionality is:

  • set() - given a single command string, split it into target / action and call the 2-string version
  • set() - given a target string and action string, check whether it is for the clock itself (if so, move to taking a clock action), or for one of the hands (if so, pass the action to the named hand)
  • tick() - needs to be called every loop() of the main application, passthrough for underlying motor control of the AccelStepper library
  • manage() - dispatches to functions for named clock actions
  • calibrate() - calibrates each hand in turn
  • facelift() - moves the hands to a horizontal position to enable changing the clock face
  • randomize() - moves each hand to a random position on the clock face
Clock.cpp
#include "Clock.h"

Clock::Clock(ClockFace& face, std::vector<ClockHand>& hands) : _face(face), _hands(hands)
{
   _io = nullptr;
}

void Clock::set(std::string handAndPosition)
{
  std::string cmd = handAndPosition;
  std::istringstream sstream(cmd);
  std::string token;
  std::vector<std::string> tokens;
  while (std::getline(sstream, token, ':'))
  {
    tokens.push_back(token);
  }
  if (tokens.size() > 1)
  {
    this->set(tokens[0],tokens[1]);
  }
}

void Clock::set(std::string handName, std::string toPosition)
{
  if (handName == "clock")
  {
    this->manage(toPosition);
  }
  else
  {
    for (size_t i = 0; i < _hands.size(); ++i)
    {
      if (_hands[i].name == handName)
      {
        _hands[i].moveToDegree(_face.getPosition(toPosition));
      }
    }
  }
}

void Clock::tick()
{
  for (size_t i = 0; i < _hands.size(); ++i)
  {
    _hands[i].run();
  }
}

void Clock::setIO(Stream &io)
{
  _io = &io;
}

void Clock::manage (std::string operation)
{
  if (operation == "calibrate")
  {
    this->calibrate();
  }
  else if (operation == "facelift")
  {
    this->facelift();
  }
  else if (operation == "randomize")
  {
    this->randomize();
  }  
}

void Clock::calibrate ()
{
  if (_io != nullptr)
  {
    for (size_t i = 0; i < _hands.size(); ++i)
    {
      _hands[i].calibrate(*_io);
    }
  }
}

void Clock::calibrate (Stream& io)
{
  for (size_t i = 0; i < _hands.size(); ++i)
  {
    _hands[i].calibrate(io);
  }
}

void Clock::facelift ()
{
  for (size_t i = 0; i < _hands.size(); ++i)
  {
     _hands[i].moveToDegree(90);
  }
}

void Clock::randomize ()
{
  for (size_t i = 0; i < _hands.size(); ++i)
  {
     _hands[i].moveToDegree(_face.getPositionRandom());
  }
}

Main Application

The main application sets up the clock and communication functions. It loops to check for input from the serial monitor, and calls the functions needed to drive the ArduinoBLE and AccelStepper library functionality.

  • The SafeStringReader library is used to enable non-blocking reads from the serial monitor input.
  • BLE communication sets up a service (with UUID) and writeable control characteristic (with UUID) to receive clock commands over BLE communication
clockdriver.ino
#include "ClockHand.h"
#include "ClockFace.h"
#include "Clock.h"

#include "SafeStringReader.h"

#include <ArduinoBLE.h>

// Global Setup for Clock
ClockFace face = ClockFace({{"home", 22}, {"peril", 67}, {"transit", 112}, {"lost", 157}, 
                            {"work", 202}, {"prison", 247}, {"shopping", 292},{"holidays", 337}});
std::vector<ClockHand> hands =   
{
    ClockHand ("h1", MOTOR_28BYJ_48, ClockHand::ANTI_CLOCKWISE,2, 4, 3, 5),
    ClockHand ("h2", MOTOR_28BYJ_48, ClockHand::ANTI_CLOCKWISE, 6, 8, 7, 9),
    ClockHand ("h3", MOTOR_28BYJ_48, ClockHand::CLOCKWISE, 10, 12, 11, 13),
    ClockHand ("h4", MOTOR_28BYJ_48, ClockHand::ANTI_CLOCKWISE, A0, A2, A1, A3)
};
Clock theClock = Clock(face, hands);

// Global Setup for Safe String 
createSafeStringReader(sfReader,50,"\r\n");

// Global Setup for Arduino BLE
BLEService clockService("19B10000-E8F2-537E-4F6C-D104768A1214"); // create service
BLEStringCharacteristic clockCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite, 20);

void setup() {
  setupSerialMonitor();
  setupBLE();
  setupSafeString();
  theClock.setIO(Serial);
}

void loop() {
  // Non-Blocking Safe String Read
  if (sfReader.read())
  {
    sfReader.trim();
    theClock.set(sfReader.c_str());
  }

  BLE.poll();
  theClock.tick();
}

void setupSerialMonitor () {
  // Serial Monitor Setup
  for (int timer = 0; timer < 1000; timer++)
  {
    if (Serial)
    {
      Serial.begin(9600);
      delay(1000);
      Serial.println();
      Serial.println("Arduino Serial Monitor started...");
      break;
    }
    delay(10);
  }
}

void setupBLE () {
  for (int timer = 0; timer < 1000; timer++)
  {
    if (BLE.begin())
    {
      Serial.println("Arduino BLE started...");
      BLE.setLocalName("Clock");
      clockService.addCharacteristic(clockCharacteristic);
      clockCharacteristic.setValue("Test");
      BLE.setEventHandler(BLEConnected, bleConnectHandler);
      BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);
      clockCharacteristic.setEventHandler(BLEWritten, clockCharacteristicWritten);
      BLE.addService(clockService);
      BLE.setAdvertisedService(clockService);
      BLE.advertise();
      Serial.println("Bluetooth® device active, waiting for connections...");
      return;
    }
    delay(10);
  }
}

void setupSafeString () {
  SafeString::setOutput(Serial);
  sfReader.setTimeout(1000);
  sfReader.flushInput();
  sfReader.echoOn();
  sfReader.connect(Serial);
}

void bleConnectHandler(BLEDevice central) {
  // central connected event handler
  BLE.stopAdvertise();
  Serial.print("Connection made with central device: ");
  Serial.println(central.address());
}

void blePeripheralDisconnectHandler(BLEDevice central) {
  // central disconnected event handler
  Serial.print("Disconnected from central device: ");
  Serial.println(central.address());
  BLE.advertise();
}

void clockCharacteristicWritten(BLEDevice central, BLECharacteristic characteristic) {
  // central wrote new value to characteristic, update clock
  Serial.print("Clock characteristic written: ");
  Serial.println(clockCharacteristic.value());
  theClock.set(clockCharacteristic.value().c_str());
}