Browse Source

Merge branch 'dev' into feature/firmware-updater

# Conflicts:
#	app.py
#	templates/index.html
Thokoop 1 year ago
parent
commit
cf7c16b7f4
6 changed files with 287 additions and 19 deletions
  1. 194 18
      app.py
  2. 3 0
      arduino_code/arduino_code.ino
  3. 3 0
      arduino_code_TMC2209/arduino_code_TMC2209.ino
  4. 3 0
      esp32/esp32.ino
  5. 70 1
      static/main.js
  6. 14 0
      templates/index.html

+ 194 - 18
app.py

@@ -7,6 +7,7 @@ import threading
 import serial.tools.list_ports
 import math
 import json
+from datetime import datetime
 import subprocess
 
 app = Flask(__name__)
@@ -25,10 +26,25 @@ os.makedirs(THETA_RHO_DIR, exist_ok=True)
 ser = None
 ser_port = None  # Global variable to store the serial port name
 stop_requested = False
+pause_requested = False
+pause_condition = threading.Condition()
+
+# Global variables to store device information
+arduino_table_name = None
+arduino_driver_type = None
+
+# Table status
+current_playing_file = None
+execution_progress = None
+firmware_version = None
+current_playing_index = None
+current_playlist = None
+is_clearing = False
+
 serial_lock = threading.Lock()
 
 PLAYLISTS_FILE = os.path.join(os.getcwd(), "playlists.json")
