浏览代码

Added Auto Connect and UI improvement (#3)

* Added Auto Connect and Default Pattern functionality
Improved UX and added Changelog file

* Fix indent issue

* Moved serial_lock to correct position after solving merge conflict

* Minor codestyle fixes

* Removed default pattern functionality.
Moved connect_to_serial() after app.run

* Added missing closing div and await

* remove retry logic, modify changelog

* fix changelog

---------

Co-authored-by: Tuan Nguyen <anhtuan.nguyen@me.com>
Thom Koopman 1 年之前
父节点
当前提交
5631a63350
共有 6 个文件被更改,包括 280 次插入112 次删除
  1. 1 0
      .gitignore
  2. 30 0
      CHANGELOG.md
  3. 42 21
      app.py
  4. 二进制
      static/UI.png
  5. 79 15
      static/style.css
  6. 128 76
      templates/index.html

+ 1 - 0
.gitignore

@@ -3,3 +3,4 @@ __pycache__/
 *.pyc
 *.pyo
 .env
+.idea

+ 30 - 0
CHANGELOG.md

@@ -0,0 +1,30 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [1.1.0] - Default Pattern functionality
+
+### Added
+- **Auto-connect Serial Connection when app is started**
+    - Automatically selected the first available serial port if none was specified.
+- **Added Footer with:**
+  - Links to github
+  - Toggle button to show/hide the debug log
+
+### Changed
+- **Improved UI**
+  - Certain buttons are now only visible when it's applicable for the current state.
+  - Moved Stop button and Speed input to Quick Actions**
+- **Pattern file prioritization:**
+    - Updated the `/list_theta_rho_files` endpoint to:
+        - Only display files with the `.thr` extension.
+        - Include `custom_patterns/default_pattern.thr` at the top of the list if it exists.
+        - Prioritize files in the `custom_patterns/` folder over other files.
+
+## [1.0.0] - Initial Version
+- Initial implementation of the Flask application to control the Dune Weaver sand table.
+- Added core functionality for:
+    - Serial port connection and management.
+    - Parsing `.thr` files (theta-rho format).
+    - Executing patterns via Arduino.
+    - Basic Flask endpoints for listing, uploading, and running `.thr` files.

+ 42 - 21
app.py

@@ -8,12 +8,12 @@ import math
 
 app = Flask(__name__)
 
-# Theta-rho directory
+# Configuration
 THETA_RHO_DIR = './patterns'
 IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
 os.makedirs(THETA_RHO_DIR, exist_ok=True)
 
-# Serial connection (default None, will be set by user)
+# Serial connection (First available will be selected by default)
 ser = None
 ser_port = None  # Global variable to store the serial port name
 stop_requested = False
@@ -24,15 +24,32 @@ def list_serial_ports():
     ports = serial.tools.list_ports.comports()
     return [port.device for port in ports if port.device not in IGNORE_PORTS]
 
-def connect_to_serial(port, baudrate=115200):
-    """Connect to the specified serial port."""
+def connect_to_serial(port=None, baudrate=115200):
+    """Automatically connect to the first available serial port or a specified port."""
     global ser, ser_port
-    with serial_lock:
-        if ser and ser.is_open:
-            ser.close()
-        ser = serial.Serial(port, baudrate)
-        ser_port = port  # Store the connected port globally
+
+    try:
+        if port is None:
+            ports = list_serial_ports()
+            if not ports:
+                print("No serial port connected")
+                return False
+            port = ports[0]  # Auto-select the first available port
+
+        with serial_lock:
+            if ser and ser.is_open:
+                ser.close()
+            ser = serial.Serial(port, baudrate)
+            ser_port = port  # Store the connected port globally
+        print(f"Connected to serial port: {port}")
         time.sleep(2)  # Allow time for the connection to establish
+        return True  # Successfully connected
+    except serial.SerialException as e:
+        print(f"Failed to connect to serial port {port}: {e}")
+        port = None  # Reset the port to try the next available one
+
+    print("Max retries reached. Could not connect to a serial port.")
+    return False
 
 def disconnect_serial():
     """Disconnect the current serial connection."""
@@ -81,7 +98,7 @@ 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:
         with serial_lock:
@@ -91,7 +108,6 @@ def send_command(command):
                 if response == "DONE":
                     print("Command execution completed.")
                     break
-        # time.sleep(0.5)  # Small delay to avoid busy waiting
 
 def run_theta_rho_file(file_path):
     """Run a theta-rho file by sending data in optimized batches."""
@@ -124,12 +140,13 @@ def run_theta_rho_file(file_path):
                         break
                     else:
                         print(f"Arduino response: {response}")
-                
+
     # Reset theta after execution or stopping
     reset_theta()
     ser.write("FINISHED\n".encode())
-                
+
 def reset_theta():
+    """Reset theta on the Arduino."""
     ser.write("RESET_THETA\n".encode())
     while True:
         with serial_lock:
@@ -141,7 +158,7 @@ def reset_theta():
                     break
         time.sleep(0.5)  # Small delay to avoid busy waiting
 
-# Flask endpoints
+# Flask API Endpoints
 @app.route('/')
 def index():
     return render_template('index.html')
@@ -233,7 +250,6 @@ def run_theta_rho():
         return jsonify({'success': True})
     except Exception as e:
         return jsonify({'error': str(e)}), 500
-    
 
 @app.route('/stop_execution', methods=['POST'])
 def stop_execution():
@@ -291,7 +307,7 @@ def move_to_center():
         return jsonify({"success": True})
     except Exception as e:
         return jsonify({"success": False, "error": str(e)}), 500
-    
+
 @app.route('/move_to_perimeter', methods=['POST'])
 def move_to_perimeter():
     """Move the sand table to the perimeter position."""
@@ -305,11 +321,11 @@ def move_to_perimeter():
         return jsonify({"success": True})
     except Exception as e:
         return jsonify({"success": False, "error": str(e)}), 500
-    
+
 @app.route('/preview_thr', methods=['POST'])
 def preview_thr():
     file_name = request.json.get('file_name')
-    
+
     if not file_name:
         return jsonify({'error': 'No file name provided'}), 400
 
@@ -323,7 +339,8 @@ def preview_thr():
         return jsonify({'success': True, 'coordinates': coordinates})
     except Exception as e:
         return jsonify({'error': str(e)}), 500
-    
+
+
 @app.route('/send_coordinate', methods=['POST'])
 def send_coordinate():
     """Send a single (theta, rho) coordinate to the Arduino."""
@@ -359,7 +376,7 @@ def serial_status():
         'connected': ser.is_open if ser else False,
         'port': ser_port  # Include the port name
     })
