Explorar o código

first working iteration

Tuan Nguyen hai 1 ano
achega
16bfef09a1
Modificáronse 4 ficheiros con 593 adicións e 0 borrados
  1. 142 0
      arduino_code/arduino_code.ino
  2. 3 0
      requirements.txt
  3. 215 0
      templates/theta_rho_controller.html
  4. 233 0
      theta_rho_app.py

+ 142 - 0
arduino_code/arduino_code.ino

@@ -0,0 +1,142 @@
+#include <AccelStepper.h>
+#include <MultiStepper.h>
+#include <math.h>  // For M_PI and mathematical operations
+
+#define rotInterfaceType AccelStepper::DRIVER
+#define inOutInterfaceType AccelStepper::DRIVER
+
+#define stepPin_rot 2
+#define dirPin_rot 5
+#define stepPin_InOut 3
+#define dirPin_InOut 6
+
+#define rot_total_steps 16000.0
+#define inOut_total_steps 5760.0
+
+#define BUFFER_SIZE 10  // Maximum number of theta-rho pairs in a batch
+
+// Create stepper motor objects
+AccelStepper rotStepper(rotInterfaceType, stepPin_rot, dirPin_rot);
+AccelStepper inOutStepper(inOutInterfaceType, stepPin_InOut, dirPin_InOut);
+
+// Create a MultiStepper object
+MultiStepper multiStepper;
+
+// Buffer for storing theta-rho pairs
+float buffer[BUFFER_SIZE][2];  // Store theta, rho pairs
+int bufferCount = 0;   // Number of pairs in the buffer
+bool batchComplete = false;
+
+void setup() {
+    // Set maximum speed and acceleration
+    rotStepper.setMaxSpeed(2000);  // Adjust as needed
+    rotStepper.setAcceleration(1);  // Adjust as needed
+
+    inOutStepper.setMaxSpeed(2000);  // Adjust as needed
+    inOutStepper.setAcceleration(1);  // Adjust as needed
+    // inOutStepper.setPinsInverted(true, false, false);
+
+    // Add steppers to MultiStepper
+    multiStepper.addStepper(rotStepper);
+    multiStepper.addStepper(inOutStepper);
+
+    // Initialize serial communication
+    Serial.begin(115200);
+    Serial.println("READY");
+    homing();
+}
+
+void loop() {
+    // Check for incoming serial commands or theta-rho pairs
+    if (Serial.available() > 0) {
+        String input = Serial.readStringUntil('\n');
+
+        // Ignore invalid messages
+        if (input != "HOME" && !input.endsWith(";")) {
+            Serial.println("IGNORED");
+            return;
+        }
+
+        if (input == "HOME") {
+            homing();
+            return;
+        }
+
+        // If not a command, assume it's a batch of theta-rho pairs
+        if (!batchComplete) {
+            int pairIndex = 0;
+            int startIdx = 0;
+
+            // Split the batch line into individual theta-rho pairs
+            while (pairIndex < BUFFER_SIZE) {
+                int endIdx = input.indexOf(";", startIdx);
+                if (endIdx == -1) break;  // No more pairs in the line
+
+                String pair = input.substring(startIdx, endIdx);
+                int commaIndex = pair.indexOf(',');
+
+                // Parse theta and rho values
+                float theta = pair.substring(0, commaIndex).toFloat();  // Theta in radians
+                float rho = pair.substring(commaIndex + 1).toFloat();   // Rho (0 to 1)
+
+                buffer[pairIndex][0] = theta;
+                buffer[pairIndex][1] = rho;
+                pairIndex++;
+
+                startIdx = endIdx + 1; // Move to next pair
+            }
+            bufferCount = pairIndex;
+            batchComplete = true;
+        }
+    }
+
+    // Process the buffer if a batch is ready
+    if (batchComplete && bufferCount > 0) {
+        for (int i = 0; i < bufferCount; i++) {
+            movePolar(buffer[i][0], buffer[i][1]);
+        }
+        bufferCount = 0;        // Clear buffer
+        batchComplete = false;  // Reset batch flag
+        Serial.println("READY");
+    }
+}
+
+void homing() {
+    Serial.println("HOMING");
+
+    // Move inOutStepper inward for homing
+    inOutStepper.setSpeed(-5000);  // Adjust speed for homing
+    while (true) {
+        inOutStepper.runSpeed();
+        if (inOutStepper.currentPosition() <= -5760 * 1.1) {  // Adjust distance for homing
+            break;
+        }
+    }
+    inOutStepper.setCurrentPosition(0);  // Set home position
+    Serial.println("HOMED");
+}
+
+void movePolar(float theta, float rho) {
+    // Convert polar coordinates to motor steps
+    long rotSteps = theta * (rot_total_steps / (2.0 * M_PI));  // Steps for rot axis
+    long inOutSteps = rho * inOut_total_steps;                 // Steps for in-out axis
+
+    // Calculate offset for inOut axis
+    float revolutions = theta / (2.0 * M_PI);  // Fractional revolutions (can be positive or negative)
+    long offsetSteps = revolutions * 1600;    // 1600 steps inward or outward per revolution
+
+    // Apply the offset to the inout axis
+    inOutSteps += offsetSteps;
+
+    // Define target positions for both motors
+    long targetPositions[2];
+    targetPositions[0] = rotSteps;
+    targetPositions[1] = inOutSteps;
+
+    // Move both motors synchronously
+    multiStepper.moveTo(targetPositions);
+    unsigned long lastStepTime = 0;
+    const unsigned long stepInterval = 10;  // 10 ms between checks
+
+    multiStepper.runSpeedToPosition();  // Blocking call
+}

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+flask
+flask-socketio
+pyserial