-OPTIONS_FILE = os.path.join(os.getcwd(), "options.json")
+
 MOTOR_TYPE_MAPPING = {
     "TMC2209": "./arduino_code_TMC2209/arduino_code_TMC2209.ino",
     "DRV8825": "./arduino_code/arduino_code.ino",
@@ -98,7 +114,7 @@ def list_serial_ports():
 
 def connect_to_serial(port=None, baudrate=115200):
     """Automatically connect to the first available serial port or a specified port."""
-    global ser, ser_port
+    global ser, ser_port, arduino_table_name, arduino_driver_type
 
     try:
         if port is None:
@@ -111,10 +127,32 @@ def connect_to_serial(port=None, baudrate=115200):
         with serial_lock:
             if ser and ser.is_open:
                 ser.close()
-            ser = serial.Serial(port, baudrate)
+            ser = serial.Serial(port, baudrate, timeout=2)  # Set timeout to avoid infinite waits
             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
+
+        # Read initial startup messages from Arduino
+        arduino_table_name = None
+        arduino_driver_type = None
+
+        while ser.in_waiting > 0:
+            line = ser.readline().decode().strip()
+            print(f"Arduino: {line}")  # Print the received message
+
+            # Store the device details based on the expected messages
+            if "Table:" in line:
+                arduino_table_name = line.replace("Table: ", "").strip()
+            elif "Drivers:" in line:
+                arduino_driver_type = line.replace("Drivers: ", "").strip()
+            elif "Version:" in line:
+                firmware_version = line.replace("Version: ", "").strip()
+
+        # Display stored values
+        print(f"Detected Table: {arduino_table_name or 'Unknown'}")
+        print(f"Detected Drivers: {arduino_driver_type or 'Unknown'}")
+
         return True  # Successfully connected
     except serial.SerialException as e:
         print(f"Failed to connect to serial port {port}: {e}")
@@ -197,42 +235,109 @@ def send_command(command):
                     print("Command execution completed.")
                     break
 
-def run_theta_rho_file(file_path):
+def wait_for_start_time(schedule_hours):
+    """
+    Keep checking every 30 seconds if the time is within the schedule to resume execution.
+    """
+    global pause_requested
+    start_time, end_time = schedule_hours
+
+    while pause_requested:
+        now = datetime.now().time()
+        if start_time <= now < end_time:
+            print("Resuming execution: Within schedule.")
+            pause_requested = False
+            with pause_condition:
+                pause_condition.notify_all()
+            break  # Exit the loop once resumed
+        else:
+            time.sleep(30)  # Wait for 30 seconds before checking again
+
+# Function to check schedule based on start and end time
+def schedule_checker(schedule_hours):
+    """
+    Pauses/resumes execution based on a given time range.
+
+    Parameters:
+    - schedule_hours (tuple): (start_time, end_time) as `datetime.time` objects.
+    """
+    global pause_requested
+    if not schedule_hours:
+        return  # No scheduling restriction
+
+    start_time, end_time = schedule_hours
+    now = datetime.now().time()  # Get the current time as `datetime.time`
+
+    # Check if we are currently within the scheduled time
+    if start_time <= now < end_time:
+        if pause_requested:
+            print("Starting execution: Within schedule.")
+        pause_requested = False  # Resume execution
+        with pause_condition:
+            pause_condition.notify_all()
+    else:
+        if not pause_requested:
+            print("Pausing execution: Outside schedule.")
+        pause_requested = True  # Pause execution
+
+        # Start a background thread to periodically check for start time
+        threading.Thread(target=wait_for_start_time, args=(schedule_hours,), daemon=True).start()
+
+def run_theta_rho_file(file_path, schedule_hours=None):
     """Run a theta-rho file by sending data in optimized batches."""
-    global stop_requested
+    global stop_requested, current_playing_file, execution_progress
     stop_requested = False
+    current_playing_file = file_path  # Track current playing file
+    execution_progress = (0, 0)  # Reset progress
 
     coordinates = parse_theta_rho_file(file_path)
-    if len(coordinates) < 2:
+    total_coordinates = len(coordinates)
+
+    if total_coordinates < 2:
         print("Not enough coordinates for interpolation.")
+        current_playing_file = None  # Clear tracking if failed
+        execution_progress = None
         return
 
-    # Optimize batch size for smoother execution
+    execution_progress = (0, total_coordinates)  # Update total coordinates
     batch_size = 10  # Smaller batches may smooth movement further
-    for i in range(0, len(coordinates), batch_size):
-        # Check stop_requested flag after sending the batch
+
+    for i in range(0, total_coordinates, batch_size):
         if stop_requested:
             print("Execution stopped by user after completing the current batch.")
             break
+
+        with pause_condition:
+            while pause_requested:
+                print("Execution paused...")
+                pause_condition.wait()  # This will block execution until notified
+
         batch = coordinates[i:i + batch_size]
         if i == 0:
             send_coordinate_batch(ser, batch)
+            execution_progress = (i + batch_size, total_coordinates)  # Update progress
             continue
-        # Wait until Arduino is READY before sending the batch
+
         while True:
+            schedule_checker(schedule_hours)  # Check if within schedule
             with serial_lock:
                 if ser.in_waiting > 0:
                     response = ser.readline().decode().strip()
                     if response == "R":
                         send_coordinate_batch(ser, batch)
+                        execution_progress = (i + batch_size, total_coordinates)  # Update progress
                         break
                     else:
                         print(f"Arduino response: {response}")
 
-    # Reset theta after execution or stopping
     reset_theta()
     ser.write("FINISHED\n".encode())
 
+    # Clear tracking variables when done
+    current_playing_file = None
+    execution_progress = None
+    print("Pattern execution completed.")
+
 def get_clear_pattern_file(pattern_name):
     """Return a .thr file path based on pattern_name."""
     if pattern_name == "random":
@@ -246,7 +351,8 @@ def run_theta_rho_files(
     pause_time=0,
     clear_pattern=None,
     run_mode="single",
-    shuffle=False
+    shuffle=False,
+    schedule_hours=None
 ):
     """
     Runs multiple .thr files in sequence with options for pausing, clearing, shuffling, and looping.
@@ -259,14 +365,20 @@ def run_theta_rho_files(
     - shuffle (bool): Whether to shuffle the playlist before running.
     """
     global stop_requested
+    global current_playlist
+    global current_playing_index
     stop_requested = False  # Reset stop flag at the start
 
     if shuffle:
         random.shuffle(file_paths)
         print("Playlist shuffled.")
 
+    current_playlist = file_paths
+
     while True:
         for idx, path in enumerate(file_paths):
+            current_playing_index = idx
+            schedule_checker(schedule_hours)
             if stop_requested:
                 print("Execution stopped before starting next pattern.")
                 return
@@ -279,12 +391,12 @@ def run_theta_rho_files(
                 # Determine the clear pattern to run
                 clear_file_path = get_clear_pattern_file(clear_pattern)
                 print(f"Running clear pattern: {clear_file_path}")
-                run_theta_rho_file(clear_file_path)
+                run_theta_rho_file(clear_file_path, schedule_hours)
 
             if not stop_requested:
                 # Run the main pattern
                 print(f"Running pattern {idx + 1} of {len(file_paths)}: {path}")
-                run_theta_rho_file(path)
+                run_theta_rho_file(path, schedule_hours)
 
             if idx < len(file_paths) -1:
                 if stop_requested:
@@ -436,6 +548,12 @@ def run_theta_rho():
 def stop_execution():
     global stop_requested
     stop_requested = True
+    current_playing_index = None
+    current_playlist = None
+    is_clearing = False
+    current_playing_file = None
+    execution_progress = None
+
     return jsonify({'success': True})
 
 @app.route('/send_home', methods=['POST'])
@@ -558,9 +676,45 @@ def serial_status():
         'port': ser_port  # Include the port name
     })
 
