Kyelo Torres
Published

PaceLight

The PaceLight makes running simple. All you have to do is input the desired parameters and follow the colors on your wrist!

IntermediateShowcase (no instructions)10 hours615
PaceLight

Things used in this project

Hardware components

ESP32S
Espressif ESP32S
×1
Adafruit GPS Featherwing
×1
Adafruit Neopixel LED strip 0.5m Long
×1
Adafruit Lithium Ion Polymer Battery - 3.7v 500mAh
×1
Velcro Strap
×1
M3 Cap Head Screws
OpenBuilds M3 Cap Head Screws
×2

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)
Hot glue gun (generic)
Hot glue gun (generic)

Story

Read more

Custom parts and enclosures

Case Body

3D print it

Schematics

PaceLight Presentation and Circuit Diagram

The pdf explains the problems of keeping on pace, the benefits of using PaceLight, the components used, the data flow diagram, the circuit diagram (pg 22), and future improvements.

Code

paceLightEventually.ino

Arduino
This is the main function. Upload this function to your esp32. You will need to change the wifi credentials on lines 13-14. It is best to use a hotspot on your phone and run with your phone. You will need to create a separate MQTT and establish the feed names (line 38-42) to run this code. Lastly, you will need to use IFTTT to create a button that will send data to the paceLight_Control feed.

To use:
1. text in your tolerance (pace range you want to stay in), pace (pace you want to maintain), period (period that lights changes to update you on your pace)
Example: I want to be in a tolerance of 2min/mile, pace of 10min/mile and sample every 15 seconds.
#tol 2 #pace 10 #period 15

2. Press the IFTTT button to begin sampling the GPS data

3. Run and follow the lights.
Blinking Red - Too Slow
Red - Slow
Green - On Pace
Blue - Fast
Blinkiing Blue - Too Fast
/***************LIBRARY INCLUSION****************/
// bring in libraries
#include <WiFi.h>
#include <Adafruit_GPS.h>
#include <Adafruit_MQTT.h>
#include <Adafruit_MQTT_Client.h>
#include <Eventually.h>
#include <NeoPixelBus.h>
#define colorSaturation 128

/***************WIFI CREDENTIALS****************/
// assign wifi credentials
const char* ssid = "logisticnexus";
const char* password = "lcpl6414";

/**************MQTT CREDENTIALS*****************/
// assign MQTT credentials
#define aio_server "io.adafruit.com"
#define aio_serverport 1883
#define aio_username  "jskelley"
#define aio_key       "ac57937d116a444089f1c500e3310459"

/**************GPS SERIAL*********************/
// assign, connect to GPS serial
#define GPSSerial Serial2
Adafruit_GPS GPS(&GPSSerial);

//disable data echo to serial
#define GPSECHO false

EvtManager mgr;

// this establishes method for data xmission, receive
WiFiClient client;

// Establish mqtt object, and subscribe / publish channels
Adafruit_MQTT_Client mqtt(&client, aio_server, aio_serverport, aio_username, aio_key);
Adafruit_MQTT_Subscribe ctrl = Adafruit_MQTT_Subscribe(&mqtt, aio_username "/feeds/paceLight_Control");
Adafruit_MQTT_Subscribe pace = Adafruit_MQTT_Subscribe(&mqtt, aio_username "/feeds/paceLight_pace");
Adafruit_MQTT_Subscribe period = Adafruit_MQTT_Subscribe(&mqtt, aio_username "/feeds/paceLight_period");
Adafruit_MQTT_Subscribe tolerance = Adafruit_MQTT_Subscribe(&mqtt, aio_username "/feeds/paceLight_tolerance");
Adafruit_MQTT_Publish dataz = Adafruit_MQTT_Publish(&mqtt, aio_username "/feeds/paceLight_data");

bool stateHIGH = LOW;
bool stateMEDIUM = LOW;
const uint16_t PixelCount = 30;
const uint8_t PixelPin = 4;
NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod> strip(PixelCount, PixelPin);
bool c = 0; // c tracks logging state, 1 = logging, 0 = not logging
double P; // P is user-input desired pace
int f; // f is user-input desired feedback period (seconds between update)
double tol; // tol represents size of tolerance bands between zones
double D = 1.15078; // D is the conversion factor between knots and MPH
double B;
double Bt = 0;
double dP;
double t_mqtt = 1000;  // Time between MQTT data read / connection check
double t_read = 1; // Time between GPS read events
double t_parse = 2000; // Time between GPS parse events
double t_ping = 150000; // Time between MQTT ping events
int count;
int nSplit;
bool geeps_status;
bool wifi_status = 0;
int Pbool = 0;
int fbool = 0;
int tolbool = 0;
RgbColor red(colorSaturation, 0, 0);
RgbColor green(0, colorSaturation, 0);
RgbColor blue(0, 0, colorSaturation);
RgbColor white(colorSaturation);
RgbColor black(0);
RgbColor yellow(colorSaturation,colorSaturation,0);
RgbColor purple(colorSaturation, 0, colorSaturation);

