/*
MODULAR WATCH BOX — Tier firmware TEST sketch (for Wokwi logic simulation)
---------------------------------------------------------------------------
Proves ONE tier's control LOGIC on an ESP32:
- 4 winder motors (28BYJ-48 + ULN2003 on real hardware; plain steppers in Wokwi)
- 1 tier-rotation motor (A4988 here = stand-in for the real TMC2208; same STEP/DIR code)
- 1 LED strip (a single LED in the sim) with brightness control
Control via the Serial Monitor (this is the stand-in for the future phone web app):
W<n> <tpd> <CW|CCW> e.g. W1 650 CW set winder n turns-per-day + direction
ROT <CW|CCW> <ON|OFF> e.g. ROT CCW ON tier rotation direction + on/off
LED <0-255> e.g. LED 180 LED brightness
STATUS print all current settings
IMPORTANT: Wokwi ignores real current/voltage, so this proves the CODE, not the power.
The power side (PSU, buck, slip ring) is what you map in Cirkit and test on the bench.
*/
#include <Arduino.h>
// ---------------- WINDER motors (4 bays) ----------------
const int NUM_WINDERS = 4;
// IN1, IN2, IN3, IN4 pins for each winder
const int winderPins[NUM_WINDERS][4] = {
{13, 14, 27, 26}, // Winder 1
{32, 33, 25, 4}, // Winder 2
{16, 17, 18, 19}, // Winder 3
{21, 22, 23, 15} // Winder 4
};
// Standard 8-step half-step sequence for the 28BYJ-48 (unipolar)
const int halfStep[8][4] = {
{1,0,0,0},{1,1,0,0},{0,1,0,0},{0,1,1,0},
{0,0,1,0},{0,0,1,1},{0,0,0,1},{1,0,0,1}
};
const long STEPS_PER_REV = 4096; // 28BYJ-48: 4096 half-steps per output revolution
struct Winder {
long tpd; // turns per day (0 = stopped)
int dir; // +1 = CW, -1 = CCW
int phase; // index into halfStep[]
unsigned long lastStepUs;
unsigned long stepIntervalUs; // microseconds between half-steps
};
Winder winders[NUM_WINDERS];
void computeInterval(int i) {
if (winders[i].tpd <= 0) { winders[i].stepIntervalUs = 0; return; }
double stepsPerSec = (double)winders[i].tpd * STEPS_PER_REV / 86400.0; // 86400 s/day
winders[i].stepIntervalUs = (unsigned long)(1000000.0 / stepsPerSec);
}
void applyPhase(int i) {
for (int p = 0; p < 4; p++)
digitalWrite(winderPins[i][p], halfStep[winders[i].phase][p]);
}
// ---------------- ROTATION motor (A4988 / TMC2208) ----------------
const int ROT_STEP = 5;
const int ROT_DIR = 2;
bool rotOn = true;
int rotDir = +1;
unsigned long rotLastUs = 0;
unsigned long rotIntervalUs = 3000; // slow "display" rotation
bool rotStepState = false;
// ---------------- LED ----------------
const int LED_PIN = 12;
int ledBrightness = 128;
void printStatus() {
Serial.println(F("---- STATUS ----"));
for (int i = 0; i < NUM_WINDERS; i++)
Serial.printf("Winder %d: %ld TPD, %s\n", i + 1, winders[i].tpd,
winders[i].dir > 0 ? "CW" : "CCW");
Serial.printf("Rotation: %s, %s\n", rotOn ? "ON" : "OFF", rotDir > 0 ? "CW" : "CCW");
Serial.printf("LED: %d/255\n", ledBrightness);
Serial.println(F("----------------"));
}
void parseCommand(String cmd) {
cmd.trim();
cmd.toUpperCase();
if (cmd.startsWith("W") && cmd.length() > 1 && isDigit(cmd[1])) {
int sp1 = cmd.indexOf(' ');
int sp2 = cmd.indexOf(' ', sp1 + 1);
if (sp1 < 0 || sp2 < 0) { Serial.println(F("Use: W<n> <tpd> <CW|CCW>")); return; }
int n = cmd.substring(1, sp1).toInt();
long tpd = cmd.substring(sp1 + 1, sp2).toInt();
String d = cmd.substring(sp2 + 1);
if (n >= 1 && n <= NUM_WINDERS) {
winders[n - 1].tpd = tpd;
winders[n - 1].dir = d.startsWith("CCW") ? -1 : +1;
computeInterval(n - 1);
Serial.printf("Winder %d set: %ld TPD %s\n", n, tpd, winders[n - 1].dir > 0 ? "CW" : "CCW");
}
} else if (cmd.startsWith("ROT")) {
if (cmd.indexOf("CCW") > 0) rotDir = -1; else if (cmd.indexOf("CW") > 0) rotDir = +1;
if (cmd.indexOf("OFF") > 0) rotOn = false; else if (cmd.indexOf("ON") > 0) rotOn = true;
digitalWrite(ROT_DIR, rotDir > 0 ? HIGH : LOW);
Serial.printf("Rotation %s %s\n", rotOn ? "ON" : "OFF", rotDir > 0 ? "CW" : "CCW");
} else if (cmd.startsWith("LED")) {
ledBrightness = constrain(cmd.substring(3).toInt(), 0, 255);
analogWrite(LED_PIN, ledBrightness);
Serial.printf("LED %d\n", ledBrightness);
} else if (cmd.startsWith("STATUS")) {
printStatus();
} else if (cmd.length()) {
Serial.println(F("Commands: W<n> <tpd> <CW|CCW> | ROT <CW|CCW> <ON|OFF> | LED <0-255> | STATUS"));
}
}
void handleSerial() {
static String line;
while (Serial.available()) {
char c = Serial.read();
if (c == '\n' || c == '\r') { if (line.length()) parseCommand(line); line = ""; }
else line += c;
}
}
void setup() {
Serial.begin(115200);
delay(200);
for (int i = 0; i < NUM_WINDERS; i++) {
for (int p = 0; p < 4; p++) { pinMode(winderPins[i][p], OUTPUT); digitalWrite(winderPins[i][p], LOW); }
winders[i].tpd = 650; // sensible default for an automatic watch
winders[i].dir = (i % 2) ? -1 : +1;
winders[i].phase = 0;
winders[i].lastStepUs = 0;
computeInterval(i);
}
pinMode(ROT_STEP, OUTPUT);
pinMode(ROT_DIR, OUTPUT);
digitalWrite(ROT_DIR, HIGH);
pinMode(LED_PIN, OUTPUT);
analogWrite(LED_PIN, ledBrightness);
Serial.println(F("Tier firmware test ready. Type STATUS or a command."));
printStatus();
}
void loop() {
unsigned long nowUs = micros();
// advance each winder independently (non-blocking)
for (int i = 0; i < NUM_WINDERS; i++) {
if (winders[i].stepIntervalUs == 0) continue;
if (nowUs - winders[i].lastStepUs >= winders[i].stepIntervalUs) {
winders[i].lastStepUs = nowUs;
winders[i].phase = (winders[i].phase + winders[i].dir + 8) % 8;
applyPhase(i);
}
}
// pulse the rotation driver's STEP line (non-blocking)
if (rotOn && nowUs - rotLastUs >= rotIntervalUs) {
rotLastUs = nowUs;
rotStepState = !rotStepState;
digitalWrite(ROT_STEP, rotStepState);
}
handleSerial();
}