diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6269b43 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ + +# Version 0.2.0 29/11/2024 + +- Changed pulley diameters to pulley teeth +- Added `float epsilon` for calibrating substeps. +- Added circuit diagram. +- Updated documentation and README.md +- updated `spin_degs()` if statements and compensation. +- Added unit tests `calibrate_pulley_teeth()` and `test_spin_degs_multi()`. + +# Version 0.1.0 22/11/2024 + +Initial release. Development till 22/11/2024. diff --git a/README.md b/README.md index 2841940..dcc7fa8 100644 --- a/README.md +++ b/README.md @@ -7,27 +7,20 @@ Open-source 3D clinostat. A clinostat is a small scale [microgravity simulator]( This is based on the European Space Agency's work, specifically on [Jack Van Loon's clinorotation work](https://doi.org/10.3389/fpls.2019.01577). -It is very difficult to access a 3D clinostat, there are some companies that sell it but can be prohibitively expensive for gravity research. OpenClino can be built for £100 using off the shelf parts. OpenClino can run in continuous clinorotation or as a Random Positioning Machine (RPM). - -This is a side project for me and is very much work in progress. - -OpenClino is designed to be simple, accessible, affordable, and **reliable**. It is designed to make use of 3D printing's strengths and requires *no machining* and minimum tools. To build OpenClino all non-printed parts are available off the shelf, mainly 3d printer stepper motors, belts, controllers, and skateboard bearings. All these parts are rated for thousands of hours of operation, and I have fully tested OpenClino to run for a minimum of 100 hrs without fault. +OpenClino is designed to be simple, accessible, affordable, and **reliable**. It is designed to make use of 3D printing's strengths and requires *no machining* and minimum tools. To build OpenClino all non-printed parts are available off the shelf, mainly 3d printer stepper motors, belts, controllers, and skateboard bearings. All these parts are rated for thousands of hours of operation, and I have fully tested OpenClino to run for a minimum of 100 hours. ## Documentation Clinostat build! I have provided: -- Docs in [`docs/1_.md/`](docs/1_documentation.md) + +- Docs in [`docs/1_documentation.md/`](docs/1_documentation.md) including circuit diagram. - Code in [`src/`](src/openclino.ino) - 3D print files as .3MF in [`3d_files/`](3d_files/) - Bill of materials in [`docs/2_BOM.md/`](docs/2_BOM.md/) - Build guide in [`docs/3_build_guide.md/`](docs/3_build_guide.md/) - -I will provide (TODO): - -- code documentation. - +- Code documentation in [`docs/4_code_documentation.md/`](docs/4_code_documentation.md/) ## Quick Usage @@ -41,7 +34,6 @@ void loop() { Or to run as a random positioning machine, this will run a random walk routine as specified in ESA's work: - ```cpp void loop() { RPM(); @@ -50,12 +42,6 @@ void loop() { Don't forget to set the output pins for you motor controllers! -## Collaboration - -Please contact me on LinkedIn or raise an issue. - -I would be happy to collaborate on this. - ## Contributors [Dan Dragomirescu @dragomda](https://github.com/dragomda) diff --git a/docs/1_documentation.md b/docs/1_documentation.md index 6540b59..cafe563 100644 --- a/docs/1_documentation.md +++ b/docs/1_documentation.md @@ -1,5 +1,9 @@ # OpenClino +It is very difficult to access a 3D clinostat, there are some companies that sell it but can be prohibitively expensive for gravity research. OpenClino can be built for £100 using off the shelf parts. OpenClino can run in continuous clinorotation or as a Random Positioning Machine (RPM). + +This is a side project for me and is very much work in progress. + ## Contents list - [`Bill of Materials`](2_BOM.md) @@ -9,45 +13,19 @@ - [Notation](#notation) - [Code](#code) -## Design requirements - -Clinostats are usually used in biology for plants, cell cultures, and small animals such as fish. - -1. OpenClino shall be robust to failures to not harm the animals or samples. -2. Openclino must be simple, accessible, and affordable, requiring no machining. -3. OpenClino should operate in hot and humid environments such as incubators. -4. OpenClino should operate as a clinostat or RPM. - -At the moment it is a work in progress. -I'm not an electrical engineer so this is 12v. Be careful with your electronics. -At the moment it functions as a clino but not a true RPM. The motors can't start a new direction or speed independently. -You can buy waterproof nema17s and have the electronics outside of the incubator if needed. - -TODO material requirements - -## Notation -TODO make a figure defining part names - -Definitions: -motor_pulley -x_pulley -y_pulley -m2y pulley -y_turn -y_turn_pulley -y_guide -motor_mount -motor_enclosure -which is x and y +## Convention & Definitions +- X body is the external larger square. +- Y body is the internal smaller body. +- Motor pulleys attach to the motor. +- X/Y pulleys attach to the body. +- The "`m2y`" pulley is the one with 2 belts attached. +- The "`y_turn`" is the bearings bending the long y belt. +- The "`y_guide`" is the L shaped bracket holding the y pulley. ## Code -The code is work in progress, it requires the following: - - control multiple motors separately. - - acceleration. - - docs. - - Implementing the airbus algorithm for variable G. +[OpenClino Documentation.](4_code_documentation.md) ### Usage @@ -61,7 +39,6 @@ void loop() { Or to run as a random positioning machine, this will run a random walk routine as specified in ESA's work: - ```cpp void loop() { RPM(); @@ -70,3 +47,8 @@ void loop() { Don't forget to set the output pins for you motor controllers! +# Circuit design + +This is a very simple arduino circuit with a button and 2 stepper motor controllers. + +![Clinostat circuit.](images/openclino_circuit_diagram.png "Circuit diagram. Made with Cirkit Designer.") diff --git a/docs/4_code_documentation.md b/docs/4_code_documentation.md new file mode 100644 index 0000000..48cc716 --- /dev/null +++ b/docs/4_code_documentation.md @@ -0,0 +1,70 @@ +# OpenClino Documentation + +This document provides reference information about the functions used to control stepper motors in the Arduino project. Each function is described with its purpose, parameters, and behavior. + +## Functions + +### `void yClockwise(bool foo)` + +Sets the direction of the Y-axis motor. + +- **Parameters:** + - `foo` (bool): If `true`, sets the direction to clockwise; if `false`, sets it to counterclockwise. + +--- + +### `void spin_continuous(float speedRPMX = 60, float speedRPMY = 60, int finalDelay = 100)` + +Spins the X and Y motors continuously at specified speeds until a button is pressed. + +- **Parameters:** + - `speedRPMX` (float, default = 60): Speed of the X-axis motor in RPM. + - `speedRPMY` (float, default = 60): Speed of the Y-axis motor in RPM. + - `finalDelay` (int, default = 100): Delay in milliseconds after stopping the motors. + +--- + +### `void spin_degs(float degX, float degY, float speedX = 10, float speedY = 10, int finalDelay = 100)` + +Spins the X and Y motors by specified degrees at specified speeds. + +- **Parameters:** + - `degX` (float): Degrees to rotate the X-axis motor. + - `degY` (float): Degrees to rotate the Y-axis motor. + - `speedX` (float, default = 10): Speed of the X-axis motor in RPM. + - `speedY` (float, default = 10): Speed of the Y-axis motor in RPM. + - `finalDelay` (int, default = 100): Delay in milliseconds after stopping the motors. + +--- + +### `void RPM()` + +Placeholder function for future implementation. + +--- + +### `void calibrate_pulley_teeth()` + +Calibrates the pulley teeth by rotating the motors in specific patterns. + +--- + +### `void test_spin_degs_multi()` + +Tests the `spin_degs` function with multiple rotations. + +--- + +### `void setup()` + +Initializes the Arduino pins and serial communication, and prints a welcome message. + +--- + +### `void loop()` + +Main loop that reads the button state and performs actions based on the button press. + +- **Behavior:** + - Turns off motors when the button is not pressed. + - When the button is pressed, it turns on the motors, lights up the built-in LED, and runs calibration and test functions. diff --git a/docs/images/openclino_circuit_diagram.png b/docs/images/openclino_circuit_diagram.png new file mode 100644 index 0000000..046d5cf Binary files /dev/null and b/docs/images/openclino_circuit_diagram.png differ diff --git a/src/openclino.ino b/src/openclino.ino index 9a9980f..0adca1e 100644 --- a/src/openclino.ino +++ b/src/openclino.ino @@ -1,7 +1,4 @@ -// Define steps per revolution -const int motorSteps = 200; -const int subStep = 16; -const unsigned long stepsPerRevolution = motorSteps * subStep; +#include // pins const int buttonPin = 2; @@ -12,19 +9,37 @@ const int enablePinY = 6; const int stepPinY = 7; const int dirPinY = 8; -// pulley diameters -const float xMotD = 9.5; -const float xPulleyD = 31.5; -const float yMotD = 9.5; -const float yTurnD = 31.5; -const float yPulleyD = 18.5; -const double xRatio = xMotD / xPulleyD; // diameters -const double yRatio = yMotD / yPulleyD; -const int nStepsXperRot = stepsPerRevolution / xRatio; -const int nStepsYperRot = stepsPerRevolution / yRatio; +// Define steps per revolution +const int motorSteps = 200; +const int subStep = 16; +const unsigned long stepsPerRevolution = motorSteps * subStep; +// eposilon to fix discrepancy between motor substeps +// set to 1 to undo calibration. +const double epsilon = 1.5; + +// Pulley teeth +const float xMotTeeth = 16; +const float xPulleyTeeth = 50; +const float yMotTeeth = 16; +const float yTurnTeeth = 50; +const float yPulleyTeeth = 20; + +// Calculate the gear ratios based on pulley teeth +const double xRatio = xPulleyTeeth / xMotTeeth; +const double yRatio1 = yTurnTeeth / yMotTeeth; +const double yRatio2 = yPulleyTeeth / yTurnTeeth; +const double yRatio = yRatio1 * yRatio2 * epsilon; + +// Calculate effective steps per rotation +const double nStepsPerRotX = stepsPerRevolution * xRatio; +const double nStepsPerRotY = stepsPerRevolution * yRatio; int buttonState = 0; +// Define AccelStepper objects for X and Y axes +AccelStepper stepperX(1, stepPinX, dirPinX); +AccelStepper stepperY(1, stepPinY, dirPinY); + void wake(bool x, bool y) { if (x == true) @@ -69,194 +84,92 @@ void yClockwise(bool foo) } } -// function to continuous spin x and y given and rpm where if 0 keep still -// function to spin and -// void spin_degs(): -// think about smoothing - -void spin_continuous(int t = 1, float speedX = 10, float speedY = 10, int finalDelay = 100) +void spin_continuous(float speedRPMX = 60, float speedRPMY = 60, int finalDelay = 100) { - bool xClock; - bool yClock; + // Convert RPM to steps per second for speed settings + double speedX = (speedRPMX * nStepsPerRotX) / 60.0; + double speedY = (speedRPMY * nStepsPerRotY) / 60.0; - if (speedX >= 0) - { - xClock = true; - } - else - { - xClock = false; - } - if (speedY >= 0) - { - yClock = true; - } - else - { - yClock = false; - } + // Set speeds and accelerations for each motor + stepperX.setMaxSpeed(speedX); + stepperY.setMaxSpeed(speedY); + stepperX.setAcceleration(speedX * 2); // Adjust acceleration factor as needed + stepperY.setAcceleration(speedY * 2); - long IntervalX = (6e7 / speedX) / nStepsXperRot; - long IntervalY = (6e7 / speedY) / nStepsYperRot; - unsigned long stepsY = 0; - unsigned long stepsX = 0; - unsigned long previousTimeX = micros(); - unsigned long previousTimeY = micros(); + delay(1000); // Final delay to settle motors - while (true) + // set motors to run until button is pressed + while (buttonState == LOW) { - unsigned long currentTimeX = micros(); - unsigned long currentTimeY = micros(); - bool xGo = false; // whether to step x - bool yGo = false; // whether to step y - bool skipCompensation = false; - - digitalWrite(stepPinX, HIGH); - digitalWrite(stepPinY, HIGH); - - if (currentTimeX - previousTimeX > IntervalX) - { - xGo = true; - } - if (currentTimeY - previousTimeY > IntervalY) - { - yGo = true; - } - if (yGo == true && xGo == true && yClock == false && xClock != yClock) - { - skipCompensation = true; - } - - if (xGo) - { - xClockwise(xClock); - digitalWrite(stepPinX, LOW); - digitalWrite(stepPinX, HIGH); - previousTimeX = currentTimeX; - - if (skipCompensation == false) - { - yClockwise(!xClock); // spin y motor with x - - digitalWrite(stepPinY, LOW); // compensator - digitalWrite(stepPinY, HIGH); - } - stepsX++; - } - - if (yGo) - { - yClockwise(yClock); - digitalWrite(stepPinY, LOW); - digitalWrite(stepPinY, HIGH); - previousTimeY = currentTimeY; - stepsY++; - } - // if (stepsX >= nStepsX && stepsY >= nStepsY) {keepGoing = false;} // check if finished + stepperX.runSpeed(); + stepperY.runSpeed(); + stepperX.run(); } - delay(finalDelay); + + // Optionally, stop the motors after the button is pressed + stepperX.stop(); + stepperY.stop(); } -void spin_degs(float degX, float degY, float speedX = 60, float speedY = 60, int finalDelay = 100) +void spin_degs(float degX, float degY, float speedX = 10, float speedY = 10, int finalDelay = 100) { - // wake(true, true); - - // define directions because y motor has to constantly change direction to compensate - bool xClock; - bool yClock; - - if (degX >= 0) - { - xClock = true; - } - else - { - xClock = false; - } - if (degY >= 0) - { - yClock = true; - } - else - { - yClock = false; - } + // This function is perfect without accel + // Initialize motor direction based on target degrees + bool xClock = (degX >= 0); + bool yClock = (degY >= 0); - int nStepsX = nStepsXperRot * abs(degX / 360.); // define fraction of full turn - int nStepsY = nStepsYperRot * abs(degY / 360.); + // Calculate steps needed for each axis based on degree input + int nStepsX = nStepsPerRotX * abs(degX / 360.0); + int nStepsY = nStepsPerRotY * abs(static_cast(degY) / 360.0); - bool keepGoing = true; + // Determine intervals for each motor based on speed + long intervalX = (6e7 / speedX) / nStepsPerRotX; + long intervalY = (6e7 / speedY) / nStepsPerRotY; - long IntervalX = (6e7 / speedX) / nStepsXperRot; - long IntervalY = (6e7 / speedY) / nStepsYperRot; - unsigned long stepsY = 0; + // Initialize step counters and timing variables unsigned long stepsX = 0; + unsigned long stepsY = 0; unsigned long previousTimeX = micros(); unsigned long previousTimeY = micros(); - while (keepGoing) + while (stepsX < nStepsX || stepsY < nStepsY) { - unsigned long currentTimeX = micros(); - unsigned long currentTimeY = micros(); - bool xGo = false; // whether to step x - bool yGo = false; // whether to step y + unsigned long currentTime = micros(); bool skipCompensation = false; - digitalWrite(stepPinX, HIGH); - digitalWrite(stepPinY, HIGH); - - if (currentTimeX - previousTimeX > IntervalX && stepsX <= nStepsX) - { - xGo = true; - } - if (currentTimeY - previousTimeY > IntervalY && stepsY <= nStepsY) - { - yGo = true; - } - if (yGo == true && xGo == true && yClock == false && xClock != yClock) - { - skipCompensation = true; - } - - if (xGo) + // Check if it's time to move motor X + if ((currentTime - previousTimeX > intervalX) && (stepsX < nStepsX)) { + // Move X motor xClockwise(xClock); digitalWrite(stepPinX, LOW); digitalWrite(stepPinX, HIGH); - previousTimeX = currentTimeX; + previousTimeX = currentTime; + stepsX++; - if (skipCompensation == false) + // Compensation logic: Skip compensation when both axes move, Y motor moves in opposite direction to X + if (!(stepsY < nStepsY && (currentTime - previousTimeY > intervalY) && yClock != xClock)) { - yClockwise(!xClock); // spin y motor with x - - digitalWrite(stepPinY, LOW); // compensator + // Move Y motor to compensate + yClockwise(!xClock); + digitalWrite(stepPinY, LOW); digitalWrite(stepPinY, HIGH); } - stepsX++; } - if (yGo) + // Check if it's time to move motor Y + if ((currentTime - previousTimeY > intervalY) && (stepsY < nStepsY)) { + // Move Y motor yClockwise(yClock); digitalWrite(stepPinY, LOW); digitalWrite(stepPinY, HIGH); - previousTimeY = currentTimeY; + previousTimeY = currentTime; stepsY++; } - if (stepsX >= nStepsX && stepsY >= nStepsY) - { - keepGoing = false; - } // check if finished } - delay(finalDelay); -} -void show_off() -{ - spin_degs(180, 180); - spin_degs(-180, -180); - spin_degs(-180, 180); - spin_degs(180, -180); + delay(finalDelay); // Final delay to settle motors } void RPM() @@ -264,9 +177,28 @@ void RPM() spin_degs(random(-360, 360), random(-360, 360)); } +// Unit tests + +void calibrate_pulley_teeth() +{ + spin_degs(90.0, 0); + spin_degs(-90.0, 0); + spin_degs(0, 90.0); + spin_degs(0, -90.0); +} + +void test_spin_degs_multi() +{ + spin_degs(90, -90); + spin_degs(-90, 90); + spin_degs(180, -180); + spin_degs(-180, 180); + spin_degs(360, 360); + spin_degs(-360, -360); +} + void setup() { - // put your setup code here, to run once: pinMode(buttonPin, INPUT); pinMode(LED_BUILTIN, OUTPUT); pinMode(stepPinX, OUTPUT); @@ -276,8 +208,7 @@ void setup() pinMode(enablePinX, OUTPUT); pinMode(enablePinY, OUTPUT); Serial.begin(9600); - Serial.print("--\nHello\n--"); - + Serial.print("--\nHello from OpenClino.\n--"); wake(false, false); } @@ -285,17 +216,18 @@ void loop() { buttonState = digitalRead(buttonPin); + // Turn off motors when button is not pressed. wake(false, false); if (buttonState == HIGH) { digitalWrite(LED_BUILTIN, HIGH); wake(true, true); - spin_continuous(1, 60, 60); - // show_off(); - // RPM(); + calibrate_pulley_teeth(); + test_spin_degs_multi(); + // uncomment to run continuous spin + // spin_continuous(); + // uncomment to run RPM + // RPM(); } - - digitalWrite(LED_BUILTIN, LOW); - delay(10); // Wait a second } diff --git a/src/raspberry_pi/pulley_calc.py b/src/raspberry_pi/pulley_calc.py deleted file mode 100644 index efb6f22..0000000 --- a/src/raspberry_pi/pulley_calc.py +++ /dev/null @@ -1,14 +0,0 @@ -import numpy as np - -def belt_length(p1, p2, centre_distance): - return np.pi * (p1+p2) * 0.5 + (2*centre_distance) + ((p2-p1)**2/(4*centre_distance)) - - -def main(): - b = belt_length(10, 30, 70) - print(b) - b = belt_length(10, 30, 70) - print(b) - -if __name__ == "__main__": - main() \ No newline at end of file