/*****************COLOR WIPE*******************/
// Fill the ALL dots with a color
void colorWipe(RgbColor c,uint32_t start,uint32_t num) {
  for(uint16_t i=start; i<(num-1); i++) {
    strip.SetPixelColor(i, c);
  }
  strip.Show();
}

/****************BLINK MEDIUM****************/
bool blinkmeMEDIUM() {
  stateMEDIUM = !stateMEDIUM; // Switch light states
  if (c == 0 || geeps_status == 0){
    colorWipe(black,5,31); // OFF No Running
    return false;
  }
  // Check if too fast by 1xTolerance
  else if (dP>tol && dP <2*tol){
    if (stateMEDIUM == true){colorWipe(red,5,31);} // BLUE
    else{colorWipe(black,5,31);} // OFF
  }
  else if (dP<-tol && dP > -2*tol){
    if (stateMEDIUM == true){colorWipe(blue,5,31);} // RED
    else{colorWipe(black,5,31);} // OFF
  }
  return false; // Allow the event chain to continue
}

/****************BLINK HIGH******************/
bool blinkmeHIGH() {
  stateHIGH = !stateHIGH; // Switch light states
  if (c == 0 || geeps_status == 0){
    colorWipe(black,5,31); // OFF No Running
    return false;
  }
  // Check if too fast by 2xTolerance
  else if (dP >2*tol){
    if (stateHIGH == true){colorWipe(red,5,31);} // BLUE
    else{colorWipe(black,5,31);} // OFF
  }
  else if (dP < -2*tol){
    if (stateHIGH == true){colorWipe(blue,5,31);} // RED
    else{colorWipe(black,5,31);} // OFF
  }
  else if (dP>=-tol && dP <= tol){
    colorWipe(green,5,31); // GREEN
  }
  return false; // Allow the event chain to continue
}

// This is for debugging purposes but it allows the user to type in a float value for the dP value
bool readme(){
  if (Serial.available()>0){
    float tempX = Serial.parseFloat();
    if (tempX != 0) {
      if (tempX == 1) wifi_status =0;
      else if(tempX == 2) wifi_status = 1;
      else if(tempX == 3) geeps_status = 0;
      else if(tempX == 4) geeps_status = 1;
      else if(tempX == 5) dP = 10;
      else if(tempX == 6) dP = 15;
      else if(tempX == 7) dP = 25;
      else if(tempX == 8) dP = -10;
      else if(tempX == 9) dP = -15;
      else if(tempX == 10) dP = -25;
      else if(tempX == 11) c = 0;
      else if(tempX == 12) c = 1;
      else if(tempX == 13) Pbool = 0;
      else if(tempX == 14) Pbool = 1;
      else if(tempX == 15) Pbool = 2;
      else if(tempX == 16) fbool = 0;
      else if(tempX == 17) fbool = 1;
      else if(tempX == 18) fbool = 2;
      else if(tempX == 19) tolbool = 0;
      else if(tempX == 20) tolbool = 1;
      else if(tempX == 21) tolbool = 2;
      }
  }
}

/*****************CALLBACK*******************/
// Function callback is used to enable or disable GPS logging
// callback is triggered each time paceLight_control receives a message
void callback(double y) {
  if (c == 0 && tolbool == 2 && fbool ==2 && Pbool ==2) {
  Serial.println("Starting GPS logging");
  c = !c;
  }
  else if (c == 1) { 
  Serial.println("Ceasing GPS logging");
  c = !c;
  }
  else {
    Serial.println("you're dumb");
  }
}


/***************CALLBACK_PACE****************/
// Function callback_pace is used to define a user's desired pace
// callback_pace is triggered each time paceLight_pace receives a message
void callback_pace(double z) {
  if (c==0) {
  mqtt.readSubscription(t_mqtt);
  P = atof((char *)pace.lastread);  // Note: if invalid (non-double) data is read, P = 0
  if (P > 0) {
    Pbool = 2;
    colorWipe(white,2,4); // green red
    strip.Show();
    Serial.print("\n Target Pace: ");
    Serial.print(P);    
  }
  else {
    Pbool = 1;
    Serial.println("invalid pace input");
    colorWipe(red,2,4); // BAD red
  }
  }
}

/***************CALLBACK_PERIOD**************/
// Function callback_period is used to define a user's desired feedback period
// callback_period is triggered each time paceLight_period receives a message
void callback_period(double s) {
  if (c == 0) {
  mqtt.readSubscription(t_mqtt);
  f = atoi((char *)period.lastread); // Note: if invalid (non-double) data is read, f = 0
  if (f > 0) {  // If input is valid...
    fbool = 2;
    Serial.print("\nFeedback period: ");
    Serial.print(f);
    colorWipe(white,3,5); // green red
  }
  else { // If input is invalid...
    fbool = 1;
    Serial.println("invalid period input");
    colorWipe(red,3,5); // BAD red
  }
  }
}