-if not os.path.exists(PLAYLISTS_FILE):
-    with open(PLAYLISTS_FILE, "w") as f:
-        json.dump({}, f, indent=2)
+@app.route('/pause_execution', methods=['POST'])
+def pause_execution():
+    """Pause the current execution."""
+    global pause_requested
+    with pause_condition:
+        pause_requested = True
+    return jsonify({'success': True, 'message': 'Execution paused'})
+
+@app.route('/status', methods=['GET'])
+def get_status():
+    """Returns the current status of the sand table."""
+    global is_clearing
+    if current_playing_file in CLEAR_PATTERNS.values():
+        is_clearing = True
+    else:
+        is_clearing = False
+
+    return jsonify({
+        "ser_port": ser_port,
+        "stop_requested": stop_requested,
+        "pause_requested": pause_requested,
+        "current_playing_file": current_playing_file,
+        "execution_progress": execution_progress,
+        "arduino_table_name": arduino_table_name,
+        "arduino_driver_type": arduino_driver_type,
+        "firmware_version": firmware_version,
+        "current_playing_index": current_playing_index,
+        "current_playlist": current_playlist,
+        "is_clearing": is_clearing
+    })
+
+@app.route('/resume_execution', methods=['POST'])
+def resume_execution():
+    """Resume execution after pausing."""
+    global pause_requested
+    with pause_condition:
+        pause_requested = False
+        pause_condition.notify_all()  # Unblock the waiting thread
+    return jsonify({'success': True, 'message': 'Execution resumed'})
 
 def load_playlists():
     """
@@ -724,6 +878,8 @@ def run_playlist():
         "clear_pattern": "random",         # Optional: "clear_in", "clear_out", "clear_sideway", or "random"
         "run_mode": "single",              # 'single' or 'indefinite'
         "shuffle": True                    # true or false
+        "start_time": ""
+        "end_time": ""
     }
     """
     data = request.get_json()
@@ -737,6 +893,8 @@ def run_playlist():
     clear_pattern = data.get("clear_pattern", None)
     run_mode = data.get("run_mode", "single")  # Default to 'single' run
     shuffle = data.get("shuffle", False)       # Default to no shuffle
+    start_time = data.get("start_time", None)
+    end_time = data.get("end_time", None)
 
     # Validate pause_time
     if not isinstance(pause_time, (int, float)) or pause_time < 0:
@@ -755,6 +913,23 @@ def run_playlist():
     if not isinstance(shuffle, bool):
         return jsonify({"success": False, "error": "'shuffle' must be a boolean value"}), 400
 
+    schedule_hours = None
+    if start_time and end_time:
+        try:
+            # Convert HH:MM to datetime.time objects
+            start_time_obj = datetime.strptime(start_time, "%H:%M").time()
+            end_time_obj = datetime.strptime(end_time, "%H:%M").time()
+
+            # Ensure start_time is before end_time
+            if start_time_obj >= end_time_obj:
+                return jsonify({"success": False, "error": "'start_time' must be earlier than 'end_time'"}), 400
+
+            # Create schedule tuple with full time
+            schedule_hours = (start_time_obj, end_time_obj)
+        except ValueError:
+            return jsonify({"success": False, "error": "Invalid time format. Use HH:MM (e.g., '09:30')"}), 400
+
+
     # Load playlists
     playlists = load_playlists()
 
@@ -776,7 +951,8 @@ def run_playlist():
                 'pause_time': pause_time,
                 'clear_pattern': clear_pattern,
                 'run_mode': run_mode,
-                'shuffle': shuffle
+                'shuffle': shuffle,
+                'schedule_hours': schedule_hours
             },
             daemon=True  # Daemonize thread to exit with the main program
         ).start()

+ 3 - 0
arduino_code/arduino_code.ino

@@ -72,6 +72,9 @@ void setup()
 
     // Initialize serial communication
     Serial.begin(115200);
+    Serial.println("Table: Dune Weaver");
+    Serial.println("Drivers: DRV8825");
+    Serial.println("Version: 1.4.0";)
     Serial.println("R");
     homing();
 }

+ 3 - 0
arduino_code_TMC2209/arduino_code_TMC2209.ino

@@ -72,6 +72,9 @@ void setup()
 
     // Initialize serial communication
     Serial.begin(115200);
+    Serial.println("Table: Dune Weaver");
+    Serial.println("Drivers: TMC2209");
+    Serial.println("Version: 1.4.0";)
     Serial.println("R");
     homing();
 }

+ 3 - 0
esp32/esp32.ino

@@ -67,6 +67,9 @@ void setup()
 
     // Initialize serial communication
     Serial.begin(115200);
+    Serial.println("Table: Mini Dune Weaver");
+    Serial.println("Drivers: ULN2003");
+    Serial.println("Version: 1.4.0";)
     Serial.println("R");
     homing();
 }

+ 70 - 1
static/main.js

@@ -241,6 +241,36 @@ async function stopExecution() {
     }
 }
 