-    
+
 @app.route('/set_speed', methods=['POST'])
 def set_speed():
     """Set the speed for the Arduino."""
@@ -386,5 +403,9 @@ def set_speed():
         return jsonify({"success": False, "error": str(e)}), 500
 
 if __name__ == '__main__':
-    # Start the thread for reading Arduino responses    
+    # Auto-connect to serial
+    connect_to_serial()
+    
+    # Start the Flask app
     app.run(debug=True, host='0.0.0.0', port=8080)
+

二进制
static/UI.png


+ 79 - 15
static/style.css

@@ -7,6 +7,10 @@ body {
     color: #333;
 }
 
+body * {
+    box-sizing: border-box;
+}
+
 h1 {
     text-align: center;
     color: #4A90E2;
@@ -58,6 +62,10 @@ h2 {
     padding: 10px 15px;
 }
 
+.button-group label {
+    align-content: center;
+}
+
 button {
     background: #4A90E2;
     color: #fff;
@@ -74,6 +82,10 @@ button:hover {
     background: #357ABD;
 }
 
+.small-button {
+    font-size: 0.8rem;
+}
+
 /* Delete Button */
 .delete-button {
     background: #e74c3c; /* Red color */
@@ -131,11 +143,8 @@ li.selected {
 /* Status Log */
 #status_log {
     display: none;
-    position: fixed;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    background: #fff;
+    background: #000;
+    font-family: monospace;
     border-top: 1px solid #ddd;
     padding: 10px;
     max-height: 150px;
@@ -143,13 +152,14 @@ li.selected {
     box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
     -webkit-overflow-scrolling: touch; /* Enables smooth scrolling on mobile */
     touch-action: auto; /* Ensures touch gestures are handled properly */
+    width: 100%;
 }
 
 
 #status_log p {
-    margin: 0.5em 0;
+    margin: 0;
     font-size: 0.9em;
-    color: #444;
+    color: #ddd;
 }
 
 #theta_rho_files {
@@ -228,19 +238,20 @@ input[type="radio"]:checked {
     box-shadow: 0 0 3px rgba(74, 144, 226, 0.5);
 }
 
+#speed_input {
+    min-width: 80px;
+}
+
 .coordinate-input {
     display: flex;
     align-items: center;
     gap: 10px;
     margin-top: 10px;
 }
-.coordinate-input label, 
-.coordinate-input input, 
-.coordinate-input button {
+
+.coordinate-input input {
     font-size: 1rem;
     padding: 5px;
-}
-.coordinate-input input {
     width: 80px;
 }
 
@@ -300,8 +311,61 @@ input[type="radio"]:checked {
     color: #444;
 }
 
+#serial_ports_container > * {
+    display: inline-block;
+}
+
+#serial_ports_container select {
+    margin: 10px;
+    flex-basis: 100px;
+    flex-grow: 0;
+}
+
+#serial_ports {
+    width: auto;
+    min-width: 200px;
+}
+
+#serial_status_container,
+#serial_ports_buttons {
+    display: inline-block;
+}
+
+.status.connected {
+    color: #4CAF50;
+    font-weight: bold;
+}
+
+.status.not-connected {
+    color: #E53935;
+    font-weight: bold;
+}
+
+.footer {
+    align-items: center;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    width: 100%;
+}
+
+.footer #github {
+    align-content: center;
+    display: flex;
+    font-size: 0.8em;
+}
+
+.footer #github img {
+    margin: 0 5px
+}
+
 /* Responsive Layout for Small Screens */
 @media (max-width: 768px) {
+    body {
+        font-size: 0.9rem;
+    }
+
     .container {
         flex-direction: column; /* Stack columns vertically */
     }
@@ -310,7 +374,7 @@ input[type="radio"]:checked {
         width: 100%;
     }
 
-    #status_log {
-        display: none; /* Completely hides the status log on small screens */
+    .footer {
+        flex-direction: column;
     }
-}
+}