/***************CALLBACK_TOLERANCE**************/
// Function callback_tolerance is used to define a user's desired pace tolerance
// callback_tolerance is triggered each time paceLight_tolerance receives a message
void callback_tolerance(double n) {
  if (c == 0) {
  mqtt.readSubscription(t_mqtt);
  tol = atof((char *)tolerance.lastread); // Note: if invalid (non-double) data is read, tol = 0
  tol = (tol/60);
  if (tol > 0) {
    tolbool = 2;
    Serial.print("\nTolerance: ");
    Serial.print(tol);
    colorWipe(white,4,6); // green red
  }
  else {
    tolbool = 1;
    Serial.print("invalid tolerance input");
    colorWipe(red,4,6); // BAD red
  }
  }
}

/**************MQTT CONNECT*******************/
void MQTT_connect() {
  int8_t ret;

// If connected to MQTT, continue script
if (mqtt.connected()) {
  return;
}

Serial.print("Connecting to MQTT... ");

// 3 attempts.  Check status of MQTT, return string of error, if applicable, die if > attempts
uint8_t retries = 3;
while ((ret = mqtt.connect()) !=0) {
  Serial.println(mqtt.connectErrorString(ret));
  Serial.println("Retrying MQTT connection in 5 seconds...");
  mqtt.disconnect();
  delay(5000);
  retries--;
  if (retries == 0){
    while (1);
  }
}
  Serial.println("MQTT connected");
}

/******************MQTT PING*****************/
void mqtt_ping() {
  mqtt.ping();
  Serial.println("pinging MQTT");
}

/*******************MQTT LISTEN***************/
void mqtt_listen() {
  MQTT_connect();
  mqtt.processPackets(1000);
}

/*******************GEEPS READ****************/
void geeps_read() {
  GPS.read();
}

/*******************GEEPS PARSE***************/
void geeps_parse() {
  GPS.parse(GPS.lastNMEA());
  if (GPS.fix) {
    geeps_status = 1;
    //Serial.println("the fix is in");
    colorWipe(white,0,2);// WHITE GOOD
    //Serial.println("IS IT ON? no.");
  }
  else {
    geeps_status = 0;
    colorWipe(red,0,2); // PURPLE BAD
  }
  if (c==1) { 
      if (GPS.fix) {
        count ++;
        //Serial.print("\ncount: ");
        //Serial.print(count);
        B = (60/(D*GPS.speed)); // B = actual pace, minutes / mile
        //Serial.print("\npace: ");
        //Serial.print(B);
        Bt = Bt+B; // Total pace values for use with average
        Serial.println(Bt);
      if (count == (f / (t_parse/1000))) {
        //nSplit ++;
        B = Bt / count;
        //Bs[nSplit] = B;
        dP = B - P;
        //Serial.print("\nsplit: ");
        //Serial.print(nSplit);
        Serial.print("\navg pace: ");
        Serial.print(B);
        Serial.print("\ndiscrepancy");
        Serial.print(dP);
        Bt = 0;
        count = 0;
    if (! dataz.publish(B)) {
    //Serial.println(F("Failed"));
  } else {
    //Serial.println(F("OK!"));
  }
  }
}
}
}

/********************SETUP*********************/
void setup() {

  strip.Begin();
  colorWipe(black,0,31); // TURN OFF 2 front LED
  //Begin wifi and serial commo
  WiFi.begin(ssid, password);
  colorWipe(red,1,3); // BAD YELLOW
  Serial.begin(115200);
  GPS.begin(9600);
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);
  
// If wifi isn't connected, pause, yell at user until connected
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.println("this shit doesn't work, idiot");
  }
  Serial.println("connected to " + WiFi.SSID());
  colorWipe(white,1,3); // WHITE GOOD
  wifi_status = 1;

ctrl.setCallback(callback);
pace.setCallback(callback_pace);
period.setCallback(callback_period);
tolerance.setCallback(callback_tolerance);

// Setup sub for control feed
mqtt.subscribe(&ctrl);
mqtt.subscribe(&pace);
mqtt.subscribe(&period);
mqtt.subscribe(&tolerance);

mgr.addListener(new EvtTimeListener(t_mqtt, true, (EvtAction)mqtt_listen));
mgr.addListener(new EvtTimeListener(t_read, true, (EvtAction)geeps_read));
mgr.addListener(new EvtTimeListener(t_parse, true, (EvtAction)geeps_parse));
//mgr.addListener(new EvtTimeListener(t_ping, true, (EvtAction)mqtt_ping));
mgr.addListener(new EvtTimeListener(200, true, (EvtAction)blinkmeMEDIUM));
mgr.addListener(new EvtTimeListener(50, true, (EvtAction)blinkmeHIGH)); 


delay(5000);

}

/**************** LOOP **********************/

USE_EVENTUALLY_LOOP(mgr)

Credits

Kyelo Torres

Kyelo Torres

9 projects • 5 followers
Hi, my name is Kyelo and I am a 4th-year mechanical engineer at UC Berkeley. My class, club and personal projects will be posted here!

Comments