+let isPaused = false;
+
+function togglePausePlay() {
+    const button = document.getElementById("pausePlayButton");
+
+    if (isPaused) {
+        // Resume execution
+        fetch('/resume_execution', { method: 'POST' })
+            .then(response => response.json())
+            .then(data => {
+                if (data.success) {
+                    isPaused = false;
+                    button.innerHTML = "⏸"; // Change to pause icon
+                }
+            })
+            .catch(error => console.error("Error resuming execution:", error));
+    } else {
+        // Pause execution
+        fetch('/pause_execution', { method: 'POST' })
+            .then(response => response.json())
+            .then(data => {
+                if (data.success) {
+                    isPaused = true;
+                    button.innerHTML = "▶"; // Change to play icon
+                }
+            })
+            .catch(error => console.error("Error pausing execution:", error));
+    }
+}
+
 function removeCurrentPattern() {
     if (!selectedFile) {
         logMessage('No file selected to remove.', LOG_TYPE.ERROR);
@@ -815,6 +845,10 @@ function openPlaylistEditor(playlistName) {
     loadPlaylist(playlistName);
 }
 
+function clearSchedule() {
+    document.getElementById("start_time").value = "";
+    document.getElementById("end_time").value = "";
+}
 
 // Function to run the selected playlist with specified parameters
 async function runPlaylist() {
@@ -829,6 +863,8 @@ async function runPlaylist() {
     const clearPatternSelect = document.getElementById('clear_pattern').value;
     const runMode = document.querySelector('input[name="run_mode"]:checked').value;
     const shuffle = document.getElementById('shuffle_playlist').checked;
+    const startTimeInput = document.getElementById('start_time').value.trim();
+    const endTimeInput = document.getElementById('end_time').value.trim();
 
     const pauseTime = parseFloat(pauseTimeInput);
     if (isNaN(pauseTime) || pauseTime < 0) {
@@ -836,6 +872,37 @@ async function runPlaylist() {
         return;
     }
 
+    // Validate start and end time format and logic
+    let startTime = startTimeInput || null;
+    let endTime = endTimeInput || null;
+
+    // Ensure that if one time is filled, the other must be as well
+    if ((startTime && !endTime) || (!startTime && endTime)) {
+        logMessage("Both start and end times must be provided together or left blank.", LOG_TYPE.WARNING);
+        return;
+    }
+
+    // If both are provided, validate format and ensure start_time < end_time
+    if (startTime && endTime) {
+        try {
+            const startDateTime = new Date(`1970-01-01T${startTime}:00`);
+            const endDateTime = new Date(`1970-01-01T${endTime}:00`);
+
+            if (isNaN(startDateTime.getTime()) || isNaN(endDateTime.getTime())) {
+                logMessage("Invalid time format. Please use HH:MM format (e.g., 09:30).", LOG_TYPE.WARNING);
+                return;
+            }
+
+            if (startDateTime >= endDateTime) {
+                logMessage("Start time must be earlier than end time.", LOG_TYPE.WARNING);
+                return;
+            }
+        } catch (error) {
+            logMessage("Error parsing start or end time. Ensure correct HH:MM format.", LOG_TYPE.ERROR);
+            return;
+        }
+    }
+
     logMessage(`Running playlist: ${playlistName} with pause_time=${pauseTime}, clear_pattern=${clearPatternSelect}, run_mode=${runMode}, shuffle=${shuffle}.`);
 
     try {
@@ -847,7 +914,9 @@ async function runPlaylist() {
                 pause_time: pauseTime,
                 clear_pattern: clearPatternSelect,
                 run_mode: runMode,
-                shuffle: shuffle
+                shuffle: shuffle,
+                start_time: startTimeInput,
+                end_time: endTimeInput
             })
         });
 

+ 14 - 0
templates/index.html

@@ -121,6 +121,19 @@
                         </select>
                     </div>
                 </div>
+                <h3>Schedule:</h3>
+                <div class="control-group">
+                    <div class="item column">
+                        <label for="start_time">Start time</label>
+                        <input type="time" id="start_time" min="00:00" max="24:00">
+                    </div>
+                    <div class="item column">
+                        <label for="end_time">End time</label>
+                        <input type="time" id="end_time" min="00:00" max="24:00">
+                    </div>
+                </div>
+                <button onclick="clearSchedule()" class="cancel">Clear</button>
+
             </div>
         </section>
 
@@ -161,6 +174,7 @@
             </div>
 
             <div class="action-buttons">
+                <button id="pausePlayButton" onclick="togglePausePlay()" class="cancel">⏸</button> <!-- Default: Pause Icon -->
                 <button onclick="stopExecution()" class="cancel">Stop Current Pattern</button>
                 <div class="dropdown">
                     <button id="clear_button" class="dropdown-button" onclick="executeClearAction()">