+ 215 - 0
templates/theta_rho_controller.html

@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Theta-Rho Controller</title>
+    <style>
+        #theta_rho_files li {
+            cursor: pointer;
+        }
+
+        #theta_rho_files li.selected {
+            font-weight: bold;
+            color: green;
+        }
+
+        .status {
+            margin-top: 1em;
+            font-weight: bold;
+        }
+
+        #serial_status {
+            color: blue;
+        }
+
+    </style>
+</head>
+<body>
+    <h1>Theta-Rho Controller</h1>
+
+    <h2>Serial Connection</h2>
+    <label for="serial_ports">Available Ports:</label>
+    <select id="serial_ports"></select>
+    <button onclick="connectSerial()">Connect</button>
+    <button onclick="disconnectSerial()">Disconnect</button>
+    <button onclick="restartSerial()">Restart</button>
+    <p id="serial_status" class="status">Status: Not connected</p>
+
+    <h2>Theta-Rho Files</h2>
+    <ul id="theta_rho_files"></ul>
+    <input type="file" id="upload_file">
+    <button onclick="uploadThetaRho()">Upload</button>
+
+    <h2>Run Theta-Rho</h2>
+    <button id="run_button" disabled>Run Selected File</button>
+    <button onclick="stopExecution()">Stop</button>
+
+    <h2>Quick Actions</h2>
+    <button onclick="sendHomeCommand()">Home Device</button>
+    <button onclick="runClearIn()">Clear from in</button>
+    <button onclick="runClearOut()">Clear from out</button>
+
+    <script>
+        let selectedFile = null;
+
+        async function loadSerialPorts() {
+            const response = await fetch('/list_serial_ports');
+            const ports = await response.json();
+            const select = document.getElementById('serial_ports');
+            select.innerHTML = '';
+            ports.forEach(port => {
+                const option = document.createElement('option');
+                option.value = port;
+                option.textContent = port;
+                select.appendChild(option);
+            });
+        }
+
+        async function connectSerial() {
+            const port = document.getElementById('serial_ports').value;
+            const response = await fetch('/connect_serial', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ port })
+            });
+            const result = await response.json();
+            if (result.success) {
+                document.getElementById('serial_status').textContent = `Status: Connected to ${port}`;
+                alert('Connected to serial port!');
+            } else {
+                alert(`Error connecting to serial port: ${result.error}`);
+            }
+        }
+
+        async function disconnectSerial() {
+            const response = await fetch('/disconnect_serial', { method: 'POST' });
+            const result = await response.json();
+            if (result.success) {
+                document.getElementById('serial_status').textContent = 'Status: Disconnected';
+                alert('Serial port disconnected!');
+            } else {
+                alert(`Error disconnecting: ${result.error}`);
+            }
+        }
+
+        async function restartSerial() {
+            const port = document.getElementById('serial_ports').value;
+            const response = await fetch('/restart_serial', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ port })
+            });
+            const result = await response.json();
+            if (result.success) {
+                document.getElementById('serial_status').textContent = `Status: Restarted connection to ${port}`;
+                alert('Serial connection restarted!');
+            } else {
+                alert(`Error restarting serial connection: ${result.error}`);
+            }
+        }
+
+        async function loadThetaRhoFiles() {
+            const response = await fetch('/list_theta_rho_files');
+            const files = await response.json();
+            const ul = document.getElementById('theta_rho_files');
+            ul.innerHTML = '';
+            files.forEach(file => {
+                const li = document.createElement('li');
+                li.textContent = file;
+                li.onclick = () => selectFile(file, li);
+                ul.appendChild(li);
+            });
+        }
+
+        function selectFile(file, listItem) {
+            selectedFile = file;
+            document.querySelectorAll('#theta_rho_files li').forEach(li => li.classList.remove('selected'));
+            listItem.classList.add('selected');
+            document.getElementById('run_button').disabled = false;
+        }
+
+        async function uploadThetaRho() {
+            const fileInput = document.getElementById('upload_file');
+            const file = fileInput.files[0];
+            const formData = new FormData();
+            formData.append('file', file);
+
+            const response = await fetch('/upload_theta_rho', {
+                method: 'POST',
+                body: formData
+            });
+
+            const result = await response.json();
+            if (result.success) {
+                alert('File uploaded successfully!');
+                loadThetaRhoFiles();
+            } else {
+                alert('Failed to upload file.');
+            }
+        }
+
+        async function runThetaRho() {
+            if (!selectedFile) return;
+
+            const response = await fetch('/run_theta_rho', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ file_name: selectedFile })
+            });
+
+            const result = await response.json();
+            if (result.success) {
+                alert(`Running Theta-Rho file: ${selectedFile}`);
+            } else {
+                alert(`Error starting Theta-Rho execution: ${result.error}`);
+            }
+        }
+
+        async function stopExecution() {
+            const response = await fetch('/stop_execution', { method: 'POST' });
+            const result = await response.json();
+            if (result.success) {
+                alert('Execution stopped.');
+            } else {
+                alert('Failed to stop execution.');
+            }
+        }
+
+        async function sendHomeCommand() {
+            const response = await fetch('/send_home', { method: 'POST' });
+            const result = await response.json();
+            if (result.success) {
+                alert('HOME command sent successfully!');
+            } else {
+                alert('Failed to send HOME command.');
+            }
+        }
+
+        async function runClearIn() {
+            await runFile('clear_from_in.thr');
+        }
+
+        async function runClearOut() {
+            await runFile('clear_from_out.thr');
+        }
+
+        async function runFile(fileName) {
+            const response = await fetch(`/run_theta_rho_file/${fileName}`, { method: 'POST' });
+            const result = await response.json();
+            if (result.success) {
+                alert(`Running file: ${fileName}`);
+            } else {
+                alert(`Failed to run file: ${fileName}`);
+            }
+        }
+
+        // Initial load of serial ports and Theta-Rho files
+        loadSerialPorts();
+        loadThetaRhoFiles();
+
+        // Attach runThetaRho function to the Run button
+        document.getElementById('run_button').onclick = runThetaRho;
+    </script>
+</body>
+</html>

