#include // ----- Pin definitions ----- #define Hold0 18// changed to 18 for demo #define Hold1 1 #define Hold2 2 #define Hold3 21 #define Hold4 22 #define Hold5 23 #define Battery_LED 0 //change to 0 for demo #define NeoPixelPin 19 #define ButtonPin 3 // start button (active LOW) // ----- NeoPixel setup ----- #define NUMPIXELS 8 Adafruit_NeoPixel strip(NUMPIXELS, NeoPixelPin, NEO_GRB + NEO_KHZ800); // ----- Holds & workout configuration ----- #define NUM_HOLDS 6 #define MAX_STEPS 16 // max number of workout steps you want to support // Array of hold pins for easier mapping const uint8_t holdPins[NUM_HOLDS] = {Hold0, Hold1, Hold2, Hold3, Hold4, Hold5}; // Workout data (modifiable via serial) unsigned long workTimes[MAX_STEPS]; // ms unsigned long restTimes[MAX_STEPS]; // ms uint8_t holdOrder[MAX_STEPS]; // index into holdPins[] uint8_t stepCount = 0; // number of valid steps // Extra rest after button press, in ms (configurable via PRE: command) unsigned long preStartRest = 10000; // default 10s // ----- Helper functions ----- void allHoldsLow() { for (int i = 0; i < NUM_HOLDS; i++) { digitalWrite(holdPins[i], LOW); } } void clearStrip() { for (int i = 0; i < NUMPIXELS; i++) { strip.setPixelColor(i, 0, 0, 0); // off } strip.show(); } void workPhase(unsigned long workTimeMs) { clearStrip(); if (workTimeMs < NUMPIXELS) workTimeMs = NUMPIXELS; // avoid 0 delays unsigned long stepDelay = workTimeMs / NUMPIXELS; // Fill bar one pixel at a time (green) for (int i = 0; i < NUMPIXELS; i++) { strip.setPixelColor(i, 0, 255, 0); // green strip.show(); delay(stepDelay); } // If any leftover time due to integer division, wait it out unsigned long usedTime = stepDelay * NUMPIXELS; if (workTimeMs > usedTime) { delay(workTimeMs - usedTime); } } void restPhase(unsigned long restTimeMs) { // All pixels red at start of rest for (int i = 0; i < NUMPIXELS; i++) { strip.setPixelColor(i, 255, 0, 0); // red } strip.show(); if (restTimeMs < NUMPIXELS) restTimeMs = NUMPIXELS; // avoid 0 delays unsigned long stepDelay = restTimeMs / NUMPIXELS; // Decrease bar one pixel at a time (turn off from the end) for (int i = NUMPIXELS - 1; i >= 0; i--) { strip.setPixelColor(i, 0, 0, 0); // off strip.show(); delay(stepDelay); if (i == 0) break; // avoid underflow } // Leftover time if any unsigned long usedTime = stepDelay * NUMPIXELS; if (restTimeMs > usedTime) { delay(restTimeMs - usedTime); } } // ----- Serial parsing helpers ----- void resetWorkoutToDefaults() { // Optional: some default 6-step workout if nothing has been sent yet stepCount = 6; for (uint8_t i = 0; i < stepCount; i++) { holdOrder[i] = i % NUM_HOLDS; // Hold 0..5 in order workTimes[i] = 10000UL; // 10s restTimes[i] = 5000UL; // 5s } preStartRest = 10000UL; // 10s } void parseLine(String line) { line.trim(); if (line.length() == 0) return; // PRE: if (line.startsWith("PRE:")) { String val = line.substring(4); unsigned long seconds = val.toInt(); if (seconds > 0) { preStartRest = seconds * 1000UL; } return; } // H,, if (line.charAt(0) == 'H') { if (stepCount >= MAX_STEPS) { // Ignore extra steps beyond MAX_STEPS return; } int firstComma = line.indexOf(','); int secondComma = line.indexOf(',', firstComma + 1); if (firstComma == -1 || secondComma == -1) { // Bad format, ignore return; } String holdStr = line.substring(1, firstComma); String workStr = line.substring(firstComma + 1, secondComma); String restStr = line.substring(secondComma + 1); int holdIdx = holdStr.toInt(); unsigned long workSec = workStr.toInt(); unsigned long restSec = restStr.toInt(); if (holdIdx < 0 || holdIdx >= NUM_HOLDS) { // Invalid hold index return; } holdOrder[stepCount] = (uint8_t)holdIdx; workTimes[stepCount] = workSec * 1000UL; restTimes[stepCount] = restSec * 1000UL; stepCount++; return; } // Anything else: ignore for now } void readSerialConfig() { // Read all available lines from serial and parse them while (Serial.available()) { String line = Serial.readStringUntil('\n'); parseLine(line); } } void setup() { // Serial for config Serial.begin(9600); // Hold pins pinMode(Hold0, OUTPUT); pinMode(Hold1, OUTPUT); pinMode(Hold2, OUTPUT); pinMode(Hold3, OUTPUT); pinMode(Hold4, OUTPUT); pinMode(Hold5, OUTPUT); // Battery LED always on pinMode(Battery_LED, OUTPUT); digitalWrite(Battery_LED, HIGH); // Button input (using internal pull-up, active LOW) pinMode(ButtonPin, INPUT_PULLUP); // NeoPixel init strip.begin(); strip.setBrightness(80); // adjust 0–255 if you want brighter/dimmer clearStrip(); // Make sure holds are off at start allHoldsLow(); // Default workout in case nothing has been sent yet resetWorkoutToDefaults(); } void loop() { // Continuously listen for new configuration data readSerialConfig(); // Idle state: all holds off, LEDs off except battery LED allHoldsLow(); clearStrip(); // Wait until button is pressed (active LOW) while (digitalRead(ButtonPin) == HIGH) { readSerialConfig(); // allow updating workout while waiting delay(10); } // Simple debounce delay(50); // Wait for button release while (digitalRead(ButtonPin) == LOW) { readSerialConfig(); delay(10); } // Pre-start rest phase (if configured > 0) if (preStartRest > 0) { restPhase(preStartRest); // ----- RUN FULL HOLD SEQUENCE ONCE ----- for (uint8_t s = 0; s < stepCount; s++) { allHoldsLow(); uint8_t holdIdx = holdOrder[s]; if (holdIdx < NUM_HOLDS) { digitalWrite(holdPins[holdIdx], HIGH); // activate current hold } // Work phase: bar fills up workPhase(workTimes[s]); // Turn this hold off before moving to next allHoldsLow(); // Rest phase: bar red then decreases restPhase(restTimes[s]); } // After this, loop() restarts, goes back to waiting for the button }