+ 128 - 76
templates/index.html

@@ -14,16 +14,16 @@
         <div class="left-column">
             <div class="section">
                 <h2>Serial Connection</h2>
-                <label for="serial_ports">Available Ports:</label>
-                <select id="serial_ports"></select>
-                <div class="button-group">
+                <div id="serial_status_container">Status: <span id="serial_status" class="status"> Not connected</span></div>
+                <div id="serial_ports_container">
+                    <label for="serial_ports">Available Ports:</label>
+                    <select id="serial_ports"></select>
                     <button onclick="connectSerial()">Connect</button>
+                </div>
+                <div id="serial_ports_buttons" class="button-group">
                     <button onclick="disconnectSerial()">Disconnect</button>
                     <button onclick="restartSerial()">Restart</button>
                 </div>
-                <p id="serial_status" class="status">Status: Not connected</p>
-                <input type="number" id="speed_input" placeholder="Set speed" min="1" step="1">
-                <button onclick="changeSpeed()">Set Speed</button>
             </div>
 
             <div class="section">
@@ -32,21 +32,26 @@
                     <button onclick="sendHomeCommand()">Home Device</button>
                     <button onclick="moveToCenter()">Move to Center</button>
                     <button onclick="moveToPerimeter()">Move to Perimeter</button>
-                </br>
                     <button onclick="runClearIn()">Clear from In</button>
                     <button onclick="runClearOut()">Clear from Out</button>
-                    <div class="coordinate-input">
-                        <label for="theta_input">θ:</label>
-                        <input type="number" id="theta_input" placeholder="Theta">
-                        <label for="rho_input">ρ:</label>
-                        <input type="number" id="rho_input" placeholder="Rho">
-                        <button onclick="sendCoordinate()">Send</button>
-                    </div>
+                    <button onclick="stopExecution()" class="delete-button">Stop</button>
+                </div>
+                <div class="coordinate-input button-group">
+                    <label for="theta_input">θ:</label>
+                    <input type="number" id="theta_input" placeholder="Theta">
+                    <label for="rho_input">ρ:</label>
+                    <input type="number" id="rho_input" placeholder="Rho">
+                    <button class="small-button" onclick="sendCoordinate()">Send to coordinate</button>
+                </div>
+                <div class="button-group">
+                    <label for="speed_input">Speed:</label>
+                    <input type="number" id="speed_input" placeholder="1-5000" min="1" step="1" max="5000">
+                    <button class="small-button"  onclick="changeSpeed()">Set Speed</button>
                 </div>
             </div>
             <div class="section">
                 <h2>Preview</h2>