+ 233 - 0
theta_rho_app.py

@@ -0,0 +1,233 @@
+from flask import Flask, request, jsonify, render_template
+import os
+import serial
+import time
+import threading
+import serial.tools.list_ports
+import math
+
+app = Flask(__name__)
+
+# Theta-rho directory
+THETA_RHO_DIR = './theta_rho_files'
+os.makedirs(THETA_RHO_DIR, exist_ok=True)
+
+# Serial connection (default None, will be set by user)
+ser = None
+stop_requested = False
+
+def list_serial_ports():
+    """Return a list of available serial ports."""
+    ports = serial.tools.list_ports.comports()
+    return [port.device for port in ports]
+
+def connect_to_serial(port, baudrate=115200):
+    """Connect to the specified serial port."""
+    global ser
+    if ser and ser.is_open:
+        ser.close()
+    ser = serial.Serial(port, baudrate)
+    time.sleep(2)  # Allow time for the connection to establish
+
+def disconnect_serial():
+    """Disconnect the current serial connection."""
+    global ser
+    if ser and ser.is_open:
+        ser.close()
+        ser = None
+
+def restart_serial(port, baudrate=115200):
+    """Restart the serial connection."""
+    disconnect_serial()
+    connect_to_serial(port, baudrate)
+
+def parse_theta_rho_file(file_path):
+    """Parse a theta-rho file and return a list of (theta, rho) pairs."""
+    coordinates = []
+    with open(file_path, 'r') as file:
+        for line in file:
+            line = line.strip()
+            
+            # Skip header or comment lines (starting with '#' or empty lines)
+            if not line or line.startswith("#"):
+                print(f"Skipping invalid line: {line}")
+                continue
+
+            # Parse lines with theta and rho separated by spaces
+            try:
+                theta, rho = map(float, line.split())
+                coordinates.append((theta, rho))
+            except ValueError:
+                print(f"Skipping invalid line: {line}")
+    return coordinates
+
+def send_coordinate_batch(ser, coordinates):
+    """Send a batch of theta-rho pairs to the Arduino."""
+    print("Sending batch:", coordinates)
+    batch_str = ";".join(f"{theta:.3f},{rho:.3f}" for theta, rho in coordinates) + ";\n"
+    ser.write(batch_str.encode())
+
+def send_command(command):
+    """Send a single command to the Arduino."""
+    ser.write(f"{command}\n".encode())
+    print(f"Sent: {command}")
+    
+    # Wait for "DONE" acknowledgment from Arduino
+    while True:
+        if ser.in_waiting > 0:
+            response = ser.readline().decode().strip()
+            print(f"Arduino response: {response}")
+            if response == "DONE":
+                print("Command execution completed.")
+                break
+        time.sleep(0.5)  # Small delay to avoid busy waiting
+
+def interpolate_path(start, end, step_size=0.001):
+    """Interpolate a straight path between two theta-rho points with a fixed step size."""
+    start_theta, start_rho = start
+    end_theta, end_rho = end
+
+    # Calculate the total distance in the polar coordinate system
+    distance = math.sqrt((end_theta - start_theta)**2 + (end_rho - start_rho)**2)
+    num_steps = max(1, int(distance / step_size))  # Ensure at least one step
+
+    interpolated_points = []
+    for step in range(num_steps + 1):
+        t = step / num_steps  # Interpolation factor (0 to 1)
+        theta = start_theta + t * (end_theta - start_theta)
+        rho = start_rho + t * (end_rho - start_rho)
+        interpolated_points.append((theta, rho))
+
+    return interpolated_points
+
+def run_theta_rho_file(file_path):
+    """Run a theta-rho file by interpolating straight paths and sending data in optimized batches."""
+    global stop_requested
+    stop_requested = False
+
+    coordinates = parse_theta_rho_file(file_path)
+    if len(coordinates) < 2:
+        print("Not enough coordinates for interpolation.")
+        return
+
+    # Interpolate paths between points with fine resolution
+    step_size = 0.005  # Smaller values create finer steps for smoother movement
+    interpolated_coordinates = []
+    for i in range(len(coordinates) - 1):
+        interpolated_coordinates.extend(interpolate_path(coordinates[i], coordinates[i + 1], step_size=step_size))
+
+    # Optimize batch size for smoother execution
+    batch_size = 20  # Smaller batches may smooth movement further
+    for i in range(0, len(interpolated_coordinates), batch_size):
+        if stop_requested:
+            print("Execution stopped by user.")
+            break
+
+        batch = interpolated_coordinates[i:i + batch_size]
+        while True:
+            if ser.in_waiting > 0:
+                response = ser.readline().decode().strip()
+                print(f"Arduino response: {response}")
+                if response == "READY":
+                    send_coordinate_batch(ser, batch)
+                    break
+
+@app.route('/')
+def index():
+    return render_template('theta_rho_controller.html')
+
+@app.route('/list_serial_ports', methods=['GET'])
+def list_ports():
+    return jsonify(list_serial_ports())
+
+@app.route('/connect_serial', methods=['POST'])
+def connect_serial():
+    port = request.json.get('port')
+    if not port:
+        return jsonify({'error': 'No port provided'}), 400
+
+    try:
+        connect_to_serial(port)
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'error': str(e)}), 500
+
+@app.route('/disconnect_serial', methods=['POST'])
+def disconnect():
+    try:
+        disconnect_serial()
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'error': str(e)}), 500
+
+@app.route('/restart_serial', methods=['POST'])
+def restart():
+    port = request.json.get('port')
+    if not port:
+        return jsonify({'error': 'No port provided'}), 400
+
+    try:
+        restart_serial(port)
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'error': str(e)}), 500
+
+@app.route('/list_theta_rho_files', methods=['GET'])
+def list_theta_rho_files():
+    files = os.listdir(THETA_RHO_DIR)
+    return jsonify(files)
+
+@app.route('/upload_theta_rho', methods=['POST'])
+def upload_theta_rho():
+    file = request.files['file']
+    if file:
+        file.save(os.path.join(THETA_RHO_DIR, file.filename))
+        return jsonify({'success': True})
+    return jsonify({'success': False})
+
+@app.route('/run_theta_rho', methods=['POST'])
+def run_theta_rho():
+    file_name = request.json.get('file_name')
+    if not file_name:
+        return jsonify({'error': 'No file name provided'}), 400
+
+    file_path = os.path.join(THETA_RHO_DIR, file_name)
+    if not os.path.exists(file_path):
+        return jsonify({'error': 'File not found'}), 404
+
+    threading.Thread(target=run_theta_rho_file, args=(file_path,)).start()
+    return jsonify({'success': True})
+
+@app.route('/stop_execution', methods=['POST'])
+def stop_execution():
+    global stop_requested
+    stop_requested = True
+    return jsonify({'success': True})
+
+@app.route('/send_home', methods=['POST'])
+def send_home():
+    """Send the HOME command to the Arduino."""
+    try:
+        send_command("HOME")
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'error': str(e)}), 500
+
+@app.route('/run_theta_rho_file/<file_name>', methods=['POST'])
+def run_specific_theta_rho_file(file_name):
+    """Run a specific theta-rho file."""
+    file_path = os.path.join(THETA_RHO_DIR, file_name)
+    if not os.path.exists(file_path):
+        return jsonify({'error': 'File not found'}), 404
+
+    threading.Thread(target=run_theta_rho_file, args=(file_path,)).start()
+    return jsonify({'success': True})
+
+# Expose files for download if needed
+@app.route('/download/<filename>', methods=['GET'])
+def download_file(filename):
+    """Download a file from the theta-rho directory."""
+    return send_from_directory(THETA_RHO_DIR, filename)
+
+if __name__ == '__main__':
+    app.run(debug=True, host='0.0.0.0', port=8080)