Primary Control - Location Data Management & Clock Control Commands
Application control for location data management is fundamentally about:
- Communicating with the data broker (Adafruit IO) over WiFi to receive location events
- Processing location events to create specific clock commands
- Communicating with secondary clockwork control over Bluetooth® Low Energy (BLE) to send specific clock commands
Application control for primary control was created using Arduino toolchains and workflows - the Arduino IDE with C/C++ programming and libraries.
Research
I selected Adafruit IO as the Data Broker for the application. Adafruit IO provides an Adafruit IO Arduino library for interacting with the service. To manage the connection, I reviewed example documentation from the library, including the Adafruit IO Examples.
Since the focus is on receiving events, I used the basic Adafruit IO Subscribe Example for subscribing to a data feed and receiving events.
Locus Pocus - Primary Application Control Design
The foundational application control is based on the Adafruit IO Subscribe Example. I have extensively modified the code for:
- Customized the feed detail
- Added clearer status instrumentation / messaging for connections
- Added ArduinoBLE code for embedded networking communication with the clockwork controller
- Added message processing module for analyzing incoming messages from the Adafruit IO feed and creating the appropriate clock command
- Added SafeStringReader functionality to enable non-blocking reads from the serial monitor
- Added onboard LED output status indicator - showing Adafruit IO connection process
- Added onboard Button control - currently set for for demonstration mode - concurrently moving all 4 hands of the clock to various positions
| locus-pocus-primary.ino |
|---|
| // Adafruit IO Subscription Example
//
// Adafruit invests time and resources providing this open source code.
// Please support Adafruit and open source hardware by purchasing
// products from Adafruit!
//
// Written by Todd Treece for Adafruit Industries
// Copyright (c) 2016 Adafruit Industries
// Licensed under the MIT license.
//
// All text above must be included in any redistribution.
/************************ Clock Application *********************************/
// Clock Application Code by David Wilson
// Based on Original Adafruit IO Subscription Example at:
// https://github.com/adafruit/Adafruit_IO_Arduino/tree/master/examples/adafruitio_01_subscribe
// Clock Application Incorporates
// - Arduino BLE connection to clock peripheral
// - Safe String Reader for non-blocking reads
/************************** Configuration ***********************************/
// edit the config.h tab and enter your Adafruit IO credentials
// and any additional configuration needed for WiFi, cellular,
// or ethernet clients.
#include "config.h"
// Using Arduino Libraries
// ArduinoBLE - Bluetooth BLE Connectivity
// SafeStringReader - Non-blocking read from Serial Monitor
// Pushbutton - Button management with debouncing
#include <ArduinoBLE.h>
#include "SafeStringReader.h"
#include <Pushbutton.h>
/************************ Example Starts Here *******************************/
// set up the 'clock' data feed
AdafruitIO_Feed *clockdata = io.feed("clock");
// Define XIAO ESP32C3 Development Board Pins in Use
const int onboardLEDPin = D6;
const int onboardButtonPin = D7;
// Set up the pushbutton
Pushbutton onboardButton(onboardButtonPin);
// Globals for clock peripheral and safe string reader
BLEDevice clockPeripheral;
createSafeStringReader(sfReader,50,"\r\n");
// BLE Service Identifiers
#define CLOCK_PERIPHERAL_UUID "19b10000-e8f2-537e-4f6c-d104768a1214"
#define CLOCK_CHARACTERISTIC_UUID "19b10001-e8f2-537e-4f6c-d104768a1214"
void setup() {
// Start the Arduino Serial Monitor
Serial.begin(9600);
// Set Onboard LED pin as Output
// Set Onboard Button pin as Input
pinMode(onboardLEDPin, OUTPUT);
// Initialize LED to Off
digitalWrite(onboardLEDPin,LOW);
// Application Header
Serial.println();
Serial.println("================================");
Serial.println(" Starting Primary Clock Control");
Serial.println("================================");
// start MQTT connection to io.adafruit.com
Serial.println("Connecting to data broker - Adafruit IO");
io.connect();
// set up a message handler for the count feed.
// the handleMessage function (defined below)
// will be called whenever a message is
// received from adafruit io.
clockdata->onMessage(handleMessage);
// wait for an MQTT connection
// NOTE: when blending the HTTP and MQTT API, always use the mqttStatus
// method to check on MQTT connection status specifically
int progressCount = 1;
while(io.mqttStatus() < AIO_CONNECTED) {
Serial.print(".");
if ((progressCount%50)==0)
{
Serial.println();
}
progressCount++;
digitalWrite(onboardLEDPin,HIGH);
delay(500);
digitalWrite(onboardLEDPin,LOW);
delay(500);
}
digitalWrite(onboardLEDPin,HIGH);
delay(3000);
digitalWrite(onboardLEDPin,LOW);
// Because Adafruit IO doesn't support the MQTT retain flag, we can use the
// get() function to ask IO to resend the last value for this feed to just
// this MQTT client after the io client is connected.
clockdata->get();
// we are connected
Serial.println();
Serial.println(io.statusText());
// Initialize Safe String Reader
setupSafeStringReader();
// Initialize BLE
setupBLE();
// Start scanning for clock peripheral
Serial.println("Scanning for Clock BLE peripheral ...");
BLE.scanForUuid(CLOCK_PERIPHERAL_UUID);
}
void loop() {
// Non-Blocking Safe String Read - Check for Manual Data
if (sfReader.read())
{
sfReader.trim();
sendClockMessage(sfReader.c_str());
}
// If the pushbutton is pressed, randomize the clock hands
if (onboardButton.getSingleDebouncedPress())
{
sendClockMessage("clock:randomize");
}
// io.run(); is required for all sketches.
// it should always be present at the top of your loop
// function. it keeps the client connected to
// io.adafruit.com, and processes any incoming data.
io.run();
// BLE.poll keeps BLE connection & events active
BLE.poll();
}
// this function is called whenever a 'clock' message
// is received from Adafruit IO. it was attached to
// the clock feed in the setup() function above.
void handleMessage(AdafruitIO_Data *data) {
// Get broker data as string and process message
sendClockMessage(data->value());
}
void bleDisconnectHandler(BLEDevice peripheral)
{
Serial.println();
Serial.print("Disconnected from Peripheral: ");
Serial.println(peripheral.address());
Serial.println("... resuming scan for clock peripheral");
BLE.scanForUuid(CLOCK_PERIPHERAL_UUID);
}
void blePeripheralConnectHandler(BLEDevice peripheral) {
// central connected event handler
Serial.print("Connected to Peripheral: ");
Serial.println(peripheral.address());
clockPeripheral = peripheral;
}
void bleCentralDiscoverHandler(BLEDevice peripheral) {
// discovered a peripheral
Serial.print("Discovered Peripheral: ");
Serial.println(peripheral.address());
// Stop scanning
BLE.stopScan();
// Connect to peripheral
Serial.println("... connecting to discovered peripheral ...");
if (!peripheral.connect())
{
Serial.println("... failed to connect! Resuming peripheral scan.");
BLE.scanForUuid(CLOCK_PERIPHERAL_UUID);
}
}
void setupBLE () {
// Try starting BLE communication
// Use a loop to timeout, so program doesn't hang forever on fail
// Try every 1/10 second for 2 minutes
Serial.print("Starting Arduino BLE Communication for Clock Control ... ");
for (int timer = 0; timer < 1200; timer++)
{
if (BLE.begin())
{
// BLE communication active - set up event handlers
BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
BLE.setEventHandler(BLEDisconnected, bleDisconnectHandler);
BLE.setEventHandler(BLEDiscovered, bleCentralDiscoverHandler);
// Notification for successful startup
Serial.println("started ok!");
return;
}
// Wait 1/10 second before trying again
delay(100);
}
// Notificaton if BLE startup process times out
Serial.println("failed!");
}
void sendClockMessage(const char* data) {
// Show full message data
Serial.println("Received location data from Broker");
Serial.print(" <- ");
Serial.println(data);
// Parse message data and show potential clock message
String msg = parseClockMessage(data);
int msgLen = msg.length();
Serial.print(" -- Clock message would be: ");
Serial.println(msg);
// Default BLE message size is 20 bytes, don't send longer messages
// Need to account for string termination character '\0' as part of message
if (msgLen >= 20)
{
Serial.println(" -- Notification data too long (20 byte limit) - not sent to Clock BLE");
return;
}
// Can't send BLE message, if not connected to receiver
if (!BLE.connected())
{
Serial.println(" -- Clock Peripneral not BLE connected - ignoring this message");
return;
}
// Message size ok, receiver connected - try sending message data via BLE
Serial.println(" -- sending update to Clock Peripheral via BLE");
clockPeripheral.discoverAttributes();
BLECharacteristic clockCharacteristic = clockPeripheral.characteristic(CLOCK_CHARACTERISTIC_UUID);
// BLE messages are sent as bytes
byte buffer[20];
msg.getBytes(buffer, 20);
Serial.print(" --> ");
Serial.println((char *)buffer);
clockCharacteristic.writeValue(buffer,msgLen+1);
}
// Clock messages have a format of:
//
// target:action@qualifier*extra
//
// target - focus of the action - typically person/hand or clock command
// : - delimiter, typically coupling target and action
// action - what to do with/for target - typically entering a location
// @ - delimiter, typically coupling action with additional detail
// qualifier - typically for a location, as entered or exited
// * - delimiter, setting off extra detail in messages
//
// h1:shopping@entered*November 8, 2025 at 01:51PM
String parseClockMessage (const char* message)
{
String messageString = String(message);
int delimiterIndex = messageString.indexOf('*');
int targetIndex = messageString.indexOf(':');
int enterExitIndex = messageString.indexOf('@');
// If delimiter present, clear extra message content
if (delimiterIndex != -1)
{
messageString.remove(delimiterIndex);
}
messageString.trim();
String target = String();
String action = String();
String enterExit = String();
String msg = String();
// Get target
if (targetIndex != -1)
{
target = messageString.substring(0,targetIndex);
}
else if (enterExitIndex != -1)
{
target = messageString.substring(0,enterExitIndex);
}
else
{
target = messageString.substring(0);
}
target.trim();
// Get action
if ((targetIndex != -1) && (enterExitIndex != -1))
{
action = messageString.substring(targetIndex+1,enterExitIndex);
}
else if (targetIndex != -1)
{
action = messageString.substring(targetIndex+1);
}
action.trim();
// Get whether entering or exiting
if (enterExitIndex != -1)
{
enterExit = messageString.substring(enterExitIndex+1);
}
enterExit.trim();
if (target.length() > 0)
{
msg.concat(target);
}
if (targetIndex != -1)
{
msg.concat(':');
}
if (enterExit == "exited")
{
msg.concat("transit");
}
else if (action.length() > 0)
{
msg.concat(action);
}
return msg;
}
// Initialize Safe String Reader
void setupSafeStringReader () {
SafeString::setOutput(Serial);
sfReader.setTimeout(1000);
sfReader.flushInput();
// sfReader.echoOn();
sfReader.connect(Serial);
}
|