octv2_motor_controller.ino 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /*
  2. OCTv2 (Oreo Cookie Thrower v2) - ESP32 Motor Controller
  3. Controls two stepper motors for rotation and pitch
  4. Receives commands via Serial at 115200 baud
  5. Hardware:
  6. - Rotation Stepper: A4988 driver
  7. - Pitch Stepper: A4988 driver
  8. - Fire mechanism: Servo or solenoid
  9. - Limit switches for homing
  10. */
  11. #include <ESP32Servo.h>
  12. #include <AccelStepper.h>
  13. // Pin definitions
  14. #define ROTATION_STEP_PIN 2
  15. #define ROTATION_DIR_PIN 3
  16. #define ROTATION_ENABLE_PIN 4
  17. #define ROTATION_LIMIT_PIN 5 // Home limit switch
  18. #define PITCH_STEP_PIN 6
  19. #define PITCH_DIR_PIN 7
  20. #define PITCH_ENABLE_PIN 8
  21. #define PITCH_LIMIT_PIN 9 // Home limit switch
  22. #define FIRE_SERVO_PIN 10
  23. #define FIRE_SOLENOID_PIN 11
  24. #define STATUS_LED_PIN 13
  25. // Motor configuration
  26. #define STEPS_PER_REVOLUTION 200 // Standard stepper motor
  27. #define MICROSTEPS 16 // A4988 microstepping
  28. #define TOTAL_STEPS (STEPS_PER_REVOLUTION * MICROSTEPS)
  29. // Mechanical configuration
  30. #define ROTATION_GEAR_RATIO 5.0 // 5:1 gear reduction
  31. #define PITCH_GEAR_RATIO 3.0 // 3:1 gear reduction
  32. #define ROTATION_STEPS_PER_DEGREE (TOTAL_STEPS * ROTATION_GEAR_RATIO / 360.0)
  33. #define PITCH_STEPS_PER_DEGREE (TOTAL_STEPS * PITCH_GEAR_RATIO / 360.0)
  34. // Movement limits
  35. #define ROTATION_MIN_DEGREES -90.0
  36. #define ROTATION_MAX_DEGREES 90.0
  37. #define PITCH_MIN_DEGREES 0.0
  38. #define PITCH_MAX_DEGREES 60.0
  39. // Speed settings
  40. #define MAX_SPEED_ROTATION 2000 // Steps per second
  41. #define MAX_SPEED_PITCH 1500 // Steps per second
  42. #define ACCELERATION 1000 // Steps per second squared
  43. // Create stepper objects
  44. AccelStepper rotationStepper(AccelStepper::DRIVER, ROTATION_STEP_PIN, ROTATION_DIR_PIN);
  45. AccelStepper pitchStepper(AccelStepper::DRIVER, PITCH_STEP_PIN, PITCH_DIR_PIN);
  46. // Fire mechanism
  47. Servo fireServo;
  48. bool useServoForFire = true; // Set to false to use solenoid instead
  49. // Current positions in degrees
  50. float currentRotation = 0.0;
  51. float currentPitch = 0.0;
  52. bool isHomed = false;
  53. // Safety and error handling
  54. bool emergencyStop = false;
  55. unsigned long lastCommandTime = 0;
  56. const unsigned long WATCHDOG_TIMEOUT = 30000; // 30 seconds
  57. // Command parsing
  58. String inputCommand = "";
  59. bool commandReady = false;
  60. void setup() {
  61. Serial.begin(115200);
  62. Serial.println("🍪 OCTv2 ESP32 Motor Controller Starting...");
  63. // Setup pins
  64. pinMode(ROTATION_ENABLE_PIN, OUTPUT);
  65. pinMode(PITCH_ENABLE_PIN, OUTPUT);
  66. pinMode(ROTATION_LIMIT_PIN, INPUT_PULLUP);
  67. pinMode(PITCH_LIMIT_PIN, INPUT_PULLUP);
  68. pinMode(FIRE_SOLENOID_PIN, OUTPUT);
  69. pinMode(STATUS_LED_PIN, OUTPUT);
  70. // Configure steppers
  71. rotationStepper.setMaxSpeed(MAX_SPEED_ROTATION);
  72. rotationStepper.setAcceleration(ACCELERATION);
  73. pitchStepper.setMaxSpeed(MAX_SPEED_ELEVATION);
  74. pitchStepper.setAcceleration(ACCELERATION);
  75. // Enable steppers
  76. digitalWrite(ROTATION_ENABLE_PIN, LOW); // LOW = enabled for A4988
  77. digitalWrite(PITCH_ENABLE_PIN, LOW);
  78. // Setup fire mechanism
  79. if (useServoForFire) {
  80. fireServo.attach(FIRE_SERVO_PIN);
  81. fireServo.write(0); // Home position
  82. } else {
  83. digitalWrite(FIRE_SOLENOID_PIN, LOW);
  84. }
  85. // Status LED
  86. digitalWrite(STATUS_LED_PIN, HIGH);
  87. Serial.println("✅ OCTv2 Motor Controller Ready");
  88. Serial.println("Commands: HOME, MOVE <rot> <pitch>, REL <rot> <pitch>, FIRE, POS");
  89. }
  90. void loop() {
  91. // Check for emergency stop or communication timeout
  92. if (emergencyStop || (millis() - lastCommandTime > WATCHDOG_TIMEOUT && isHomed)) {
  93. if (!emergencyStop) {
  94. Serial.println("WARNING: Communication timeout - emergency stop");
  95. emergencyStop = true;
  96. }
  97. // Emergency stop: disable motors and fast blink LED
  98. rotationStepper.stop();
  99. pitchStepper.stop();
  100. digitalWrite(ROTATION_ENABLE_PIN, HIGH);
  101. digitalWrite(PITCH_ENABLE_PIN, HIGH);
  102. static unsigned long emergencyBlink = 0;
  103. if (millis() - emergencyBlink > 200) {
  104. digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
  105. emergencyBlink = millis();
  106. }
  107. return;
  108. }
  109. // Handle serial commands
  110. handleSerialInput();
  111. if (commandReady) {
  112. processCommand();
  113. commandReady = false;
  114. inputCommand = "";
  115. lastCommandTime = millis(); // Reset watchdog timer
  116. }
  117. // Run steppers (only if not in emergency stop)
  118. if (!emergencyStop) {
  119. rotationStepper.run();
  120. pitchStepper.run();
  121. }
  122. // Status LED: blink when moving, solid when idle
  123. static unsigned long lastBlink = 0;
  124. if (rotationStepper.isRunning() || pitchStepper.isRunning()) {
  125. if (millis() - lastBlink > 100) {
  126. digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
  127. lastBlink = millis();
  128. }
  129. } else {
  130. digitalWrite(STATUS_LED_PIN, HIGH);
  131. }
  132. }
  133. void handleSerialInput() {
  134. while (Serial.available()) {
  135. char c = Serial.read();
  136. if (c == '\n' || c == '\r') {
  137. if (inputCommand.length() > 0) {
  138. commandReady = true;
  139. }
  140. } else {
  141. inputCommand += c;
  142. }
  143. }
  144. }
  145. void processCommand() {
  146. inputCommand.trim();
  147. inputCommand.toUpperCase();
  148. if (inputCommand == "HOME") {
  149. homeMotors();
  150. }
  151. else if (inputCommand.startsWith("MOVE ")) {
  152. handleMoveCommand();
  153. }
  154. else if (inputCommand.startsWith("REL ")) {
  155. handleRelativeCommand();
  156. }
  157. else if (inputCommand == "FIRE") {
  158. fireOreo();
  159. }
  160. else if (inputCommand == "POS") {
  161. reportPosition();
  162. }
  163. else if (inputCommand == "STOP") {
  164. emergencyStop = true;
  165. Serial.println("EMERGENCY STOP ACTIVATED");
  166. }
  167. else if (inputCommand == "RESET") {
  168. emergencyStop = false;
  169. digitalWrite(ROTATION_ENABLE_PIN, LOW);
  170. digitalWrite(PITCH_ENABLE_PIN, LOW);
  171. lastCommandTime = millis();
  172. Serial.println("Emergency stop reset - system ready");
  173. }
  174. else if (inputCommand == "STATUS") {
  175. reportStatus();
  176. }
  177. else {
  178. Serial.println("ERROR: Unknown command");
  179. }
  180. }
  181. void homeMotors() {
  182. Serial.println("🏠 Homing motors...");
  183. // Disable acceleration for homing
  184. rotationStepper.setAcceleration(500);
  185. pitchStepper.setAcceleration(500);
  186. // Home rotation motor
  187. Serial.println("Homing rotation...");
  188. rotationStepper.setSpeed(-500); // Move slowly towards limit
  189. while (digitalRead(ROTATION_LIMIT_PIN) == HIGH) {
  190. rotationStepper.runSpeed();
  191. delay(1);
  192. }
  193. rotationStepper.stop();
  194. rotationStepper.setCurrentPosition(0);
  195. // Back off from limit
  196. rotationStepper.move(100); // Move away from limit
  197. while (rotationStepper.run()) { delay(1); }
  198. // Home pitch motor
  199. Serial.println("Homing pitch...");
  200. pitchStepper.setSpeed(-300); // Move slowly towards limit
  201. while (digitalRead(PITCH_LIMIT_PIN) == HIGH) {
  202. pitchStepper.runSpeed();
  203. delay(1);
  204. }
  205. pitchStepper.stop();
  206. pitchStepper.setCurrentPosition(0);
  207. // Back off from limit
  208. pitchStepper.move(50);
  209. while (pitchStepper.run()) { delay(1); }
  210. // Restore normal acceleration
  211. rotationStepper.setAcceleration(ACCELERATION);
  212. pitchStepper.setAcceleration(ACCELERATION);
  213. // Set home position
  214. currentRotation = 0.0;
  215. currentPitch = 0.0;
  216. isHomed = true;
  217. Serial.println("OK");
  218. }
  219. void handleMoveCommand() {
  220. // Parse "MOVE <rotation> <pitch>"
  221. int firstSpace = inputCommand.indexOf(' ', 5);
  222. if (firstSpace == -1) {
  223. Serial.println("ERROR: Invalid MOVE syntax");
  224. return;
  225. }
  226. float targetRotation = inputCommand.substring(5, firstSpace).toFloat();
  227. float targetPitch = inputCommand.substring(firstSpace + 1).toFloat();
  228. moveToPosition(targetRotation, targetPitch);
  229. }
  230. void handleRelativeCommand() {
  231. // Parse "REL <delta_rotation> <delta_pitch>"
  232. int firstSpace = inputCommand.indexOf(' ', 4);
  233. if (firstSpace == -1) {
  234. Serial.println("ERROR: Invalid REL syntax");
  235. return;
  236. }
  237. float deltaRotation = inputCommand.substring(4, firstSpace).toFloat();
  238. float deltaPitch = inputCommand.substring(firstSpace + 1).toFloat();
  239. float targetRotation = currentRotation + deltaRotation;
  240. float targetPitch = currentPitch + deltaPitch;
  241. moveToPosition(targetRotation, targetPitch);
  242. }
  243. void moveToPosition(float targetRotation, float targetPitch) {
  244. if (!isHomed) {
  245. Serial.println("ERROR: Not homed");
  246. return;
  247. }
  248. // Clamp to limits
  249. targetRotation = constrain(targetRotation, ROTATION_MIN_DEGREES, ROTATION_MAX_DEGREES);
  250. targetPitch = constrain(targetPitch, ELEVATION_MIN_DEGREES, ELEVATION_MAX_DEGREES);
  251. // Convert to steps
  252. long rotationSteps = (long)(targetRotation * ROTATION_STEPS_PER_DEGREE);
  253. long elevationSteps = (long)(targetPitch * PITCH_STEPS_PER_DEGREE);
  254. // Set new targets (non-blocking)
  255. rotationStepper.moveTo(rotationSteps);
  256. pitchStepper.moveTo(elevationSteps);
  257. // Update target positions
  258. currentRotation = targetRotation;
  259. currentPitch = targetPitch;
  260. // Immediate response - movement happens in background
  261. Serial.println("OK");
  262. }
  263. void fireOreo() {
  264. Serial.println("🔥 FIRING OREO!");
  265. if (useServoForFire) {
  266. // Servo fire mechanism
  267. fireServo.write(90); // Fire position
  268. delay(200); // Fire duration
  269. fireServo.write(0); // Return to home
  270. } else {
  271. // Solenoid fire mechanism
  272. digitalWrite(FIRE_SOLENOID_PIN, HIGH);
  273. delay(100); // Fire pulse
  274. digitalWrite(FIRE_SOLENOID_PIN, LOW);
  275. }
  276. Serial.println("OK");
  277. }
  278. void stopMotors() {
  279. rotationStepper.stop();
  280. pitchStepper.stop();
  281. Serial.println("OK");
  282. }
  283. void reportPosition() {
  284. // Report actual current position based on stepper positions
  285. float actualRotation = rotationStepper.currentPosition() / ROTATION_STEPS_PER_DEGREE;
  286. float actualPitch = pitchStepper.currentPosition() / PITCH_STEPS_PER_DEGREE;
  287. Serial.print(actualRotation, 1);
  288. Serial.print(" ");
  289. Serial.println(actualPitch, 1);
  290. }
  291. void reportStatus() {
  292. // Report actual positions and targets
  293. float actualRotation = rotationStepper.currentPosition() / ROTATION_STEPS_PER_DEGREE;
  294. float actualPitch = pitchStepper.currentPosition() / PITCH_STEPS_PER_DEGREE;
  295. float targetRotation = rotationStepper.targetPosition() / ROTATION_STEPS_PER_DEGREE;
  296. float targetPitch = pitchStepper.targetPosition() / PITCH_STEPS_PER_DEGREE;
  297. Serial.print("HOMED:");
  298. Serial.print(isHomed ? "1" : "0");
  299. Serial.print(" ROT:");
  300. Serial.print(actualRotation, 1);
  301. Serial.print(" PITCH:");
  302. Serial.print(actualPitch, 1);
  303. Serial.print(" TARGET_ROT:");
  304. Serial.print(targetRotation, 1);
  305. Serial.print(" TARGET_PITCH:");
  306. Serial.print(targetPitch, 1);
  307. Serial.print(" MOVING:");
  308. Serial.print((rotationStepper.isRunning() || pitchStepper.isRunning()) ? "1" : "0");
  309. Serial.print(" EMERGENCY:");
  310. Serial.print(emergencyStop ? "1" : "0");
  311. Serial.print(" LIMITS:");
  312. Serial.print(digitalRead(ROTATION_LIMIT_PIN) ? "0" : "1");
  313. Serial.print(",");
  314. Serial.println(digitalRead(PITCH_LIMIT_PIN) ? "0" : "1");
  315. }
  316. // Helper function for debugging
  317. void printDebugInfo() {
  318. Serial.print("Debug - Rot pos: ");
  319. Serial.print(rotationStepper.currentPosition());
  320. Serial.print(" target: ");
  321. Serial.print(rotationStepper.targetPosition());
  322. Serial.print(" | Pitch pos: ");
  323. Serial.print(pitchStepper.currentPosition());
  324. Serial.print(" target: ");
  325. Serial.println(pitchStepper.targetPosition());
  326. }