diff --git a/.gitignore b/.gitignore index eab9ed6..c7d0d09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.FCBak -*.3mf \ No newline at end of file +*.3mf +Code/.vscode diff --git a/CAD/V3/FishingLineAxle/FishingLineAxle-Body-correction.stl b/CAD/V3/FishingLineAxle/FishingLineAxle-Body-correction.stl new file mode 100644 index 0000000..4a64124 Binary files /dev/null and b/CAD/V3/FishingLineAxle/FishingLineAxle-Body-correction.stl differ diff --git a/CAD/V3/FishingLineAxle/FishingLineAxle-Body.stl b/CAD/V3/FishingLineAxle/FishingLineAxle-Body.stl new file mode 100644 index 0000000..9ca90e8 Binary files /dev/null and b/CAD/V3/FishingLineAxle/FishingLineAxle-Body.stl differ diff --git a/CAD/V3/FishingLineAxle/FishingLineAxle.FCStd b/CAD/V3/FishingLineAxle/FishingLineAxle.FCStd new file mode 100644 index 0000000..5beb3b6 Binary files /dev/null and b/CAD/V3/FishingLineAxle/FishingLineAxle.FCStd differ diff --git a/CAD/V3/FishingLineAxleTensioningNut/FishingLineAxleTensioningNut-Body.stl b/CAD/V3/FishingLineAxleTensioningNut/FishingLineAxleTensioningNut-Body.stl new file mode 100644 index 0000000..d4fc5cc Binary files /dev/null and b/CAD/V3/FishingLineAxleTensioningNut/FishingLineAxleTensioningNut-Body.stl differ diff --git a/CAD/V3/FishingLineAxleTensioningNut/FishingLineAxleTensioningNut.FCStd b/CAD/V3/FishingLineAxleTensioningNut/FishingLineAxleTensioningNut.FCStd new file mode 100644 index 0000000..9fbdbd0 Binary files /dev/null and b/CAD/V3/FishingLineAxleTensioningNut/FishingLineAxleTensioningNut.FCStd differ diff --git a/CAD/V3/ReelMount/ReelMount-Body-v3.stl b/CAD/V3/ReelMount/ReelMount-Body-v3.stl new file mode 100644 index 0000000..18d3099 Binary files /dev/null and b/CAD/V3/ReelMount/ReelMount-Body-v3.stl differ diff --git a/CAD/V3/ReelMount/ReelMount-v2.FCStd b/CAD/V3/ReelMount/ReelMount-v2.FCStd new file mode 100644 index 0000000..f874bde Binary files /dev/null and b/CAD/V3/ReelMount/ReelMount-v2.FCStd differ diff --git a/Code/AFRS.ino b/Code/AFRS.ino deleted file mode 100644 index 5b23fd1..0000000 --- a/Code/AFRS.ino +++ /dev/null @@ -1,215 +0,0 @@ -#include - -// === Pin Definitions === -#define M1_STEP_PIN 0 // Reel motor step -#define M1_DIR_PIN 1 // Reel motor direction - -#define M2_STEP_PIN 2 // Carriage motor step -#define M2_DIR_PIN 3 // Carriage motor direction - -#define HOME_SW_PIN 6 // Home limit switch -#define END_SW_PIN 5 // End limit switch - -AccelStepper reelMotor(AccelStepper::DRIVER, M1_STEP_PIN, M1_DIR_PIN); -AccelStepper carriageMotor(AccelStepper::DRIVER, M2_STEP_PIN, M2_DIR_PIN); - -float RAIL_LENGTH = 50; // 100mm linear rail -float CENTER_POSITION = RAIL_LENGTH / 2.0f; // Center position - -// === Motor Configuration === -const int MOTOR_STEPS = 200; // 200 steps/rev for NEMA 17 -const int MICROSTEPPING = 8; // Set on driver board (MS1/MS2 jumpers) -const float SCREW_LEAD = 4.0; // mm per revolution (T8 screw) -const float CALIBRATION_FACTOR = 2.0; -const float STEPS_PER_MM = ((MOTOR_STEPS * MICROSTEPPING) / (SCREW_LEAD / CALIBRATION_FACTOR)); - -// === Speed Settings === -const float REEL_RPM = 150; // Reel motor speed -const float CARRIAGE_RPM = 100; // Carriage motor speed - -// State management -enum State { - IDLE, - HOMING, - FIND_END, - RUNNING -}; -State currentState = HOMING; - -// Manual distance tracking -long targetSteps = 0; -long stepsMoved = 0; -// Position tracking -float currentPositionMM = 0; // Tracks actual position in mm - -float desiredPosition = 25; -bool bounceBool = false; - -void setup() { - Serial.begin(115200); - - // Configure limit switches - pinMode(HOME_SW_PIN, INPUT_PULLUP); - pinMode(END_SW_PIN, INPUT_PULLUP); - - Serial.begin(115200); - Serial.println("Starting motors..."); - - // Configure reel motor (constant speed) - reelMotor.setMaxSpeed((REEL_RPM * MOTOR_STEPS * MICROSTEPPING) / 60.0); - reelMotor.setSpeed(-reelMotor.maxSpeed()); // Negative for reverse - reelMotor.setMinPulseWidth(50); // 50µs pulse width - - // Configure carriage motor (constant speed) - carriageMotor.setMaxSpeed((CARRIAGE_RPM * MOTOR_STEPS * MICROSTEPPING) / 60.0); - carriageMotor.setSpeed(carriageMotor.maxSpeed()); // Positive direction - carriageMotor.setMinPulseWidth(50); // 50µs pulse width -} - -void loop() { - // State machine for carriage - switch(currentState) { - case HOMING: - runHoming(); - break; - - case FIND_END: - runFindEnd(); - break; - - case RUNNING: - //reelMotor.runSpeed(); - if(bounceBool == false){ - if(runMoveToPosition(desiredPosition - 0 )){ - bounceBool = true; - } - } - else { - if(runMoveToPosition(desiredPosition - 20.0)){ - bounceBool = false; - } - } - break; - } -} - -void runHoming(){ - // Move carriage in positive direction - if (carriageMotor.runSpeed()) { - stepsMoved++; - } - - if (digitalRead(HOME_SW_PIN) == LOW) { // Switch pressed (LOW with pullup) - carriageMotor.stop(); // Stop motor - carriageMotor.setSpeed(0); // Set speed to zero - - stepsMoved = 0; - // Reset position tracking - currentPositionMM = 0; - carriageMotor.setCurrentPosition(0); - - // Calculate steps needed to reach center - targetSteps = CENTER_POSITION * STEPS_PER_MM; - - carriageMotor.setSpeed(-carriageMotor.maxSpeed()); // Positive direction - - currentState = FIND_END; - } -} - -void runFindEnd() { - // Run carriage at constant speed - if (carriageMotor.runSpeed()) { - stepsMoved++; - // Update position based on direction - currentPositionMM += (carriageMotor.speed() > 0) ? - (1.0 / STEPS_PER_MM) : - (-1.0 / STEPS_PER_MM); - - // Print position every 100ms - // static unsigned long lastPrint = 0; - // if (millis() - lastPrint > 100) { - // lastPrint = millis(); - // Serial.print("Position: "); - // Serial.print(currentPositionMM); - // Serial.println(" mm"); - // } - } - - if (digitalRead(END_SW_PIN) == LOW) { // Switch pressed (LOW with pullup) - currentState = RUNNING; - - // Stop carriage motor completely - carriageMotor.stop(); - carriageMotor.setSpeed(0); - - // Print final position - Serial.print("Final position: "); - Serial.print(currentPositionMM); - - RAIL_LENGTH = abs(currentPositionMM); - CENTER_POSITION = RAIL_LENGTH / 2.0f; - } -} - -bool runMoveToPosition(float position){ - - float distance = abs(abs(currentPositionMM) - position); - float margin = 0.1f; - - // Print position every 100ms - // static unsigned long lastPrint = 0; - // if (millis() - lastPrint > 100) { - // lastPrint = millis(); - // Serial.print("Position: "); - // Serial.print(abs(currentPositionMM)); - // Serial.println(" mm"); - // Serial.print("Desired Position: "); - // Serial.print(position); - // Serial.println(" mm"); - // Serial.println(distance); - // } - - // Calculate direction - if(distance > margin){ - int direction = sign(position - abs(currentPositionMM)); - - int previousDirection = (carriageMotor.speed() / -1.0f) / carriageMotor.maxSpeed(); - if(previousDirection != direction){ - carriageMotor.setSpeed(carriageMotor.maxSpeed() * direction * -1.0f); - } - } - - // Run carriage at constant speed - if (carriageMotor.runSpeed()) { - stepsMoved++; - // Update position based on direction - currentPositionMM += (carriageMotor.speed() > 0) ? - (1.0 / STEPS_PER_MM) : - (-1.0 / STEPS_PER_MM); - - if (distance <= margin) { - // Stop carriage motor completely - carriageMotor.setSpeed(0); - carriageMotor.stop(); - return true; - } - } - - if (distance <= margin) { - // Stop carriage motor completely - carriageMotor.setSpeed(0); - carriageMotor.stop(); - return true; - } - - return false; -} - -bool withinRange(float value, float target, float margin) { - return (value >= (target - margin)) && (value <= (target + margin)); -} - -float sign(float value) { - return float((value>0)-(value<0)); -} \ No newline at end of file diff --git a/Code/AFRS_motor/AFRS_motor.ino b/Code/AFRS_motor/AFRS_motor.ino new file mode 100644 index 0000000..c2341a5 --- /dev/null +++ b/Code/AFRS_motor/AFRS_motor.ino @@ -0,0 +1,372 @@ +#include +#include +#include + +// === Pin Definitions === +#define M1_STEP_PIN 0 // Reel motor step +#define M1_DIR_PIN 1 // Reel motor direction + +#define M2_STEP_PIN 2 // Carriage motor step +#define M2_DIR_PIN 3 // Carriage motor direction + +#define START_SW_PIN 6 // Home limit switch +#define END_SW_PIN 5 // End limit switch + +// Wi-Fi credentials +const char* ssid = ""; +const char* password = ""; + +WiFiServer motorServer(3333); // Open port 3333 +WiFiClient activeClient; // Store the current client + +String recvBuffer = ""; + +AccelStepper reelMotor(AccelStepper::DRIVER, M1_STEP_PIN, M1_DIR_PIN); +AccelStepper carriageMotor(AccelStepper::DRIVER, M2_STEP_PIN, M2_DIR_PIN); + +// === Motor Configuration === +const int MOTOR_STEPS = 200; // 200 steps/rev for NEMA 17 +const int MICROSTEPPING = 8; // Set on driver board (MS1/MS2 jumpers) +const float DISTANCE_PER_REVOLUTION = 2.0; // mm per revolution (best to do a revolution by hand and measure distance and input it here) +const float STEPS_PER_MM = (MOTOR_STEPS * MICROSTEPPING) / DISTANCE_PER_REVOLUTION; + +// === Speed Settings === +const float REEL_RPM = 40; // Reel motor speed +float CARRIAGE_RPM = 100; // Carriage motor speed (will be dynamically set) +#define FORWARD -1 // In my setup the carriage is moving forward is in negative speed, meaning away from the motor, that why we need to invert the movement in tracking + +// System state management +enum SystemState { + IDLE, + SYSTEM_CALIBRATION, + REEL_CALIBRATION, + REELING +}; +SystemState currentSystemState = IDLE; + +enum SystemCalibrationState { + FIND_START, + FIND_END +}; +SystemCalibrationState systemCalibrationState = FIND_START; + +// Position tracking +float railLength = 0; +float currentPositionMM = 0; +float targetPosition = 0; +bool isMovingToEnd = false; + +// Gap values +float reelGapStartPosition = -1; +float reelGapEndPosition = -1; + +bool starterSet = false; // Is start gap position set? +bool endSet = false; // Is start gap position set? + +void setup() { + Serial.begin(115200); + + Serial.println("Motor ESP Connecting"); + // Connect to Wi-Fi + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println("\nMotor ESP connected. IP: " + WiFi.localIP().toString()); + + // FIXME : Perhaps use UDP communication instead maybe it will not block the ESP32C3 as much + motorServer.begin(); // Start TCP server + + pinMode(START_SW_PIN, INPUT_PULLUP); + pinMode(END_SW_PIN, INPUT_PULLUP); + + // Configure reel motor + reelMotor.setMaxSpeed((REEL_RPM * MOTOR_STEPS * MICROSTEPPING) / 60.0); + reelMotor.setSpeed(-reelMotor.maxSpeed()); // To reel the line in toward the fishing reel, i need the fishing reel to move anti-clockwise + reelMotor.setMinPulseWidth(50); + + // Configure carriage motor + carriageMotor.setMaxSpeed((CARRIAGE_RPM * MOTOR_STEPS * MICROSTEPPING) / 60.0); + carriageMotor.setSpeed(carriageMotor.maxSpeed()); + carriageMotor.setMinPulseWidth(50); +} + +void loop() { + // Accept new clients + if (!activeClient || !activeClient.connected()) { + activeClient = motorServer.available(); + } + + if (activeClient && activeClient.connected() && activeClient.available()) { + while (activeClient.available()) { + char c = activeClient.read(); + + if (c == '\n') { + recvBuffer.trim(); // Remove trailing \r or whitespace + if (recvBuffer.length() > 0) { + handleCommand(recvBuffer, activeClient); + } + recvBuffer = ""; // Reset for next command + } else { + recvBuffer += c; + // Optional: protect against overflow + if (recvBuffer.length() > 200) recvBuffer = ""; // Reset if too long + } + } + } + + // Main state machine + switch (currentSystemState) { + case IDLE: break; + case REELING: runReeling(); break; + case REEL_CALIBRATION: runCalibration(); break; + case SYSTEM_CALIBRATION: + if (systemCalibrationState == FIND_START) runFindStart(); + else if (systemCalibrationState == FIND_END) runFindEnd(); + break; + } +} + +void runFindStart() { + // Force the carriage motor to find the start switch by moving it positive direction, meaning toward the motor + if(carriageMotor.speed() <= 0){ + carriageMotor.setSpeed(carriageMotor.maxSpeed()); + } + + carriageMotor.runSpeed(); + + // + if (digitalRead(START_SW_PIN) == LOW) { + carriageMotor.stop(); + carriageMotor.setSpeed(0); + currentPositionMM = 0; // Start from zero + carriageMotor.setCurrentPosition(0); + carriageMotor.setSpeed(carriageMotor.maxSpeed()); // Always move forward from here + systemCalibrationState = FIND_END; + } +} + +void runFindEnd() { + + // Force the carriage motor to find the end switch by moving it negative direction, meaning away from the motor + if(carriageMotor.speed() >= 0){ + carriageMotor.setSpeed(carriageMotor.maxSpeed() * FORWARD); + } + + if (carriageMotor.runSpeed()) { + currentPositionMM += (1.0 / STEPS_PER_MM); // Calculate the new position when the motor actually does steps + } + + if (digitalRead(END_SW_PIN) == LOW) { + carriageMotor.stop(); + carriageMotor.setSpeed(0); + + railLength = currentPositionMM; // Assign the maximum length the motor can travel + targetPosition = railLength; + + currentSystemState = IDLE; + } +} + +void runReeling() { + + if(reelGapStartPosition < 0 || reelGapEndPosition < 0){ + // TODO : Maybe throw an error for the user + stopMotors(); + return; + } + + if (isWithinRange(currentPositionMM, reelGapStartPosition, reelGapEndPosition)) { + + if(reelMotor.speed() == 0){ + reelMotor.setSpeed(-reelMotor.maxSpeed()); + } + reelMotor.runSpeed(); + } + + // === Oscillate effect === + if (!isMovingToEnd) { + if (runMoveToPosition(reelGapStartPosition)) { + isMovingToEnd = true; + } + } else { + if (runMoveToPosition(reelGapEndPosition)) { + isMovingToEnd = false; + } + } +} + +void runCalibration() { + runMoveToPosition(targetPosition); +} + +bool runMoveToPosition(float position) { + + // If it want's to move out of bounds, stop the motors, maybe it will damage the setup + if(position < 0.0f || position > railLength){ + stopMotors(); + return false; + } + + float distance = abs(currentPositionMM - position); + float margin = 0.01f; + + if (distance <= margin) { + carriageMotor.setSpeed(0); + carriageMotor.stop(); + return true; + } + + int direction = sign(position - currentPositionMM); + int previousDirection = (carriageMotor.speed() / FORWARD) / carriageMotor.maxSpeed(); + + if (previousDirection != direction) { + carriageMotor.setSpeed(carriageMotor.maxSpeed() * direction * FORWARD); + } + + if (carriageMotor.runSpeed()) { + currentPositionMM += (carriageMotor.speed() > 0) ? (-1.0 / STEPS_PER_MM) : (1.0 / STEPS_PER_MM); + + if (distance <= margin) { + carriageMotor.setSpeed(0); + carriageMotor.stop(); + return true; + } + } + + return false; +} + +void stopMotors(){ + currentSystemState = IDLE; + carriageMotor.stop(); + carriageMotor.setSpeed(0); + reelMotor.stop(); + reelMotor.setSpeed(0); +} + +float calculateCarriageRPM(float reelRPM, float gapLengthMM) { + float timeForReelRevs = 4.0f * 60.0f / reelRPM; + float mmPerSec = gapLengthMM / timeForReelRevs; + float carriageRevsPerSec = mmPerSec / DISTANCE_PER_REVOLUTION; + return carriageRevsPerSec * 60.0f; +} + +bool isWithinRange(float currentPosition, float start, float end) { + return currentPosition >= start && currentPosition <= end; +} + +float sign(float value) { + return float((value > 0) - (value < 0)); +} + +String stateToString(SystemState systemState) { + switch(systemState) { + case IDLE: return "IDLE"; + case REELING: return "REELING"; + case REEL_CALIBRATION: return "REEL_CALIBRATION"; + case SYSTEM_CALIBRATION: return "SYSTEM_CALIBRATION"; + default: return "UNKNOWN"; + } +} + +void handleCommand(const String& cmd, WiFiClient& client) { + if (cmd.startsWith("starter-position-input")) { + float value; + int scanned = sscanf(cmd.c_str(), "starter-position-input %f", &value); + if (scanned == 1) { + reelGapStartPosition = value; + starterSet = true; + + if (starterSet && endSet) { + float gap = reelGapEndPosition - reelGapStartPosition; + float newCarriageRPM = calculateCarriageRPM(REEL_RPM, gap); + CARRIAGE_RPM = newCarriageRPM; + float maxSpeed = (newCarriageRPM * MOTOR_STEPS * MICROSTEPPING) / 60.0; + carriageMotor.setMaxSpeed(maxSpeed); + } + + client.println("OK"); + } else { + client.println("Invalid starter-position-input command"); + } + + } else if (cmd.startsWith("end-position-input")) { + float value; + int scanned = sscanf(cmd.c_str(), "end-position-input %f", &value); + if (scanned == 1) { + reelGapEndPosition = value; + endSet = true; + + if (starterSet && endSet) { + float gap = reelGapEndPosition - reelGapStartPosition; + float newCarriageRPM = calculateCarriageRPM(REEL_RPM, gap); + CARRIAGE_RPM = newCarriageRPM; + float maxSpeed = (newCarriageRPM * MOTOR_STEPS * MICROSTEPPING) / 60.0; + carriageMotor.setMaxSpeed(maxSpeed); + } + + client.println("OK"); + } else { + client.println("Invalid end-position-input command"); + } + } else if (cmd == "status") { + StaticJsonDocument<200> doc; + doc["position"] = currentPositionMM; + doc["state"] = stateToString(currentSystemState); + doc["railLength"] = railLength; + doc["starterSet"] = starterSet; + doc["endSet"] = endSet; + String json; + serializeJson(doc, json); + client.println(json); + + } else if (cmd == "calibrate") { + currentSystemState = SYSTEM_CALIBRATION; + systemCalibrationState = FIND_START; + client.println("System calibration started"); + + } else if (cmd == "reel_cal") { + if(currentSystemState != REEL_CALIBRATION){ + currentSystemState = REEL_CALIBRATION; + + } else { + currentSystemState = IDLE; + } + + if(starterSet && endSet){ + targetPosition = (reelGapEndPosition - reelGapStartPosition) / 2.0f; + }else { + targetPosition = 0; + } + + client.println("Reel calibration mode"); + + } else if (cmd == "start") { + if (starterSet && endSet) { + currentSystemState = REELING; + client.println("Started reeling"); + } else { + client.println("Error: Gap not set"); + } + + } else if (cmd == "stop") { + stopMotors(); + client.println("Stopped"); + } else if (cmd.startsWith("move")) { + float value; + int scanned = sscanf(cmd.c_str(), "move %f", &value); + if (scanned == 1) { + targetPosition = value; + currentSystemState = REEL_CALIBRATION; // Use existing logic to move to target + client.println("OK"); + } else { + client.println("Invalid end-position-input command"); + } + + }else { + client.println("Unknown command"); + } +} + diff --git a/Code/AFRS_webserver/AFRS_webserver.ino b/Code/AFRS_webserver/AFRS_webserver.ino new file mode 100644 index 0000000..e8e2b7b --- /dev/null +++ b/Code/AFRS_webserver/AFRS_webserver.ino @@ -0,0 +1,139 @@ +#include +#include +#include +#include + +// Wi-Fi credentials +const char* ssid = ""; +const char* password = ""; + +// Web server on port 80 +WebServer server(80); + +// TCP client for talking to motor ESP +WiFiClient motorClient; +IPAddress motorIP(192, 168, 1, 18); // Motor ESP IP (must be static or fixed by DHCP) + +bool starterSet = false; +bool endSet = false; + +// =================== COMMUNICATION =================== + +bool ensureConnected() { + if (!motorClient.connected()) { + return motorClient.connect(motorIP, 3333); // Port must match motor ESP + } + return true; +} + +String sendMotorCommand(const String& cmd) { + if (!ensureConnected()) return "Motor Not Connected"; + + motorClient.println(cmd); + motorClient.flush(); + + String response = ""; + unsigned long start = millis(); + while (millis() - start < 300) { + while (motorClient.available()) { + char c = motorClient.read(); + if (c == '\n') return response; + response += c; + } + } + return response; +} + +// =================== SETUP =================== + +void setup() { + Serial.begin(115200); + + // Connect to Wi-Fi + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println("\nConnected to Wi-Fi. IP: " + WiFi.localIP().toString()); + + if (!LittleFS.begin(true)) { + Serial.println("Failed to mount LittleFS"); + return; + } + + // Routes + server.on("/", handleRoot); + server.on("/cmd", handleCommand); + server.on("/move", handleMove); + server.on("/set", handleSet); + server.on("/status", handleStatus); + + server.begin(); +} + +void loop() { + server.handleClient(); +} + +// =================== WEB HANDLERS =================== + +void handleRoot() { + File file = LittleFS.open("/index.html", "r"); + if (!file) { + server.send(500, "text/plain", "Failed to load HTML file"); + return; + } + server.streamFile(file, "text/html"); + file.close(); +} + +void handleCommand() { + String command = server.arg("code"); + + String reply = sendMotorCommand(command); + server.send(200, "text/plain", reply.length() > 0 ? reply : "OK"); +} + +void handleSet() { + if (!server.hasArg("type") || !server.hasArg("val")) { + server.send(400, "text/plain", "Missing arguments"); + return; + } + + String type = server.arg("type"); + float value = server.arg("val").toFloat(); + + if (type == "starter-position-input") { + starterSet = true; + String cmd = "starter-position-input " + String(value, 2); + sendMotorCommand(cmd); + } else if (type == "end-position-input") { + endSet = true; + String cmd = "end-position-input " + String(value, 2); + sendMotorCommand(cmd); + } else { + server.send(400, "text/plain", "Invalid type"); + return; + } + + server.send(200, "text/plain", "OK"); +} + +void handleStatus() { + String json = sendMotorCommand("status"); + if (json.startsWith("{")) { + server.send(200, "application/json", json); + } else { + server.send(500, "text/plain", "Invalid status"); + } +} + +void handleMove() { + float position = server.arg("pos").toFloat(); + String cmd = "move " + String(position, 2); + sendMotorCommand(cmd); + + // MUST send a response to complete the request + server.send(200, "text/plain", "OK"); +} diff --git a/Code/AFRS_webserver/data/index.html b/Code/AFRS_webserver/data/index.html new file mode 100644 index 0000000..77faf86 --- /dev/null +++ b/Code/AFRS_webserver/data/index.html @@ -0,0 +1,235 @@ + + + + + AFRS 1.3 + + + +
+
State : IDLE
+
+
+
Carriage Position : 0.0 mm
+
+ +
+ +
+ +
+ +
+
+ Target Position : + + + +
+
+ + +
+
+ Start: + + + End: + + +
+
+ + +
+ + + + \ No newline at end of file