Skip to content

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);
}