-                <canvas id="patternPreviewCanvas" style="width: 100%; height: 100%;"></canvas>
+                <canvas id="patternPreviewCanvas" style="width: 100%;"></canvas>
                 <p id="first_coordinate">First Coordinate: Not available</p>
                 <p id="last_coordinate">Last Coordinate: Not available</p>
             </div>
@@ -71,8 +76,7 @@
                     </label>
                 </div>
                 <div class="button-group">
-                    <button id="run_button" disabled>Run Selected File</button>
-                    <button onclick="stopExecution()" class="delete-button">Stop</button>
+                    <button id="run_button" disabled>Run Selected Pattern</button>
                 </div>
             </div>
             <div class="section">
@@ -85,15 +89,25 @@
                     </div>
                 </div>
             </div>
+        </div>
+
+        <div class="footer">
+            <div id="github">
+                <span>Help us improve! <a href="https://github.com/tuanchris/dune-weaver/pulls" target="_blank">Submit a Pull Request</a> or <a href="https://github.com/tuanchris/dune-weaver/issues/new" target="_blank">Report a Bug</a>.</span>
+                <a href="https://github.com/tuanchris/dune-weaver/issues" target="_blank">
+                    <img src="https://img.shields.io/github/issues/tuanchris/dune-weaver?style=flat-square" alt="GitHub Issues">
+                </a>
             </div>
+
+            <button id="debug_button" class="small-button" onclick="toggleDebugLog()">
+                Show Debug log
+            </button>
         </div>
-    </div>
 
-    <div id="status_log">
-        <h2>Status Log</h2>
-        <!-- Messages will be appended here -->
+        <div id="status_log">
+            <!-- Messages will be appended here -->
+        </div>
     </div>
-
     <script>
         let selectedFile = null;
 
@@ -105,39 +119,19 @@
             log.scrollTop = log.scrollHeight; // Keep log scrolled to the bottom
         }
 
-        async function loadThetaRhoFiles() {
-            logMessage('Loading Theta-Rho files...');
-            const response = await fetch('/list_theta_rho_files');
-            const files = await response.json();
-            const ul = document.getElementById('theta_rho_files');
-            ul.innerHTML = ''; // Clear current list
-
-            files.forEach(file => {
-                const li = document.createElement('li');
-                li.textContent = file;
-
-                // Highlight the selected file when clicked
-                li.onclick = () => selectFile(file, li);
-
-                ul.appendChild(li);
-            });
-
-            logMessage('Theta-Rho files loaded successfully.');
-        }
-
         async function selectFile(file, listItem) {
             selectedFile = file;
-        
+
             // Highlight the selected file
             document.querySelectorAll('#theta_rho_files li').forEach(li => li.classList.remove('selected'));
             listItem.classList.add('selected');
-        
+
             // Enable buttons
             document.getElementById('run_button').disabled = false;
             document.getElementById('delete_selected_button').disabled = false;
-        
+
             logMessage(`Selected file: ${file}`);
-        
+
             // Fetch and preview the selected file
             await previewPattern(file);
         }
@@ -162,7 +156,7 @@
             const result = await response.json();
             if (result.success) {
                 logMessage(`File uploaded successfully: ${file.name}`);
-                loadThetaRhoFiles();
+                await loadThetaRhoFiles();
             } else {
                 logMessage(`Failed to upload file: ${file.name}`);
             }
@@ -205,17 +199,17 @@
                 logMessage("No file selected to run.");
                 return;
             }
-        
+
             // Get the selected pre-execution action
             const preExecutionAction = document.querySelector('input[name="pre_execution"]:checked').value;
-        
+
             logMessage(`Running file: ${selectedFile} with pre-execution action: ${preExecutionAction}...`);
             const response = await fetch('/run_theta_rho', {
                 method: 'POST',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({ file_name: selectedFile, pre_execution: preExecutionAction })
             });
