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.
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
- locus-pocus-secondary - creating and running the secondary clockwork control
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 |
|---|
| // ======================================================
// Locus Pocus Secondary - Clock Device / Motor Control
// ======================================================
/*
ClockMotor.h -
Struct representing Locus Pocus clock motor as part of the clock device,
providing for definition of motor characteristics to be used by the
device.
Copyright (C) 2025 - David Wilson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#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 |
|---|
| // ======================================================
// Locus Pocus Secondary - Clock Device / Motor Control
// ======================================================
/*
ClockHand.cpp -
Class representing Locus Pocus clock hand as part of the clock device,
providing for setup and management of a clock hand and associated
movement driven by a stepper motor in response to clock commands.
Copyright (C) 2025 - David Wilson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#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 |
|---|
| // ======================================================
// Locus Pocus Secondary - Clock Device / Motor Control
// ======================================================
/*
ClockFace.cpp -
Class representing Locus Pocus clock face as part of the clock device,
providing for setup and management of a clock face with designated
semantic positions.
Copyright (C) 2025 - David Wilson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ClockFace.h"
//ClockFace::ClockFace ()
//{
//
//}
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;
}
}
void ClockFace::print_positions()
{
Serial.print(_cfp.size());
Serial.println(" Clock Face Positions");
for (ClockFacePosition pos : _cfp)
{
Serial.print(pos.name.c_str());
Serial.print(" ");
Serial.println(pos.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 |
|---|
| // ======================================================
// Locus Pocus Secondary - Clock Device / Motor Control
// ======================================================
/*
Clock.cpp -
Class representing Locus Pocus clock device, providing for setup of clock
device components (face, hands, motor), and translating clock commands into
motor control for clock hand actions.
Copyright (C) 2025 - David Wilson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#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());
}
}
|
Locus Pocus Secondary Control
The Locus Pocus secondary control application is responsible for managing overall secondary application / motor control for clock components.
The main functionality is:
- create a clock face with specified positions
- create a set of clock hands driven by specified motors
- create a clock device for overall management
- setup a SafeString reader for non-blocking serial command interaction
- setup BLE application service / characteristic
- setup callback functions for responding to BLE events (connection, clock command receipt)
- run loops for processing motor / BLE / safestring actions
| locus-pocus-secondary.ino |
|---|
| // ======================================================
// Locus Pocus Secondary - Clock Device / Motor Control
// ======================================================
/*
locus-pocus-secondary.ino -
secondary locus pocus clock device / motor control code for connecting to
primary application control via BLE, receiving commands from primary application
control via BLE, traslating commands to clock device / motor actions, and
operationalizing the clock commands.
Copyright (C) 2025 - David Wilson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#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());
}
|