-        
+
             const result = await response.json();
             if (result.success) {
                 logMessage(`File running: ${selectedFile}`);
@@ -258,8 +252,9 @@
             });
             const result = await response.json();
             if (result.success) {
-                document.getElementById('serial_status').textContent = `Status: Connected to ${port}`;
                 logMessage(`Connected to serial port: ${port}`);
+                // Refresh the status
+                await checkSerialStatus();
             } else {
                 logMessage(`Error connecting to serial port: ${result.error}`);
             }
@@ -269,8 +264,9 @@
             const response = await fetch('/disconnect_serial', { method: 'POST' });
             const result = await response.json();
             if (result.success) {
-                document.getElementById('serial_status').textContent = 'Status: Disconnected';
                 logMessage('Serial port disconnected.');
+                // Refresh the status
+                await checkSerialStatus();
             } else {
                 logMessage(`Error disconnecting: ${result.error}`);
             }
@@ -285,12 +281,15 @@
             });
             const result = await response.json();
             if (result.success) {
-                document.getElementById('serial_status').textContent = `Status: Restarted connection to ${port}`;
+                document.getElementById('serial_status').textContent = `Restarted connection to ${port}`;
                 logMessage('Serial connection restarted.');
+
+                // No need to change visibility for restart
             } else {
                 logMessage(`Error restarting serial connection: ${result.error}`);
             }
         }
+
         async function sendHomeCommand() {
             const response = await fetch('/send_home', { method: 'POST' });
             const result = await response.json();
@@ -324,9 +323,24 @@
         async function loadThetaRhoFiles() {
             logMessage('Loading Theta-Rho files...');
             const response = await fetch('/list_theta_rho_files');
-            const files = await response.json();
-            allFiles = files; // Store the full list of files
-            displayFiles(allFiles); // Initially display all files
+            let files = await response.json();
+
+            // Filter only .thr files
+            files = files.filter(file => file.endsWith('.thr'));
+
+            // Separate files into categories
+            const customPatternsFiles = files.filter(file => file.startsWith('custom_patterns/'));
+            const otherFiles = files.filter(file => !file.startsWith('custom_patterns/'));
+
+            // Sort the files
+            const sortedFiles = [
+                ...customPatternsFiles.sort(), // Custom patterns first
+                ...otherFiles.sort() // Remaining files sorted alphabetically
+            ];
+
+            allFiles = sortedFiles; // Store the sorted list of files
+            displayFiles(allFiles); // Display the sorted files
+
             logMessage('Theta-Rho files loaded successfully.');
         }
 
@@ -361,7 +375,7 @@
                 logMessage(`Failed to move to center: ${result.error}`);
             }
         }
-        
+
         async function moveToPerimeter() {
             logMessage('Moving to perimeter...');
             const response = await fetch('/move_to_perimeter', { method: 'POST' });
@@ -376,19 +390,19 @@
         async function sendCoordinate() {
             const theta = parseFloat(document.getElementById('theta_input').value);
             const rho = parseFloat(document.getElementById('rho_input').value);
-        
+
             if (isNaN(theta) || isNaN(rho)) {
                 logMessage('Invalid input: θ and ρ must be numbers.');
                 return;
             }
-        
+
             logMessage(`Sending coordinate: θ=${theta}, ρ=${rho}...`);
             const response = await fetch('/send_coordinate', {
                 method: 'POST',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({ theta, rho })
             });
-        
+
             const result = await response.json();
             if (result.success) {
                 logMessage(`Coordinate executed successfully: θ=${theta}, ρ=${rho}`);
@@ -404,11 +418,11 @@
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({ file_name: fileName })
             });
-        
+
             const result = await response.json();
             if (result.success) {
                 const coordinates = result.coordinates;
-        
+
                 // Update coordinates display
                 if (coordinates.length > 0) {
                     const firstCoord = coordinates[0];
@@ -419,7 +433,7 @@
                     document.getElementById('first_coordinate').textContent = 'First Coordinate: Not available';
                     document.getElementById('last_coordinate').textContent = 'Last Coordinate: Not available';
                 }
-        
+
                 renderPattern(coordinates);
             } else {
                 logMessage(`Failed to fetch preview for file: ${result.error}`);
@@ -428,24 +442,24 @@
                 document.getElementById('last_coordinate').textContent = 'Last Coordinate: Not available';
             }
         }
-        
+
         function renderPattern(coordinates) {
             const canvas = document.getElementById('patternPreviewCanvas');
             const ctx = canvas.getContext('2d');
-        
+
             // Make canvas full screen
             canvas.width = window.innerWidth;
             canvas.height = window.innerHeight;
-        
+
             // Clear the canvas
             ctx.clearRect(0, 0, canvas.width, canvas.height);
-        
+
             // Convert polar to Cartesian and draw the pattern
             const centerX = canvas.width / 2;
             const centerY = canvas.height / 2;
             const maxRho = Math.max(...coordinates.map(coord => coord[1]));
             const scale = Math.min(canvas.width, canvas.height) / (2 * maxRho); // Scale to fit within the screen
-        
+
             ctx.beginPath();
             coordinates.forEach(([theta, rho], index) => {
                 const x = centerX + rho * Math.cos(theta) * scale;
@@ -461,37 +475,75 @@
             logMessage('Pattern preview rendered at full screen.');
         }
 
+        function toggleDebugLog() {
+            const statusLog = document.getElementById('status_log');
+            const debugButton = document.getElementById('debug_button');
+
+            if (statusLog.style.display === 'block') {
+                statusLog.style.display = 'none';
+                debugButton.textContent = 'Show Debug Log'; // Update the button label
+            } else {
+                statusLog.style.display = 'block';
+                debugButton.textContent = 'Hide Debug Log'; // Update the button label
+                statusLog.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Smooth scrolling to the log
+            }
+        }
+
         async function checkSerialStatus() {
             const response = await fetch('/serial_status');
             const status = await response.json();
             const statusElement = document.getElementById('serial_status');
-            
+            const serialPortsContainer = document.getElementById('serial_ports_container');
+
+            const connectButton = document.querySelector('button[onclick="connectSerial()"]');
+            const disconnectButton = document.querySelector('button[onclick="disconnectSerial()"]');
+            const restartButton = document.querySelector('button[onclick="restartSerial()"]');
+
             if (status.connected) {
                 const port = status.port || 'Unknown'; // Fallback if port is undefined
-                statusElement.textContent = `Status: Connected to ${port}`;
+                statusElement.textContent = `Connected to ${port}`;
+                statusElement.classList.add('connected');
+                statusElement.classList.remove('not-connected');
                 logMessage(`Reconnected to serial port: ${port}`);
+
+                // Hide Available Ports and show disconnect/restart buttons
+                serialPortsContainer.style.display = 'none';
+                connectButton.style.display = 'none';
+                disconnectButton.style.display = 'inline-block';
+                restartButton.style.display = 'inline-block';
             } else {
-                statusElement.textContent = 'Status: Not connected';
+                statusElement.textContent = 'Not connected';
+                statusElement.classList.add('not-connected');
+                statusElement.classList.remove('connected');
                 logMessage('No active serial connection.');
+
+                // Show Available Ports and the connect button
+                serialPortsContainer.style.display = 'block';
+                connectButton.style.display = 'inline-block';
+                disconnectButton.style.display = 'none';
+                restartButton.style.display = 'none';
+
+                // Attempt to auto-load available ports
+                await loadSerialPorts();
             }
         }
 
         async function changeSpeed() {
             const speedInput = document.getElementById('speed_input');
             const speed = parseFloat(speedInput.value);
-        
+
             if (isNaN(speed) || speed <= 0) {
                 logMessage('Invalid speed. Please enter a positive number.');
                 return;
             }
-        
+
             logMessage(`Setting speed to: ${speed}...`);
             const response = await fetch('/set_speed', {
                 method: 'POST',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({ speed })
             });
-        
+
             const result = await response.json();
             if (result.success) {
                 document.getElementById('speed_status').textContent = `Current Speed: ${speed}`;
@@ -500,14 +552,14 @@
                 logMessage(`Failed to set speed: ${result.error}`);
             }
         }
-        
+
         // Call this function on page load
         checkSerialStatus();
 
         // Initial load of serial ports and Theta-Rho files
         loadSerialPorts();
         loadThetaRhoFiles();
-        
+
         document.getElementById('run_button').onclick = runThetaRho;
 
     </script>