Browse Source

Add schedule, play/pause, table config, and table status (#27)

## New Features
- **Play/Pause Button:** Control execution flow without stopping.
- **Scheduled Running Hours:** Automate table operation.
- **Adaptive clear:** Select the appropriate clearing pattenr based on the starting coordinate
- **Table Info on Serial Connection:** Real-time metadata and status.
- **Table Status API:** Poll current status remotely.
- **Firmware Versioning:** Track and fetch firmware details. Update firmware from the UI (beta)

## Fixes & Improvements
- **Quick Fixes:** Stability and performance improvements.
- **Firmware Updater (#34):** Version tracking, remote flashing, and bug fixes.
- **UI/UX Enhancements:** Improved "Currently Playing," settings overlay, and styling.
- **Software Updates:** Enhanced updater, dependency installation via `DockerFile`.
- **Performance:** Better serial locking, retry logic, and pattern execution handling.
- **ESP32_TMC2209 Support:** Added firmware flashing.

## Contributors
- **Thokoop** ([GitHub](https://github.com/Thokoop))
- **Fabio De Simone** (https://github.com/ProtoxiDe22)
Tuan Nguyen 1 năm trước cách đây
mục cha
commit
d4ef5d6df1

+ 22 - 0
CHANGELOG.md

@@ -2,6 +2,28 @@
 
 All notable changes to this project will be documented in this file.
 
+## [1.4.0] Soft- and Firmware updater
+
+### New Features
+- **Play/Pause Button:** Control execution flow without stopping.
+- **Scheduled Running Hours:** Automate table operation.
+- **Adaptive clear:** Select the appropriate clearing pattenr based on the starting coordinate
+- **Table Info on Serial Connection:** Real-time metadata and status.
+- **Table Status API:** Poll current status remotely.
+- **Firmware Versioning:** Track and fetch firmware details.
+
+### Fixes & Improvements
+- **Quick Fixes:** Stability and performance improvements.
+- **Firmware Updater (#34):** Version tracking, remote flashing, and bug fixes.
+- **UI/UX Enhancements:** Improved "Currently Playing," settings overlay, and styling.
+- **Software Updates:** Enhanced updater, dependency installation via `DockerFile`.
+- **Performance:** Better serial locking, retry logic, and pattern execution handling.
+- **ESP32_TMC2209 Support:** Added firmware flashing.
+
+### Contributors
+- **Thokoop** ([GitHub](https://github.com/Thokoop))
+- **Fabio De Simone** (https://github.com/ProtoxiDe22)
+
 ## [1.3.0] Revamped UI
 
 Massive thanks to Thokoop for helping us redesigning the UI just within a few days! The new design looks gorgeous on both PC and mobile. 

+ 9 - 0
Dockerfile

@@ -7,6 +7,15 @@ WORKDIR /app
 # Copy the current directory contents into the container at /app
 COPY . /app
 
+# Install required system packages
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    gcc \
+    avrdude \
+    wget \
+    unzip \
+    git \
+    && rm -rf /var/lib/apt/lists/*
+
 # Install any needed packages specified in requirements.txt
 RUN pip install --no-cache-dir -r requirements.txt
 

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Orion W Crook
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 616 - 83
app.py

@@ -1,4 +1,5 @@
 from flask import Flask, request, jsonify, render_template
+import atexit
 import os
 import serial
 import time
@@ -7,6 +8,9 @@ import threading
 import serial.tools.list_ports
 import math
 import json
+from datetime import datetime
+import subprocess
+from tqdm import tqdm
 
 app = Flask(__name__)
 
@@ -24,15 +28,135 @@ os.makedirs(THETA_RHO_DIR, exist_ok=True)
 ser = None
 ser_port = None  # Global variable to store the serial port name
 stop_requested = False
-serial_lock = threading.Lock()
+pause_requested = False
+pause_condition = threading.Condition()
+
+# Global variables to store device information
+arduino_table_name = None
+arduino_driver_type = 'Unknown'
+
+# Table status
+current_playing_file = None
+execution_progress = None
+firmware_version = 'Unknown'
+current_playing_index = None
+current_playlist = None
+is_clearing = False
+
+serial_lock = threading.RLock()
 
 PLAYLISTS_FILE = os.path.join(os.getcwd(), "playlists.json")
 
+MOTOR_TYPE_MAPPING = {
+    "TMC2209": "./firmware/arduino_code_TMC2209/arduino_code_TMC2209.ino",
+    "DRV8825": "./firmware/arduino_code/arduino_code.ino",
+    "esp32": "./firmware/esp32/esp32.ino",
+    "esp32_TMC2209": "./firmware/esp32_TMC2209/esp32_TMC2209.ino"
+}
+
 # Ensure the file exists and contains at least an empty JSON object
 if not os.path.exists(PLAYLISTS_FILE):
     with open(PLAYLISTS_FILE, "w") as f:
         json.dump({}, f, indent=2)
 
+def get_ino_firmware_details(ino_file_path):
+    """
+    Extract firmware details, including version and motor type, from the given .ino file.
+
+    Args:
+        ino_file_path (str): Path to the .ino file.
+
+    Returns:
+        dict: Dictionary containing firmware details such as version and motor type, or None if not found.
+    """
+    try:
+        if not ino_file_path:
+            raise ValueError("Invalid path: ino_file_path is None or empty.")
+
+        firmware_details = {"version": None, "motorType": None}
+
+        with open(ino_file_path, "r") as file:
+            for line in file:
+                # Extract firmware version
+                if "firmwareVersion" in line:
+                    start = line.find('"') + 1
+                    end = line.rfind('"')
+                    if start != -1 and end != -1 and start < end:
+                        firmware_details["version"] = line[start:end]
+
+                # Extract motor type
+                if "motorType" in line:
+                    start = line.find('"') + 1
+                    end = line.rfind('"')
+                    if start != -1 and end != -1 and start < end:
+                        firmware_details["motorType"] = line[start:end]
+
+        if not firmware_details["version"]:
+            print(f"Firmware version not found in file: {ino_file_path}")
+        if not firmware_details["motorType"]:
+            print(f"Motor type not found in file: {ino_file_path}")
+
+        return firmware_details if any(firmware_details.values()) else None
+
+    except FileNotFoundError:
+        print(f"File not found: {ino_file_path}")
+        return None
+    except Exception as e:
+        print(f"Error reading .ino file: {str(e)}")
+        return None
+
+def check_git_updates():
+    try:
+        # Fetch the latest updates from the remote repository
+        subprocess.run(["git", "fetch", "--tags", "--force"], check=True)
+
+        # Get the latest tag from the remote
+        latest_remote_tag = subprocess.check_output(
+            ["git", "describe", "--tags", "--abbrev=0", "origin/main"]
+        ).strip().decode()
+
+        # Get the latest tag from the local branch
+        latest_local_tag = subprocess.check_output(
+            ["git", "describe", "--tags", "--abbrev=0"]
+        ).strip().decode()
+
+        # Count how many tags the local branch is behind
+        tag_behind_count = 0
+        if latest_local_tag != latest_remote_tag:
+            tags = subprocess.check_output(
+                ["git", "tag", "--merged", "origin/main"], text=True
+            ).splitlines()
+
+            found_local = False
+            for tag in tags:
+                if tag == latest_local_tag:
+                    found_local = True
+                elif found_local:
+                    tag_behind_count += 1
+                    if tag == latest_remote_tag:
+                        break
+
+
+        # Check if there are new commits
+        updates_available = latest_remote_tag != latest_local_tag
+
+        return {
+            "updates_available": updates_available,
+            "tag_behind_count": tag_behind_count,  # Tags behind
+            "latest_remote_tag": latest_remote_tag,
+            "latest_local_tag": latest_local_tag,
+        }
+    except subprocess.CalledProcessError as e:
+        print(f"Error checking Git updates: {e}")
+        return {
+            "updates_available": False,
+            "tag_behind_count": 0,
+            "latest_remote_tag": None,
+            "latest_local_tag": None,
+        }
+
+
+
 def list_serial_ports():
     """Return a list of available serial ports."""
     ports = serial.tools.list_ports.comports()
@@ -40,7 +164,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, firmware_version
 
     try:
         if port is None:
@@ -53,10 +177,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}")
@@ -122,73 +268,176 @@ 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:.5f},{rho:.5f}" for theta, rho in coordinates) + ";\n"
-    ser.write(batch_str.encode())
+    with serial_lock:
+        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}")
+    with serial_lock:
+        ser.write(f"{command}\n".encode())
+        print(f"Sent: {command}")
 
-    # Wait for "R" acknowledgment from Arduino
-    while True:
-        with serial_lock:
-            if ser.in_waiting > 0:
-                response = ser.readline().decode().strip()
-                print(f"Arduino response: {response}")
-                if response == "R":
-                    print("Command execution completed.")
-                    break
+        # Wait for "R" acknowledgment from Arduino
+        while True:
+            with serial_lock:
+                if ser.in_waiting > 0:
+                    response = ser.readline().decode().strip()
+                    print(f"Arduino response: {response}")
+                    if response == "R":
+                        print("Command execution completed.")
+                        break
 
-def run_theta_rho_file(file_path):
-    """Run a theta-rho file by sending data in optimized batches."""
-    global stop_requested
-    stop_requested = False
+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 with tqdm ETA tracking."""
+    global stop_requested, current_playing_file, execution_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, None)  # Initialize progress with ETA as None
     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
-        if stop_requested:
-            print("Execution stopped by user after completing the current batch.")
-            break
-        batch = coordinates[i:i + batch_size]
-        if i == 0:
-            send_coordinate_batch(ser, batch)
-            continue
-        # Wait until Arduino is READY before sending the batch
-        while True:
-            with serial_lock:
-                if ser.in_waiting > 0:
-                    response = ser.readline().decode().strip()
-                    if response == "R":
-                        send_coordinate_batch(ser, batch)
-                        break
-                    else:
-                        print(f"Arduino response: {response}")
+    
+    # before trying to acuire the lock we send the stop command
+    # so then we will just wait for the lock to be released so we can use the serial
+    stop_actions()
+    with serial_lock:
+        current_playing_file = file_path  # Track current playing file
+        execution_progress = (0, 0, None)  # Reset progress (ETA starts as None)
+        stop_requested = False
+        with tqdm(total=total_coordinates, unit="coords", desc=f"Executing Pattern {file_path}", dynamic_ncols=True, disable=None) as pbar:
+            for i in range(0, total_coordinates, batch_size):
+                if stop_requested:
+                    print("Execution stopped by user after completing the current batch.")
+                    break
 
-    # Reset theta after execution or stopping
-    reset_theta()
-    ser.write("FINISHED\n".encode())
+                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, None)  # No ETA yet
+                    pbar.update(batch_size)
+                    continue
 
-def get_clear_pattern_file(pattern_name):
+                while True:
+                    schedule_checker(schedule_hours)  # Check if within schedule
+                    if ser.in_waiting > 0:
+                        response = ser.readline().decode().strip()
+                        if response == "R":
+                            send_coordinate_batch(ser, batch)
+                            pbar.update(batch_size)  # Update tqdm progress
+
+                            # Use tqdm's built-in ETA tracking
+                            estimated_remaining_time = pbar.format_dict['elapsed'] / (i + batch_size) * (total_coordinates - (i + batch_size))
+
+                            # Update execution progress with formatted ETA
+                            execution_progress = (i + batch_size, total_coordinates, estimated_remaining_time)
+                            break
+                        elif response != "IGNORED: FINISHED" and response.startswith("IGNORE"):  # Retry the previous batch
+                            print("Received IGNORE. Resending the previous batch...")
+                            print(response)
+                            # Calculate the previous batch indices
+                            prev_start = max(0, i - batch_size)  # Ensure we don't go below 0
+                            prev_end = i  # End of the previous batch is `i`
+                            previous_batch = coordinates[prev_start:prev_end]
+
+                            # Resend the previous batch
+                            send_coordinate_batch(ser, previous_batch)
+                            break  # Exit the retry loop after resending
+                        else:
+                            print(f"Arduino response: {response}")
+
+        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(clear_pattern_mode, path=None):
     """Return a .thr file path based on pattern_name."""
-    if pattern_name == "random":
+    if not clear_pattern_mode or clear_pattern_mode == 'none':
+        return
+    print("Clear pattern mode: " + clear_pattern_mode)
+    if clear_pattern_mode == "random":
         # Randomly pick one of the three known patterns
         return random.choice(list(CLEAR_PATTERNS.values()))
-    # If pattern_name is invalid or absent, default to 'clear_from_in'
-    return CLEAR_PATTERNS.get(pattern_name, CLEAR_PATTERNS["clear_from_in"])
+
+    if clear_pattern_mode == 'adaptive':
+        _, first_rho = parse_theta_rho_file(path)[0]
+        if first_rho < 0.5:
+            return CLEAR_PATTERNS['clear_from_out']
+        else:
+            return random.choice([CLEAR_PATTERNS['clear_from_in'], CLEAR_PATTERNS['clear_sideway']])
+    else:
+        return CLEAR_PATTERNS[clear_pattern_mode]
 
 def run_theta_rho_files(
     file_paths,
     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.
@@ -196,19 +445,26 @@ def run_theta_rho_files(
     Parameters:
     - file_paths (list): List of file paths to run.
     - pause_time (float): Seconds to pause between patterns.
-    - clear_pattern (str): Specific clear pattern to run ("clear_in", "clear_out", "clear_sideway", or "random").
+    - clear_pattern (str): Specific clear pattern to run ("clear_from_in", "clear_from_out", "clear_sideway", "adaptive", or "random").
     - run_mode (str): "single" for one-time run or "indefinite" for looping.
     - 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):
+            print("Upcoming pattern: " + path)
+            current_playing_index = idx
+            schedule_checker(schedule_hours)
             if stop_requested:
                 print("Execution stopped before starting next pattern.")
                 return
@@ -219,14 +475,14 @@ def run_theta_rho_files(
                     return
 
                 # Determine the clear pattern to run
-                clear_file_path = get_clear_pattern_file(clear_pattern)
+                clear_file_path = get_clear_pattern_file(clear_pattern, path)
                 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:
@@ -253,21 +509,24 @@ def run_theta_rho_files(
 
     # Reset theta after execution or stopping
     reset_theta()
-    ser.write("FINISHED\n".encode())
+    with serial_lock:
+        ser.write("FINISHED\n".encode())
+        
     print("All requested patterns completed (or stopped).")
 
 def reset_theta():
     """Reset theta on the Arduino."""
-    ser.write("RESET_THETA\n".encode())
-    while True:
-        with serial_lock:
-            if ser.in_waiting > 0:
-                response = ser.readline().decode().strip()
-                print(f"Arduino response: {response}")
-                if response == "THETA_RESET":
-                    print("Theta successfully reset.")
-                    break
-        time.sleep(0.5)  # Small delay to avoid busy waiting
+    with serial_lock:
+        ser.write("RESET_THETA\n".encode())
+        while True:
+            with serial_lock:
+                if ser.in_waiting > 0:
+                    response = ser.readline().decode().strip()
+                    print(f"Arduino response: {response}")
+                    if response == "THETA_RESET":
+                        print("Theta successfully reset.")
+                        break
+            time.sleep(0.5)  # Small delay to avoid busy waiting
 
 # Flask API Endpoints
 @app.route('/')
@@ -335,7 +594,7 @@ def upload_theta_rho():
 @app.route('/run_theta_rho', methods=['POST'])
 def run_theta_rho():
     file_name = request.json.get('file_name')
-    pre_execution = request.json.get('pre_execution')  # 'clear_in', 'clear_out', 'clear_sideway', or 'none'
+    pre_execution = request.json.get('pre_execution')
 
     if not file_name:
         return jsonify({'error': 'No file name provided'}), 400
@@ -348,15 +607,6 @@ def run_theta_rho():
         # Build a list of files to run in sequence
         files_to_run = []
 
-        if pre_execution == 'clear_in':
-            files_to_run.append('./patterns/clear_from_in.thr')
-        elif pre_execution == 'clear_out':
-            files_to_run.append('./patterns/clear_from_out.thr')
-        elif pre_execution == 'clear_sideway':
-            files_to_run.append('./patterns/clear_sideway.thr')
-        elif pre_execution == 'none':
-            pass  # No pre-execution action required
-
         # Finally, add the main file
         files_to_run.append(file_path)
 
@@ -366,7 +616,7 @@ def run_theta_rho():
             args=(files_to_run,),
             kwargs={
                 'pause_time': 0,
-                'clear_pattern': None
+                'clear_pattern': pre_execution
             }
         ).start()
         return jsonify({'success': True})
@@ -374,10 +624,23 @@ def run_theta_rho():
     except Exception as e:
         return jsonify({'error': str(e)}), 500
 
+def stop_actions():
+    global pause_requested
+    with pause_condition:
+        pause_requested = False
+        pause_condition.notify_all()
+        
+    global stop_requested, current_playing_index, current_playlist, is_clearing, current_playing_file, execution_progress
+    stop_requested = True
+    current_playing_index = None
+    current_playlist = None
+    is_clearing = False
+    current_playing_file = None
+    execution_progress = None
+
 @app.route('/stop_execution', methods=['POST'])
 def stop_execution():
-    global stop_requested
-    stop_requested = True
+    stop_actions()
     return jsonify({'success': True})
 
 @app.route('/send_home', methods=['POST'])
@@ -499,9 +762,42 @@ 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,
+        "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():
     """
@@ -662,9 +958,11 @@ def run_playlist():
     {
         "playlist_name": "My Playlist",
         "pause_time": 1.0,                # Optional: seconds to pause between patterns
-        "clear_pattern": "random",         # Optional: "clear_in", "clear_out", "clear_sideway", or "random"
+        "clear_pattern": "random",         # Optional: "clear_from_in", "clear_from_out", "clear_sideway", "adaptive" or "random"
         "run_mode": "single",              # 'single' or 'indefinite'
         "shuffle": True                    # true or false
+        "start_time": ""
+        "end_time": ""
     }
     """
     data = request.get_json()
@@ -678,13 +976,15 @@ 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:
         return jsonify({"success": False, "error": "'pause_time' must be a non-negative number"}), 400
 
     # Validate clear_pattern
-    valid_patterns = ["clear_in", "clear_out", "clear_sideway", "random"]
+    valid_patterns = ["clear_from_in", "clear_from_out", "clear_sideway", "random", "adaptive"]
     if clear_pattern not in valid_patterns:
         clear_pattern = None
 
@@ -696,6 +996,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()
 
@@ -717,7 +1034,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()
@@ -750,7 +1068,222 @@ def set_speed():
     except Exception as e:
         return jsonify({"success": False, "error": str(e)}), 500
 
+@app.route('/get_firmware_info', methods=['GET', 'POST'])
+def get_firmware_info():
+    """
+    Compare the installed firmware version and motor type with the one in the .ino file.
+    """
+    global firmware_version, arduino_driver_type, ser
+
+    if ser is None or not ser.is_open:
+        return jsonify({"success": False, "error": "Arduino not connected or serial port not open"}), 400
+
+    try:
+        if request.method == "GET":
+            # Attempt to retrieve installed firmware details from the Arduino
+            time.sleep(0.5)
+
+            installed_version = firmware_version
+            installed_type = arduino_driver_type
+
+            # If Arduino provides valid details, proceed with comparison
+            if installed_version != 'Unknown' and installed_type != 'Unknown':
+                ino_path = MOTOR_TYPE_MAPPING.get(installed_type)
+                firmware_details = get_ino_firmware_details(ino_path)
+
+                if not firmware_details or not firmware_details.get("version") or not firmware_details.get("motorType"):
+                    return jsonify({"success": False, "error": "Failed to retrieve .ino firmware details"}), 500
+
+                update_available = (
+                    installed_version != firmware_details["version"] or
+                    installed_type != firmware_details["motorType"]
+                )
+
+                return jsonify({
+                    "success": True,
+                    "installedVersion": installed_version,
+                    "installedType": installed_type,
+                    "inoVersion": firmware_details["version"],
+                    "inoType": firmware_details["motorType"],
+                    "updateAvailable": update_available
+                })
+
+            # If Arduino details are unknown, indicate the need for POST
+            return jsonify({
+                "success": True,
+                "installedVersion": installed_version,
+                "installedType": installed_type,
+                "updateAvailable": False
+            })
+
+        elif request.method == "POST":
+            motor_type = request.json.get("motorType", None)
+            if not motor_type or motor_type not in MOTOR_TYPE_MAPPING:
+                return jsonify({
+                    "success": False,
+                    "error": "Invalid or missing motor type"
+                }), 400
+
+            # Fetch firmware details for the given motor type
+            ino_path = MOTOR_TYPE_MAPPING[motor_type]
+            firmware_details = get_ino_firmware_details(ino_path)
+
+            if not firmware_details:
+                return jsonify({
+                    "success": False,
+                    "error": "Failed to retrieve .ino firmware details"
+                }), 500
+
+            return jsonify({
+                "success": True,
+                "installedVersion": 'Unknown',
+                "installedType": motor_type,
+                "inoVersion": firmware_details["version"],
+                "inoType": firmware_details["motorType"],
+                "updateAvailable": True
+            })
+
+    except Exception as e:
+        return jsonify({"success": False, "error": str(e)}), 500
+
+@app.route('/flash_firmware', methods=['POST'])
+def flash_firmware():
+    """
+    Flash the pre-compiled firmware to the connected device (Arduino or ESP32).
+    """
+    global ser_port
+
+    # Ensure the device is connected
+    if ser_port is None or ser is None or not ser.is_open:
+        return jsonify({"success": False, "error": "No device connected or connection lost"}), 400
+
+    try:
+        data = request.json
+        motor_type = data.get("motorType", None)
+
+        # Validate motor type
+        if not motor_type or motor_type not in MOTOR_TYPE_MAPPING:
+            return jsonify({"success": False, "error": "Invalid or missing motor type"}), 400
+
+        # Determine the firmware file
+        ino_file_path = MOTOR_TYPE_MAPPING[motor_type]  # Path to .ino file
+        hex_file_path = f"{ino_file_path}.hex"
+        bin_file_path = f"{ino_file_path}.bin"  # For ESP32 firmware
+
+        # Check the device type
+        if motor_type.lower() in ["esp32", "esp32_tmc2209"]:
+            if not os.path.exists(bin_file_path):
+                return jsonify({"success": False, "error": f"Firmware binary not found: {bin_file_path}"}), 404
+
+            # Flash ESP32 firmware
+            flash_command = [
+                "esptool.py",
+                "--chip", "esp32",
+                "--port", ser_port,
+                "--baud", "115200",
+                "write_flash", "-z", "0x1000", bin_file_path
+            ]
+        else:
+            if not os.path.exists(hex_file_path):
+                return jsonify({"success": False, "error": f"Hex file not found: {hex_file_path}"}), 404
+
+            # Flash Arduino firmware
+            flash_command = [
+                "avrdude",
+                "-v",
+                "-c", "arduino",
+                "-p", "atmega328p",
+                "-P", ser_port,
+                "-b", "115200",
+                "-D",
+                "-U", f"flash:w:{hex_file_path}:i"
+            ]
+
+        # Execute the flash command
+        flash_process = subprocess.run(flash_command, capture_output=True, text=True)
+        if flash_process.returncode != 0:
+            return jsonify({
+                "success": False,
+                "error": flash_process.stderr
+            }), 500
+
+        return jsonify({"success": True, "message": "Firmware flashed successfully"})
+    except Exception as e:
+        return jsonify({"success": False, "error": str(e)}), 500
+
+@app.route('/check_software_update', methods=['GET'])
+def check_updates():
+    update_info = check_git_updates()
+    return jsonify(update_info)
+
+@app.route('/update_software', methods=['POST'])
+def update_software():
+    error_log = []
+
+    def run_command(command, error_message):
+        try:
+            subprocess.run(command, check=True)
+        except subprocess.CalledProcessError as e:
+            print(f"{error_message}: {e}")
+            error_log.append(error_message)
+
+    # Fetch the latest version tag from remote
+    try:
+        subprocess.run(["git", "fetch", "--tags"], check=True)
+        latest_remote_tag = subprocess.check_output(
+            ["git", "describe", "--tags", "--abbrev=0", "origin/main"]
+        ).strip().decode()
+    except subprocess.CalledProcessError as e:
+        error_log.append(f"Failed to fetch tags or get latest remote tag: {e}")
+        return jsonify({
+            "success": False,
+            "error": "Failed to fetch tags or determine the latest version.",
+            "details": error_log
+        }), 500
+
+    # Checkout the latest tag
+    run_command(["git", "checkout", latest_remote_tag, '--force'], f"Failed to checkout version {latest_remote_tag}")
+
+    run_command(["docker", "compose", "pull"], "Failed to fetch Docker containers")
+
+    # Restart Docker containers
+    run_command(["docker", "compose", "up", "-d"], "Failed to restart Docker containers")
+
+    # Check if the update was successful
+    update_status = check_git_updates()
+
+    if (
+        update_status["updates_available"] is False
+        and update_status["latest_local_tag"] == update_status["latest_remote_tag"]
+    ):
+        # Update was successful
+        return jsonify({"success": True})
+    else:
+        # Update failed; include the errors in the response
+        return jsonify({
+            "success": False,
+            "error": "Update incomplete",
+            "details": error_log
+        }), 500
+
+
+def on_exit():
+    """Function to execute on application shutdown."""
+    print("Shutting down the application...")
+    stop_actions()
+    time.sleep(5)
+    print("Execution stopped and resources cleaned up.")
+
+# Register the on_exit function
+atexit.register(on_exit)
+        
+        
 if __name__ == '__main__':
     # Auto-connect to serial
     connect_to_serial()
-    app.run(debug=True, host='0.0.0.0', port=8080)
+    try:
+        app.run(debug=False, host='0.0.0.0', port=8080)
+    except KeyboardInterrupt:
+        print("Keyboard interrupt received. Shutting down.")
+    finally:
+        on_exit()  # Ensure cleanup if app is interrupted

+ 2 - 2
docker-compose.yml

@@ -1,6 +1,6 @@
 services:
   flask-app:
-    # build: . # Uncomment this if you need to build 
+    build: . # Uncomment this if you need to build 
     image: ghcr.io/tuanchris/dune-weaver:main # Use latest production image
     restart: always
     ports:
@@ -12,4 +12,4 @@ services:
     privileged: true
     environment:
       - FLASK_ENV=development # Set environment variables for Flask
-    container_name: flask-theta-rho-app
+    container_name: flask-theta-rho-app

+ 19 - 2
arduino_code/arduino_code.ino → firmware/arduino_code/arduino_code.ino

@@ -48,6 +48,10 @@ float userDefinedSpeed = maxSpeed; // Store user-defined speed
 // Running Mode
 int currentMode = MODE_APP; // Default mode is app mode.
 
+// FIRMWARE VERSION
+const char* firmwareVersion = "1.4.0";
+const char* motorType = "DRV8825";
+
 void setup()
 {
     // Set maximum speed and acceleration
@@ -72,6 +76,13 @@ void setup()
     homing();
 }
 
+void getVersion()
+{
+    Serial.println("Table: Dune Weaver");
+    Serial.println("Drivers: DRV8825");
+    Serial.println("Version: 1.4.0");
+}
+
 void resetTheta()
 {
     isFirstCoordinates = true; // Set flag to skip interpolation for the next movement
@@ -106,6 +117,7 @@ void handleModeChange(int newMode) {
         Serial.println("Spirograph Mode Active");
         rotStepper.setMaxSpeed(userDefinedSpeed * 0.5); // Use 50% of user-defined speed
         inOutStepper.setMaxSpeed(userDefinedSpeed * 0.5);
+        isFirstCoordinates = false;
     } else if (newMode == MODE_APP) {
         Serial.println("App Mode Active");
         rotStepper.setMaxSpeed(userDefinedSpeed); // Restore user-defined speed
@@ -180,12 +192,18 @@ void appMode()
         String input = Serial.readStringUntil('\n');
 
         // Ignore invalid messages
-        if (input != "HOME" && input != "RESET_THETA" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
+        if (input != "HOME" && input != "RESET_THETA" && input != "GET_VERSION" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
         {
             Serial.print("IGNORED: ");
             Serial.println(input);
             return;
         }
+
+        if (input == "GET_VERSION")
+        {
+            getVersion();
+        }
+
         if (input == "RESET_THETA")
         {
             resetTheta(); // Reset currentTheta
@@ -221,7 +239,6 @@ void appMode()
                     inOutStepper.setMaxSpeed(newSpeed);
 
                     Serial.println("SPEED_SET");  
-                    Serial.println("R");
                 }
                 else
                 {

+ 894 - 0
firmware/arduino_code/arduino_code.ino.hex

@@ -0,0 +1,894 @@
+:100000000C948F000C94B7000C94B7000C94B700BC
+:100010000C94B7000C94B7000C94B7000C94B70084
+:100020000C94B7000C94B7000C94B7000C94B70074
+:100030000C94B7000C94B7000C94B7000C94B70064
+:100040000C94160B0C94B7000C94860B0C94600B5C
+:100050000C94B7000C94B7000C94B7000C94B70044
+:100060000C94B7000C94B70005A84CCDB2D44EB98F
+:100070003836A9020C50B9918688083CA6AAAA2A4B
+:10008000BE000000803F4E414E494E495459494EF2
+:1000900046CDCCCC3D0AD7233C17B7D13877CC2BF3
+:1000A000329595E6241FB14F0A000020410000C898
+:1000B0004200401C4620BCBE4CCA1B0E5AAEC59D19
+:1000C0007400000000230026002900000000002426
+:1000D0000027002A0000000000250028002B000453
+:1000E00004040404040404020202020202030303DF
+:1000F00003030301020408102040800102040810D9
+:100100002001020408102000000008000201000085
+:10011000030407000000000000000000B80B1124D9
+:100120001FBECFEFD8E0DEBFCDBF23E0A6E1B2E037
+:1001300001C01D92A83CB207E1F712E0A0E0B1E0D7
+:10014000E2EAF6E302C005900D92A631B107D9F7B5
+:1001500010E0CFE8D0E004C02197FE010E94F9181A
+:10016000CE38D107C9F70E94CA0C0C944F1B0C94CF
+:100170000000833081F028F4813099F08230A9F0BA
+:1001800008958730A9F08830C9F08430B1F48091A7
+:1001900080008F7D03C0809180008F7780938000E6
+:1001A000089584B58F7784BD089584B58F7DFBCF86
+:1001B0008091B0008F778093B00008958091B00057
+:1001C0008F7DF9CF1F93CF93DF93282F30E0F90174
+:1001D000E95FFE4F8491F901ED50FF4FD491F90191
+:1001E000E152FF4FC491CC23A9F0162F81110E9438
+:1001F000B900EC2FF0E0EE0FFF1FEB52FF4FA5917F
+:10020000B4918FB7F894EC91111108C0D095DE230A
+:10021000DC938FBFDF91CF911F910895DE2BF8CF34
+:10022000CF93DF9390E0FC01ED50FF4F249181527A
+:100230009F4FFC0184918823C9F090E0880F991F9B
+:10024000FC01E553FF4FA591B491FC01EB52FF4F28
+:10025000C591D49161110DC09FB7F8948C912095F0
+:1002600082238C938881282328839FBFDF91CF919D
+:100270000895623051F49FB7F8943C91822F809595
+:1002800083238C93E8812E2BEFCF8FB7F894EC91DA
+:100290002E2B2C938FBFEACF8E50806480937C00EE
+:1002A00080917A00806480937A0080917A0086FD44
+:1002B000FCCF80917800909179000895AF92BF9221
+:1002C000CF92DF92EF92FF920F931F93CF93DF9322
+:1002D0006C017B018B01040F151FEB015E01AE1851
+:1002E000BF08C017D10759F06991D601ED91FC9173
+:1002F0000190F081E02DC6010995892B79F7C501A0
+:10030000DF91CF911F910F91FF90EF90DF90CF90F1
+:10031000BF90AF900895FC01538D448D252F30E0A0
+:10032000842F90E0821B930B541710F0CF96089502
+:1003300001970895FC01918D828D981761F0A28D2F
+:10034000AE0FBF2FB11D5D968C91928D9F5F9F73F5
+:10035000928F90E008958FEF9FEF08952FB7F89454
+:100360008091800290918102A0918202B0918302DB
+:100370002FBF80938C0290938D02A0938E02B09336
+:100380008F0284E892E00E949A0197FF26C02FB75F
+:10039000F8948091800290918102A0918202B091A4
+:1003A00083022FBF40918C0250918D0260918E028A
+:1003B00070918F02841B950BA60BB70B409188029E
+:1003C0005091890260918A0270918B02841795077F
+:1003D000A607B707B0F28FEF9FEF0895FC01918D4C
+:1003E000828D981731F0828DE80FF11D858D90E098
+:1003F00008958FEF9FEF0895FC01918D228D892F35
+:1004000090E0805C9F4F821B91098F73992708951C
+:1004100084E892E00E94FC0121E0892B09F420E0AD
+:10042000822F089580E090E0892B29F00E94080235
+:1004300081110C9400000895FC01A48DA80FB92F20
+:10044000B11DA35ABF4F2C91848D90E001968F73FC
+:100450009927848FA689B7892C93A089B1898C911B
+:10046000837080648C93938D848D981306C002886A
+:10047000F389E02D80818F7D80830895EF92FF9234
+:100480000F931F93CF93DF93EC0181E0888F9B8DB7
+:100490008C8D98131AC0E889F989808185FF15C071
+:1004A0009FB7F894EE89FF896083E889F989808194
+:1004B0008370806480839FBF81E090E0DF91CF9163
+:1004C0001F910F91FF90EF900895F62E0B8D10E085
+:1004D0000F5F1F4F0F731127E02E8C8D8E110CC0F4
+:1004E0000FB607FCFACFE889F989808185FFF5CF3F
+:1004F000CE010E941C02F1CFEB8DEC0FFD2FF11D00
+:10050000E35AFF4FF0829FB7F8940B8FEA89FB897B
+:1005100080818062CFCFCF93DF93EC01888D8823D9
+:10052000B9F0AA89BB89E889F9898C9185FD03C056
+:10053000808186FD0DC00FB607FCF7CF8C9185FF3B
+:10054000F2CF808185FFEDCFCE010E941C02E9CF62
+:10055000DF91CF9108954F925F926F927F92AF9209
+:10056000BF92CF92DF92EF92FF920F931F93CF93A0
+:10057000DF93EC015A018B014C8C5D8C6E8C7F8C6F
+:1005800073016201F7FAF094F7F8F0949A01AB0165
+:10059000C701B6010E942918181664F09501A80138
+:1005A000C301B2010E94AC147301620187FD02C055
+:1005B0006501780120E030E0A901C701B6010E9481
+:1005C000AC14811117C01B821C821D821E82C88E32
+:1005D000D98EEA8EFB8EDF91CF911F910F91FF9004
+:1005E000EF90DF90CF90BF90AF907F906F905F9033
+:1005F0004F900895A701960160E074E284E799E4C2
+:100600000E9496169F770E940F176B837C838D83C1
+:100610009E8311E020E030E0A901C701B6010E94ED
+:10062000291818160CF010E01A83D1CFCF92DF9260
+:10063000EF92FF92CF93DF93EC013FB7F894809154
+:100640007B0290917C02A0917D02B0917E0226B542
+:10065000A89B05C02F3F19F00196A11DB11D3FBFFA
+:10066000BA2FA92F982F88276C017D01C20ED11CAB
+:10067000E11CF11C42E0CC0CDD1CEE1CFF1C4A9579
+:10068000D1F788A599A5AAA5BBA5B701A601481BC6
+:10069000590B6A0B7B0B8B819C81AD81BE81481706
+:1006A00059076A077B0748F188899989AA89BB8914
+:1006B0002A812223F1F00196A11DB11D888B998B0F
+:1006C000AA8BBB8B488959896A897B89E881F98122
+:1006D0000484F585E02DCE010995C8A6D9A6EAA621
+:1006E000FBA681E0DF91CF91FF90EF90DF90CF905C
+:1006F00008950197A109B109E1CF80E0F3CFCF932D
+:10070000DF93FC012781222359F1EC0161E0808510
+:100710000E94100161E089850E9410018F81843060
+:1007200011F08830B1F461E08A850E94100161E027
+:100730008B850E9410018FA58F3F91F061E00E9490
+:1007400010016EA581E068278FA5DF91CF910C94F1
+:10075000E200833011F0863071F761E08A85E9CFDD
+:10076000DF91CF910895CF93DF93FC01278122235E
+:10077000A9F0EC010190F081E02D0284F385E02DD9
+:1007800060E009958FA58F3F49F061E00E9410015C
+:100790006EA58FA5DF91CF910C94E200DF91CF91F0
+:1007A0000895DC01ED91FC91228533854770552732
+:1007B0006627772741505109610971094730510572
+:1007C0006105710560F4FA01E851FC4F0C94F918C9
+:1007D000F203F403F603F803FA03FC03FE0361E0FB
+:1007E000F901099465E0FCCF64E0FACF66E0F8CF48
+:1007F00062E0F6CF6AE0F4CF68E0F2CF69E0F0CFD4
+:10080000CF93DF93EC01CB01BA0126E030E040E06A
+:1008100050E00E94DA18623071058105910589F176
+:100820006CF46115710581059105D1F06130710598
+:1008300081059105F9F0DF91CF910895643071053C
+:100840008105910561F124F16530710581059105FE
+:1008500091F7E881F9810284F385E02D66E006C016
+:10086000E881F9810284F385E02D64E0CE01DF9117
+:10087000CF910994E881F9810284F385E02D65E048
+:10088000F5CFE881F9810284F385E02D61E0EECFB8
+:10089000E881F9810284F385E02D63E0E7CFE88108
+:1008A000F9810284F385E02D62E0E0CFDC01ED9177
+:1008B000FC910284F385E02D437055276627772746
+:1008C000423051056105710571F0433051056105F4
+:1008D000710559F0413051056105710511F065E070
+:1008E000099466E0FDCF6AE0FBCF69E0F9CFCF93D2
+:1008F000DF93EC01CB01BA0123E030E040E050E0AF
+:100900000E94DA18613071058105910599F0623015
+:10091000710581059105A9F0672B682B692BC1F43E
+:10092000E881F9810284F385E02D64E0CE01DF9156
+:10093000CF910994E881F9810284F385E02D61E08B
+:10094000F5CFE881F9810284F385E02D62E0EECFF6
+:10095000DF91CF910895DC01ED91FC910284F38544
+:10096000E02D4370552766277727423051056105F2
+:10097000710571F0433051056105710559F0413041
+:1009800051056105710511F062E0099463E0FDCF46
+:1009900061E0FBCF60E0F9CFCF93DF93EC01E8811A
+:1009A000F9810284F385E02D8A8160E0811162E0A3
+:1009B000CE010995E881F9810284F385E02D8A81D1
+:1009C00061E0811163E0CE0109958CA59DA582307F
+:1009D000910538F0880F991F880F991F0597019787
+:1009E000F1F7E881F9810284F385E02D8A8160E0E6
+:1009F000811162E0CE01DF91CF910994CF93DF9313
+:100A0000EC0120E030E0A901688D798D8A8D9B8D05
+:100A10000E94291818162CF4E8A9F9A9DF91CF91A2
+:100A20000994EAA9FBA9FACFCF92DF92EF92FF9245
+:100A30000F931F93CF93DF93DC011796CC91C430B3
+:100A400039F0C83059F1C33019F0C63049F1C2E06D
+:100A50008C01085F1F4FF12CE12CC62ED12CD1E068
+:100A6000F8016481C6010E2C02C0959587950A9401
+:100A7000E2F780FD6D270F5F1F4F80810E94E2002B
+:100A8000BFEFEB1AFB0AEC1658F3DF91CF911F91E1
+:100A90000F91FF90EF90DF90CF900895C4E0D8CFF2
+:100AA000C3E0D6CFDC011796EC911797E93008F038
+:100AB00038C0F0E0E25AFA4F0C94F91867056D055A
+:100AC000730579057F059105850591058B05ED91E8
+:100AD000FC910684F785E02D0994ED91FC91008846
+:100AE000F189E02DF9CFED91FC910288F389E02D99
+:100AF000F3CFED91FC910488F589E02DEDCFED91D8
+:100B0000FC910688F789E02DE7CFED91FC91008CF0
+:100B1000F18DE02DE1CFED91FC91028CF38DE02D74
+:100B2000DBCF08954F925F926F927F928F929F9248
+:100B3000AF92BF92CF92DF92EF92FF920F931F93EB
+:100B4000CF93DF93EC01CC88DD88EE88FF8888891D
+:100B50009989AA89BB89C81AD90AEA0AFB0A688D49
+:100B6000798D8A8D9B8D9B01AC010E9412154B01E2
+:100B70005C0168A179A18AA19BA19B01AC010E94A3
+:100B800025169B01AC01C501B4010E9496160E9476
+:100B90000817C114D104E104F10409F0B6C06230B1
+:100BA0007105810591050CF0D0C01B821C821D824D
+:100BB0001E82188E198E1A8E1B8E1CAA1DAA1EAAA2
+:100BC0001FAAC12CD12C7601C701B601DF91CF91AC
+:100BD0001F910F91FF90EF90DF90CF90BF90AF905B
+:100BE0009F908F907F906F905F904F900895101618
+:100BF000110612061306B4F46C157D058E059F05CB
+:100C00001CF42A812111A1C09B01AC0188279927DE
+:100C1000DC01821B930BA40BB50B8CAB9DABAEAB75
+:100C2000BFAB93C0011511052105310509F48DC035
+:100C30006C157D058E059F050CF087C08A81882381
+:100C400009F483C030952095109501951F4F2F4FC3
+:100C50003F4F0CAB1DAB2EAB3FAB77C00115110561
+:100C60002105310509F471C08824992454018C1898
+:100C70009D08AE08BF08681579058A059B050CF02C
+:100C800064C08A81811161C0DDCFCCACDDACEEAC3B
+:100C9000FFACA7019601C701B6010E9425162B01E2
+:100CA0003C01C501B4010E94B31420E030E040E8EB
+:100CB00050E40E94121520E030E040E85FE30E941B
+:100CC00025169B01AC01C301B2010E9496169B013F
+:100CD000AC01C701B6010E9424163B016C01FE0164
+:100CE000E05CFF4FE080F180028113819701A80151
+:100CF0000E942918181614F473018601C701D8013F
+:100D00008CAF9DAFAEAFBFAF3AC00CA91DA92EA945
+:100D10003FA91C141D041E041F040CF468CF1016F8
+:100D20001106120613060CF099CF0027112798011F
+:100D30000C191D092E093F096017710782079307D7
+:100D40000CF062CF2A8121115FCF8CA89DA8AEA89C
+:100D5000BFA881149104A104B10409F096CF88AD15
+:100D600099ADAAADBBAD8CAF9DAFAEAFBFAF81E0CB
+:100D70001C141D041E041F040CF080E08A833FEF46
+:100D8000831A930AA30AB30A8CAA9DAAAEAABFAA81
+:100D90008CAC9DACAEACBFACC501B4010E940F17CA
+:100DA0006B017C01CB82DC82ED82FE82A501940185
+:100DB00060E074E284E799E40E949616688F798F68
+:100DC0008A8F9B8F2A812111FFCE9058688F798F4F
+:100DD0008A8F9B8FF9CECF92DF92EF92FF920F9383
+:100DE0001F93CF93DF93EC016A017B0120E030E099
+:100DF000A901CB01B6010E94AC1487FF04C0F7FA29
+:100E0000F094F7F8F094A70196016C8D7D8D8E8D8E
+:100E10009F8D0E94AC14882309F446C0CC8EDD8ED1
+:100E2000EE8EFF8E8E01005C1F4FA701960160E0E1
+:100E300074E284E799E40E949616F8016083718356
+:100E4000828393838CA99DA9AEA9BFA91816190600
+:100E50001A061B064CF5688D798D8A8D9B8D9B013A
+:100E6000AC010E9412156B017C0168A179A18AA1D5
+:100E70009BA19B01AC010E9425169B01AC01C701FF
+:100E8000B6010E9496160E9408176CAB7DAB8EAB24
+:100E90009FABCE01DF91CF911F910F91FF90EF900B
+:100EA000DF90CF900C949205DF91CF911F910F911D
+:100EB000FF90EF90DF90CF90089508952F923F928A
+:100EC0004F925F926F927F928F929F92AF92BF925A
+:100ED000CF92DF92EF92FF920F931F93CF93DF9306
+:100EE000CDB7DEB762970FB6F894DEBF0FBECDBFA9
+:100EF0006B017C012D873E874F87588B2AEA37E2AA
+:100F00004FE155E40E9412150E9408172B013C0185
+:100F100020E030E044EB55E46D857E858F8598892F
+:100F20000E9412150E9408174B015C0120912202B9
+:100F3000309123024091240250912502C701B6014D
+:100F40000E9424162BED3FE049EC50E40E949616D7
+:100F50009B01AC0160911A0270911B0280911C02EE
+:100F600090911D020E94251660931A0270931B0235
+:100F700080931C0290931D028091040181111AC07C
+:100F80002BED3FE049EC50E4C701B6010E949616F4
+:100F900020E030E04AE756E40E94121520E030E0FD
+:100FA00040E251E40E9496160E940817861A970A9A
+:100FB000A80AB90A49825A826B827C828D829E82FB
+:100FC000AF82B886209039039E012F5F3F4F2901E1
+:100FD00045E253E05A874987912C312C00E010E01C
+:100FE000812C2914C9F1D2016D917D918D919D9132
+:100FF0002D01E985FA85A190B190FA87E987D5019D
+:1010000050962D913D914D915C915397621B730BBE
+:10101000840B950B97FF07C090958095709561950F
+:101020007F4F8F4F9F4F0E94B314F501248D358D54
+:10103000468D578D0E9496163B015C01232D302F63
+:10104000412F582D0E942918181624F4362C072DEC
+:101050001A2D8B2C9394C5CF20E030E0A901632D8D
+:10106000702F812F982D0E94291818160CF06CC033
+:10107000912C80913903981608F066C0892D90E074
+:10108000FC01EE0FFF1FEE0FFF1F21E030E02C0FE1
+:101090003D1FE20FF31F4080518062807380AC01DE
+:1010A000440F551F5A8B498BFA01EB5DFC4FA08012
+:1010B000B180F50180899189A289B389A301920148
+:1010C000281B390B4A0B5B0BCA01B9010E94B314F0
+:1010D000232D302F412F582D0E94961669877A872D
+:1010E0008B879C87F50184899589A689B7894816DD
+:1010F00059066A067B0661F0448A558A668A778AB1
+:101100000190F081E02D0084F185E02DC501099565
+:10111000E989FA89EB5DFC4FA080B180F501208D53
+:10112000318D428D538D69857A858B859C850E9492
+:10113000AC14882339F049855A856B857C85C501B7
+:101140000E94AB02939495CF80E010E09091390318
+:10115000191720F5E12FF0E0EE0FFF1FEB5DFC4FBC
+:101160000190F081E02D84889588A688B788408911
+:1011700051896289738984169506A606B70661F0BF
+:1011800083819481A581B681892B8A2B8B2B19F0C1
+:10119000CF010E94160381E01F5FD8CF8111D4CF09
+:1011A000C0922202D0922302E0922402F092250201
+:1011B0002D853E854F85588920931E0230931F024E
+:1011C000409320025093210262960FB6F894DEBF3E
+:1011D0000FBECDBFDF91CF911F910F91FF90EF9088
+:1011E000DF90CF90BF90AF909F908F907F906F9047
+:1011F0005F904F903F902F900895FC010190002048
+:10120000E9F73197AF01481B590BBC0184E892E024
+:101210000C945E01CF93DF930E94FD08EC018DE3F7
+:1012200091E00E94FD088C0F9D1FDF91CF910895E2
+:1012300080E491E00E940A0920E030E44CE955EC9A
+:101240006091520370915303809154039091550320
+:101250000E94AC14882341F040E050E46CE975EC46
+:101260008AE393E00E94AB0280913D0390913E039C
+:10127000A0913F03B0914003892B8A2B8B2B21F047
+:101280008AE393E00E94160360914A0370914B0336
+:1012900080914C0390914D030E94B31420E030E004
+:1012A00046EC55EC0E94AC141816F4F210924A0366
+:1012B00010924B0310924C0310924D0310924E0368
+:1012C00010924F03109250031092510310926E032C
+:1012D00010926F03109270031092710310923D03ED
+:1012E00010923E0310923F0310924003109252035B
+:1012F000109253031092540310925503109222023D
+:1013000010922302109224021092250210921E02C3
+:1013100010921F02109220021092210287E491E0A5
+:101320000C940A094F925F926F927F928F929F92D4
+:10133000AF92BF92CF92DF92EF92FF92CF93DF9363
+:10134000EC016A017B0120E030E0A901CB01B6018C
+:101350000E94AC1487FF04C0F7FAF094F7F8F094F9
+:1013600088A099A0AAA0BBA0A7019601C501B401BD
+:101370000E94AC14882309F44DC06CA97DA98EA9E4
+:101380009FA90E94B3142B013C01A7019601C5013E
+:10139000B4010E9496169B01AC01C301B2010E94E8
+:1013A00012150E9408176CAB7DAB8EAB9FABA701EB
+:1013B000960160E070E080E090E40E9496160E9442
+:1013C000581826E53EE04DE25FE30E94121520E04A
+:1013D00034E244E759E40E94121568AF79AF8AAF4E
+:1013E0009BAFC8A2D9A2EAA2FBA2E881F98100843E
+:1013F000F185E02DCE01DF91CF91FF90EF90DF904E
+:10140000CF90BF90AF909F908F907F906F905F90A4
+:101410004F900994DF91CF91FF90EF90DF90CF90A4
+:10142000BF90AF909F908F907F906F905F904F9004
+:101430000895FC0124813581232B39F421E0FB013F
+:101440008081882349F020E007C0808191810E943B
+:10145000171B21E0892BB9F7822F0895FC018081A9
+:101460009181009711F00C94BD1908950C94BD1949
+:10147000FB0144815581FC01248135812417350706
+:1014800078F080819181009759F0FB016081718132
+:101490006115710529F00E94271B21E0892B09F0B5
+:1014A00020E0822F08950F931F93CF93DF93EC01D9
+:1014B00088819981009759F02A813B812617370747
+:1014C00030F081E0DF91CF911F910F9108958B0152
+:1014D0006F5F7F4F0E94461A009759F09983888367
+:1014E0001B830A832C813D81232B59F7FC01108239
+:1014F000E8CF80E0E7CFEF92FF920F931F93CF9357
+:10150000DF93EC017B018A01BA010E94530A288112
+:101510003981811114C02115310519F0C9010E94CA
+:10152000BD19198218821D821C821B821A82CE016B
+:10153000DF91CF911F910F91FF90EF9008951D8340
+:101540000C83B701C9010E94201BF1CFFC0111825D
+:1015500010821382128215821482FB0101900020F6
+:10156000E9F73197AF01461B570B0C947B0AAF92FA
+:10157000BF92CF92DF92EF92FF920F931F93CF9380
+:10158000DF93EC016B015A0179012417350720F430
+:101590008B2D5901E42EF82E6FE371E0CE010E94ED
+:1015A000A60AD60114960D911C91A016B10628F535
+:1015B000E016F10608F48701D601ED91FC91119730
+:1015C000E00FF11FF08010826D917C916A0D7B1D00
+:1015D00061157105F1F0FB0101900020E9F73197E9
+:1015E000AF01461B570BCE010E947B0AF60180819A
+:1015F0009181080F191FD801FC92CE01DF91CF9184
+:101600001F910F91FF90EF90DF90CF90BF90AF9020
+:10161000089588819981009711F00E94BD1919825F
+:1016200018821D821C821B821A82E0CF1F920F92A9
+:101630000FB60F9211242F933F938F939F93AF93E5
+:10164000BF938091800290918102A0918202B0911B
+:10165000830230917F0223E0230F2D3758F5019646
+:10166000A11DB11D20937F0280938002909381027F
+:10167000A0938202B093830280917B0290917C02BE
+:10168000A0917D02B0917E020196A11DB11D8093B3
+:101690007B0290937C02A0937D02B0937E02BF9167
+:1016A000AF919F918F913F912F910F900FBE0F900F
+:1016B0001F90189526E8230F0296A11DB11DD2CFC9
+:1016C0001F920F920FB60F9211242F933F934F93B7
+:1016D0005F936F937F938F939F93AF93BF93EF939A
+:1016E000FF9384E892E00E941C02FF91EF91BF916A
+:1016F000AF919F918F917F916F915F914F913F91AA
+:101700002F910F900FBE0F901F9018951F920F9260
+:101710000FB60F9211242F938F939F93EF93FF9304
+:10172000E0919402F09195028081E0919A02F0910B
+:101730009B0282FD1BC0908180919D028F5F8F7301
+:1017400020919E02821741F0E0919D02F0E0EC575B
+:10175000FD4F958F80939D02FF91EF919F918F9107
+:101760002F910F900FBE0F901F9018958081F4CF8E
+:101770008F929F92AF92BF92CF92DF92EF92FF92A1
+:101780000F931F93CF93DF93E4E8F2E0138212826A
+:1017900088EE93E0A0E0B0E084839583A683B783CE
+:1017A0008FE091E09183808385EC90E0958784873A
+:1017B00084EC90E09787868780EC90E0918B808B1B
+:1017C00081EC90E0938B828B82EC90E0958B848B04
+:1017D00086EC90E0978B868B118E128E138E148E72
+:1017E000EEE7F3E001E211E01183008388248394A3
+:1017F0008782108A118A128A138A148A158A168A95
+:10180000178A108E118E128E138E148E158E168ED0
+:10181000178E10A211A212A213A2C12CD12C80E803
+:10182000E82E8FE3F82EC4A2D5A2E6A2F7A2138277
+:10183000148215821682C1E0D0E0D5A7C4A79924EE
+:101840009A9497A610A611A612A613A682E08087E6
+:1018500095E0B92EB18624E0A22EA286B38616A604
+:1018600014AA15AA16AA17AA10AE11AE12AE13AE7C
+:1018700014AE15AE16AE17AEC092BE03D092BF0323
+:10188000E092C003F092C103128214861586168678
+:101890001786CF010E947F03B701A6018EE793E070
+:1018A0000E949209B701A6018EE793E00E94EB0621
+:1018B000EAE3F3E0118300838782108A118A128A97
+:1018C000138A148A158A168A178A108E118E128E20
+:1018D000138E148E158E168E178E10A211A212A2C0
+:1018E00013A2C4A2D5A2E6A2F7A213821482158283
+:1018F0001682D5A7C4A797A610A611A612A613A64E
+:1019000083E0808786E08187A286B38616A614AA24
+:1019100015AA16AA17AA10AE11AE12AE13AE14AEC7
+:1019200015AE16AE17AEC0927A03D0927B03E0924A
+:101930007C03F0927D031282148615861686178624
+:10194000CF010E947F03B701A6018AE393E00E94C2
+:101950009209B701A6018AE393E00E94EB06109278
+:10196000390380E090E4ACE9B5E4809321039093DF
+:101970002203A0932303B0932403DF91CF911F91FF
+:101980000F91FF90EF90DF90CF90BF90AF909F901E
+:101990008F900895CF93DF93CDB7DEB7A6970FB69C
+:1019A000F894DEBF0FBECDBF789484B5826084BD4D
+:1019B00084B5816084BD85B5826085BD85B5816053
+:1019C00085BD80916E00816080936E0010928100D1
+:1019D000809181008260809381008091810081608C
+:1019E000809381008091800081608093800080914D
+:1019F000B10084608093B1008091B00081608093D9
+:101A0000B00080917A00846080937A0080917A009F
+:101A1000826080937A0080917A00816080937A005E
+:101A200080917A00806880937A001092C10040E033
+:101A300050E46CE975E48EE793E00E94EB0640E029
+:101A400050E46CE975E48EE793E00E94920940E06F
+:101A500050E46CE975E48AE393E00E94EB0640E011
+:101A600050E46CE975E48AE393E00E949209E09106
+:101A70003903EA3068F481E08E0F80933903F0E097
+:101A8000EE0FFF1FEB5DFC4F8EE793E091838083A9
+:101A9000E0913903EA3068F481E08E0F80933903D6
+:101AA000F0E0EE0FFF1FEB5DFC4F8AE393E09183C4
+:101AB000808362E08BE00E94100160E08EE00E9473
+:101AC000100160E08FE00E941001E0919402F0911B
+:101AD000950282E08083E0919002F0919102108261
+:101AE000E0919202F091930280E1808310929C0237
+:101AF000E0919802F091990286E08083E09196024D
+:101B0000F0919702808180618083E0919602F0914C
+:101B10009702808188608083E0919602F09197021D
+:101B2000808180688083E0919602F09197028081A5
+:101B30008F7D80838DE491E00E940A090E9418093C
+:101B4000E2E1F1E08491EEEFF0E00491EAEEF0E002
+:101B50001491112309F4C4C181110E94B900E12F2D
+:101B6000F0E0EE0FFF1FEF53FF4FA591B4918C9162
+:101B7000082391E080E009F090E0092F182F809170
+:101B8000790290917A0280179107B9F1013011051D
+:101B900009F0A9C18FE491E00E940A0920E030E039
+:101BA00040E05FE360912103709122038091230361
+:101BB000909124030E9412156B017C01BC01A601C7
+:101BC0008EE793E00E94EB06B701A6018AE393E05B
+:101BD0000E94EB0610920401609122027091230290
+:101BE000809124029091250220E030E0A9010E941A
+:101BF0005E0710937A0200937902809179029091A6
+:101C00007A028130910509F09BC18FE00E944C015E
+:101C1000BC01990F880B990B0E94B31420E030E0AF
+:101C200040EB50E40E94121520E030EC4FE754E402
+:101C30000E94961620E030E040E05FE30E94251607
+:101C400020E030E040E251E40E9412150E942E187C
+:101C500020E030E040E251E40E9496166B017C01E6
+:101C600020E030E040E85FE30E94601720E030E0D1
+:101C700040E05FE30E94291887FD55C1C701B60106
+:101C80000E943E1723E333E343E75FE30E942516F8
+:101C90006B017C018090000190900101A0900201F5
+:101CA000B0900301409022025090230260902402E1
+:101CB00070902502AC019B01C501B4010E94AC14D7
+:101CC000882331F1A7019601C501B4010E942416B1
+:101CD000A30192010E9412159B01AC0160911602B2
+:101CE0007091170280911802909119020E94251696
+:101CF0006093160270931702809318029093190252
+:101D0000C0920001D0920101E0920201F092030121
+:101D10008EE00E944C01BC01990F880B990B0E9428
+:101D2000B31420E030E040E05FE30E94121520E0B1
+:101D300030EC4FE754E40E94961620E030E0A90111
+:101D40000E94251620E030E040EA51E40E9412157E
+:101D50000E942E1820E030E040EA51E40E949616DE
+:101D60006B017C01AC019B0160E070E080E89FE3C7
+:101D70000E94241620E030E040E05FE30E9412154C
+:101D80004B015C01AC019B01C701B6010E94251605
+:101D90006B8F7C8F8D8F9E8F2BED3FE049E45EE350
+:101DA000C301B2010E9425166B017C012BED3FE0BF
+:101DB00049EC50E40E94961660931A0270931B023D
+:101DC00080931C0290931D0280910001909101016B
+:101DD000A0910201B09103018B8B9C8BAD8BBE8BCC
+:101DE0008091160290911702A0911802B0911902E9
+:101DF0008F8B988FA98FBA8FA30192016B897C89F1
+:101E00008D899E890E9412152F89388D498D5A8D92
+:101E10000E9425160E9491169B01AC01C501B401D8
+:101E20000E9412152B8D3C8D4D8D5E8D0E942516C6
+:101E300060931E0270931F028093200290932102F0
+:101E40002B893C894D895E89C701B6010E94121514
+:101E50002F89388D498D5A8D0E9425160E94911622
+:101E60009B01AC01C501B4010E9412152B8D3C8D64
+:101E70004D8D5E8D0E9425164B015C0120E030E007
+:101E8000A9010E94AC1487FD57C020E030E040E873
+:101E90005FE3C501B4010E942918181634F4812C9F
+:101EA000912C30E8A32E3FE3B32EA5019401C70186
+:101EB000B6010E945E07C0922202D0922302E092F5
+:101EC0002402F092250280E090E0892B09F438CEBC
+:101ED0000E940802882309F433CE0E94000030CE0D
+:101EE00001E010E04CCE86E691E00E940A09C09025
+:101EF0002103D0902203E0902303F0902403B70144
+:101F0000A6018EE793E00E94EB06B701A6018AE3E3
+:101F100093E00E94EB0681E08093040186E791E064
+:101F20000E940A0959CEC701B6010E943E172DEC46
+:101F30003CEC4CEC5DE3AACE812C912C5401B5CF46
+:101F4000892B09F684E892E00E94FC011816190614
+:101F50000CF0F7C16FE371E0CE010D960E94A60A66
+:101F60000E94AE0197FD1EC08A309105D9F0898389
+:101F70001A8209891A890F5F1F4FB801CE010D9689
+:101F80000E94530A882361F32D853E8589899A89A9
+:101F9000BE016F5F7F4F820F931F0E94201B1A8B21
+:101FA000098BDECF62E871E0CE010D960E94190A1E
+:101FB000811162C067E871E0CE010D960E94190A96
+:101FC00081115AC063E971E0CE010D960E94190A91
+:101FD000811152C06FE971E0CE0101960E94A60AFC
+:101FE000BE016F5F7F4FCE010D960E94380A10E050
+:101FF000811127C069EA71E0CE0107960E94A60A06
+:1020000029893A894B855C852417350708F42DC347
+:102010008D859E85009709F428C36F8178856115A9
+:10202000710509F422C3241B350B820F931F0E94F4
+:10203000171B11E0892B09F410E0CE0107960E94CE
+:102040002E0ACE0101960E942E0A1123A9F08BEAD6
+:1020500091E00E94FD0849895A896D857E8584E852
+:1020600092E00E945E018DE391E00E94FD08CE01A6
+:102070000D960E942E0A27CF63E971E0CE010D96DE
+:102080000E94190A882361F085EB91E00E940A09F9
+:1020900088EC91E00E940A0989ED91E00E940A090A
+:1020A00067E871E0CE010D960E94190A882381F03D
+:1020B00081E08093040186E791E00E940A0986E7A7
+:1020C00091E00E940A0988EE91E00E940A09CFCFB0
+:1020D00062E871E0CE010D960E94190A882319F07A
+:1020E0000E941809C4CF6FE971E0CE0101960E94E9
+:1020F000A60ABE016F5F7F4FCE010D960E94380A7F
+:10210000182FCE0101960E942E0A112309F473C0E4
+:1021100009891A890115110509F46AC0ED84FE8444
+:1021200060E270E0C7010E940C1B009709F460C0D8
+:10213000AC014E195F094F3F540709F459C04F5F76
+:102140005F4F9801BE01635F7F4FCE0101960E94F1
+:10215000B70A89819A81009709F447C00E94371312
+:102160006B017C0120E030E040E85FE30E94291829
+:1021700087FD3BC020E030E048EC52E4C701B601E7
+:102180000E94AC1418168CF120E030E048EC52E4C8
+:10219000C701B6010E94961620E030E44CE955E4F0
+:1021A0000E9412150E9408170E94B3146B017C0153
+:1021B000C0922103D0922203E0922303F0922403E1
+:1021C000BC01A6018EE793E00E94EB06B701A601D1
+:1021D0008AE393E00E94EB068EEE91E00E940A09EA
+:1021E000CE0101960E942E0A42CF88EF91E0F6CFF1
+:1021F00086E092E06ACF8091780281119EC028E249
+:10220000222E22E0322E10E000E0D12CC12C69EA0F
+:1022100071E0CE0101960E94A60A89899A89081761
+:10222000190770F48D849E8469817A81C401800FBE
+:10223000911F0E94351B7C01E818F908892B19F4BD
+:10224000EE24EA94FE2CCE0101960E942E0AAFEFF6
+:10225000EA16FA0609F46AC09701A801BE01635F95
+:102260007F4FCE0107960E94B70A8B859C85892BEC
+:1022700061F08F8098846CE270E0C4010E940C1BB6
+:102280008C0108191909892B11F40FEF1FEF980120
+:1022900050E040E0BE01695F7F4FCE0101960E9491
+:1022A000B70A89819A81412C512C3201009721F083
+:1022B0000E9437132B013C01CE0101960E942E0A89
+:1022C0002B853C85A8014F5F5F4FBE01695F7F4F43
+:1022D000CE0101960E94B70A89819A81812C912CA6
+:1022E0005401009721F00E9437134B015C01CE018D
+:1022F00001960E942E0AF10140825182628273820D
+:1023000084829582A682B782BFEFCB1ADB0A87014F
+:102310000F5F1F4FCE0107960E942E0AE8E02E0E97
+:10232000311CFAE0CF16D10409F071CFD092270208
+:10233000C092260281E080937802CE010D960E9421
+:102340002E0A80917802882309F4BDCD809126025F
+:1023500090912702181619060CF0B5CD8091220233
+:1023600090912302A0912402B09125028B8B9C8B2B
+:10237000AD8BBE8B80911E0290911F02A091200216
+:10238000B09121028F8B988FA98FBA8F88E2682E27
+:1023900082E0782E512C412C8091260290912702C8
+:1023A000481659060CF056C180910401882309F49F
+:1023B000C4C0C0902802D0902902E0902A02F09078
+:1023C0002B022AEA37E24FE155E4C701B6010E9429
+:1023D00012150E94081760938E0370938F038093E9
+:1023E000900390939103609392037093930380936F
+:1023F0009403909395031092B2031092B30310923A
+:10240000B4031092B50310928103109282031092CC
+:1024100083031092840310929603109297031092F4
+:1024200098031092990360914A0370914B03809135
+:102430004C0390914D030E94B3144B015C0120E0CA
+:1024400030E04AE756E460911A0270911B028091D5
+:102450001C0290911D020E94121520E030E040E223
+:1024600051E40E9496169B01AC01C501B4010E9483
+:1024700024160E94081760934A0370934B038093BD
+:102480004C0390934D0360934E0370934F038093DE
+:1024900050039093510310926E0310926F031092A9
+:1024A00070031092710310923D0310923E0310923C
+:1024B0003F03109240031092520310925303109264
+:1024C000540310925503C0922202D0922302E0924C
+:1024D0002402F092250210921A0210921B0210920E
+:1024E0001C0210921D021092040120912C023091C6
+:1024F0002D0240912E0250912F02C701B6010E9479
+:102500005E07D3018D919D910D90BC91A02D8B8B79
+:102510009C8BAD8BBE8BD30114968D919D910D90AC
+:10252000BC91A02D8F8B988FA98FBA8FBFEF4B1ABC
+:102530005B0AE8E06E0E711C2FCF2B893C894D8918
+:102540005E89D3016D917D918D919C910E9424169D
+:102550006B8F7C8F8D8F9E8F2F89388D498D5A8DF3
+:10256000F30164817581868197810E9424166B0135
+:102570007C01AC019B010E9412154B015C012B8D6B
+:102580003C8D4D8D5E8DCA01B9010E9412159B01D3
+:10259000AC01C501B4010E9425160E94581820E024
+:1025A00030E0A9010E9496160E9408178B011616AA
+:1025B000170614F001E010E0312C212CC801012E87
+:1025C000000CAA0BBB0B8F8F98A3A9A3BAA3B101D0
+:1025D000032C000C880B990B0E94B3144B015C0177
+:1025E0006F8D78A189A19AA10E94B3149B01AC01BF
+:1025F000C501B4010E9496164B015C01AC019B0120
+:10260000C701B6010E9412152F89388D498D5A8D48
+:102610000E9425166BA37CA38DA39EA3A501940104
+:102620006B8D7C8D8D8D9E8D0E9412152B893C8922
+:102630004D895E890E9425162BA13CA14DA15EA16A
+:102640000E945E079FEF291A390A021513050CF044
+:10265000BECF57CF10927802109227021092260216
+:102660008DE491E00E940A092ECC11E0E6CC6627A9
+:1026700077270C943B13B0E0A0E0E1E4F3E10C9485
+:10268000E4155C017B016115710519F0DB018D9387
+:102690009C9385010F5F1F4FF501D0818D2F90E036
+:1026A0000E948B146C01892BB9F5DD32B9F50F5FEF
+:1026B0001F4FD5011196DC91C1E05801F1E0AF1A2E
+:1026C000B10843E050E06EE870E0C5010E94941448
+:1026D000892B69F5680182E0C80ED11C45E050E005
+:1026E00069E870E0C6010E949414892B21F4680106
+:1026F00097E0C90ED11CE114F10419F0D701CD9275
+:10270000DC9260E070E080E89FEFC111FFC060E004
+:1027100070E080E89FE7FAC05801BBCFDB3229F4B4
+:1027200085010E5F1F4FF501D181C0E0C6CF43E0A8
+:1027300050E066E870E0C5010E949414892BE9F02E
+:10274000F80110E000E020E030E0A9015F01B0ED09
+:102750008B2E8D0E89E08815C8F19C2E689491F817
+:102760008C2F8870C2FF16C0811102C00F5F1F4FEF
+:102770003196D501DC91C92DE9CFE114F10429F09E
+:102780000E5F1F4FF7011183008360E070E080EC63
+:102790009FE7BCC0882311F001501109A5E0B0E00B
+:1027A0000E94D3159B01AC01220F331F441F551FFC
+:1027B000280D311D411D511D283999E93907490757
+:1027C00099E15907A8F2C6609C2ED2CFAEEF8A12CB
+:1027D00006C0C3FD3CC09C2E689493F8C9CFDF7D32
+:1027E000D534A9F580818D3239F4C061DF011296AC
+:1027F000818162E070E006C0DF018B32C1F3119687
+:1028000061E070E080535D01A61AB70A8A30F8F4DF
+:10281000E0E8CE16ECE0DE065CF4B601660F771F4A
+:10282000660F771FC60ED71ECC0CDD1CC80ED11C40
+:102830005D01FFEFAF1ABF0A8C9180538A30A8F177
+:10284000C4FF03C0D194C194D1080C0D1D1DC1FF5C
+:1028500009C0E114F10431F081E0A81AB108D701F0
+:10286000AD92BC92CA01B9010E94B114C370C330C9
+:1028700009F490584B015C0120E030E0A9010E946E
+:10288000AC14882309F440C0CDEBD0E017FF05C09D
+:10289000119501951109C5EAD0E06E01B8E1CB1A96
+:1028A000D10880E2E82EF12C0FC0D501B1CFFE0196
+:1028B00025913591459154910E191F09C501B40117
+:1028C0000E9412154B015C01D501C4010E151F05B4
+:1028D00074F72497F594E794CC16DD06A9F78A2FB0
+:1028E000880F8B2F881F8F3F49F020E030E0A9012F
+:1028F000C501B4010E94AC14811106C082E290E0CF
+:102900009093C3038093C203C501B401CDB7DEB772
+:10291000ECE00C94001691110C947F15803219F0A4
+:1029200089508550C8F70895FB01DC0141505040A3
+:1029300088F08D9181341CF08B350CF4805E6591AC
+:1029400061341CF06B350CF4605E861B611171F311
+:10295000990B0895881BFCCF0E94EE1408F481E0C7
+:102960000895E89409C097FB3EF490958095709582
+:1029700061957F4F8F4F9F4F9923A9F0F92F96E9CB
+:10298000BB279395F695879577956795B795F11140
+:10299000F8CFFAF4BB0F11F460FF1BC06F5F7F4FDD
+:1029A0008F4F9F4F16C0882311F096E911C07723EF
+:1029B00021F09EE8872F762F05C0662371F096E8F8
+:1029C000862F70E060E02AF09A95660F771F881FC7
+:1029D000DAF7880F9695879597F90895990F00086B
+:1029E000550FAA0BE0E8FEEF16161706E807F907E1
+:1029F000C0F012161306E407F50798F0621B730B7C
+:102A0000840B950B39F40A2661F0232B242B252BFC
+:102A100021F408950A2609F4A140A6958FEF811D9F
+:102A2000811D08950E9425150C9499150E948B15FF
+:102A300038F00E94921520F0952311F00C94821525
+:102A40000C94881511240C94CD150E94AA1570F3CE
+:102A5000959FC1F3950F50E0551F629FF001729F43
+:102A6000BB27F00DB11D639FAA27F00DB11DAA1F52
+:102A7000649F6627B00DA11D661F829F2227B00D9F
+:102A8000A11D621F739FB00DA11D621F839FA00D2A
+:102A9000611D221F749F3327A00D611D231F849F7A
+:102AA000600D211D822F762F6A2F11249F575040D1
+:102AB0009AF0F1F088234AF0EE0FFF1FBB1F661F4C
+:102AC000771F881F91505040A9F79E3F510580F015
+:102AD0000C9482150C94CD155F3FE4F3983ED4F32B
+:102AE000869577956795B795F795E7959F5FC1F7B9
+:102AF000FE2B880F911D9695879597F90895992734
+:102B00008827089597F99F6780E870E060E008954E
+:102B10009FEF80EC089500240A94161617061806F5
+:102B20000906089500240A941216130614060506D1
+:102B30000895092E0394000C11F4882352F0BB0F62
+:102B400040F4BF2B11F460FF04C06F5F7F4F8F4FC5
+:102B50009F4F089557FD9058440F551F59F05F3F00
+:102B600071F04795880F97FB991F61F09F3F79F0AF
+:102B700087950895121613061406551FF2CF469531
+:102B8000F1DF08C0161617061806991FF1CF8695B3
+:102B90007105610508940895E894BB276627772797
+:102BA000CB0197F908950E941516A59F900DB49F2B
+:102BB000900DA49F800D911D112408952F923F9296
+:102BC0004F925F926F927F928F929F92AF92BF923D
+:102BD000CF92DF92EF92FF920F931F93CF93DF93E9
+:102BE000CDB7DEB7CA1BDB0B0FB6F894DEBF0FBE46
+:102BF000CDBF09942A88398848885F846E847D8493
+:102C00008C849B84AA84B984C884DF80EE80FD8094
+:102C10000C811B81AA81B981CE0FD11D0FB6F8940A
+:102C2000DEBF0FBECDBFED010895A29FB001B39FDF
+:102C3000C001A39F700D811D1124911DB29F700DC5
+:102C4000811D1124911D08955058BB27AA270E9469
+:102C50003C160C9499150E948B1538F00E94921521
+:102C600020F039F49F3F19F426F40C9488150EF4E3
+:102C7000E095E7FB0C948215E92F0E94AA1558F302
+:102C8000BA17620773078407950720F079F4A6F551
+:102C90000C94CC150EF4E0950B2EBA2FA02D0B0141
+:102CA000B90190010C01CA01A0011124FF27591B91
+:102CB00099F0593F50F4503E68F11A16F040A22F97
+:102CC000232F342F4427585FF3CF46953795279508
+:102CD000A795F0405395C9F77EF41F16BA0B620B07
+:102CE000730B840BBAF09150A1F0FF0FBB1F661F4E
+:102CF000771F881FC2F70EC0BA0F621F731F841F91
+:102D000048F4879577956795B795F7959E3F08F0B6
+:102D1000B0CF9395880F08F09927EE0F9795879578
+:102D200008950E94D417E3950C94FD170E94AA16EB
+:102D30000C9499150E94921558F00E948B1540F042
+:102D400029F45F3F29F00C94821551110C94CD1594
+:102D50000C9488150E94AA1568F39923B1F35523A2
+:102D600091F3951B550BBB27AA276217730784079E
+:102D700038F09F5F5F4F220F331F441FAA1FA9F334
+:102D800035D00E2E3AF0E0E832D091505040E69522
+:102D9000001CCAF72BD0FE2F29D0660F771F881F83
+:102DA000BB1F261737074807AB07B0E809F0BB0B76
+:102DB000802DBF01FF2793585F4F3AF09E3F51058A
+:102DC00078F00C9482150C94CD155F3FE4F3983E97
+:102DD000D4F3869577956795B795F7959F5FC9F773
+:102DE000880F911D9695879597F90895E1E0660FF4
+:102DF000771F881FBB1F621773078407BA0720F06D
+:102E0000621B730B840BBA0BEE1F88F7E0950895D5
+:102E10000E940F176894B1110C94CD1508950E946B
+:102E2000B21588F09F5798F0B92F9927B751B0F095
+:102E3000E1F0660F771F881F991F1AF0BA95C9F73E
+:102E400014C0B13091F00E94CC15B1E008950C94FB
+:102E5000CC15672F782F8827B85F39F0B93FCCF3AE
+:102E6000869577956795B395D9F73EF490958095BB
+:102E7000709561957F4F8F4F9F4F08950E94111855
+:102E800090F09F3748F4911116F00C94CD1560E046
+:102E900070E080E89FEB089526F41B16611D711DFC
+:102EA000811D0C94A9170C94C4170E948B1520F057
+:102EB00019F00E94921550F40C9488150C94CD15BD
+:102EC000E92F0E94AA1588F35523B1F3E7FB621797
+:102ED000730784079507A8F189F3E92FFF27882353
+:102EE0002AF03197660F771F881FDAF7952F55273D
+:102EF000442332F091505040220F331F441FD2F729
+:102F0000BB27E91BF50B621B730B840BB109B1F2F4
+:102F100022F4620F731F841FB11D31972AF0660FD0
+:102F2000771F881FBB1FEFCF9150504062F041F0D8
+:102F3000882332F0660F771F881F91505040C1F7E9
+:102F400093950C94C4178695779567959F5FD9F7ED
+:102F5000F7CF882371F4772321F09850872B762FB1
+:102F600007C0662311F499270DC09051862B70E09D
+:102F700060E02AF09A95660F771F881FDAF7880FAE
+:102F80009695879597F908959F3F31F0915020F4D9
+:102F9000879577956795B795880F911D9695879535
+:102FA00097F908950C9488150E94B215D8F3E89407
+:102FB000E0E0BB279F57F0F02AED3FE049EC06C068
+:102FC000EE0FBB0F661F771F881F28F0B23A62070B
+:102FD0007307840728F0B25A620B730B840BE395D6
+:102FE0009A9572F7803830F49A95BB0F661F771F59
+:102FF000881FD2F790480C94C617EF93E0FF07C0E4
+:10300000A2EA2AED3FE049EC5FEB0E943C160E94E9
+:1030100099150F90039401FC9058E8E6F0E00C94A9
+:103020009F180E94B215A0F0BEE7B91788F4BB271D
+:103030009F3860F41616B11D672F782F8827985F88
+:10304000F7CF869577956795B11D93959639C8F317
+:1030500008950E94EE1408F48FEF08950E94B215AF
+:10306000E8F09E37E8F09639B8F49E3848F4672FB8
+:10307000782F8827985FF9CF8695779567959395F0
+:103080009539D0F3B62FB1706B0F711D811D20F4EF
+:1030900087957795679593950C94A9170C94C41709
+:1030A0000C94CD1519F416F40C9488150C94C417CF
+:1030B0000E94B215B8F39923C9F3B6F39F57550B85
+:1030C00087FF0E9498180024A0E640EA90018058EB
+:1030D0005695979528F4805C660F771F881F20F01F
+:1030E00026173707480730F4621B730B840B20291F
+:1030F00031294A2BA69517940794202531254A2774
+:1031000058F7660F771F881F20F0261737074807E4
+:1031100030F4620B730B840B200D311D411DA09503
+:1031200081F7B901842F9158880F96958795089556
+:1031300091505040660F771F881FD2F708959F93D4
+:103140008F937F936F93FF93EF939B01AC010E944A
+:103150001215EF91FF910E94B3182F913F914F915B
+:103160005F910C941215DF93CF931F930F93FF92EF
+:10317000EF92DF927B018C01689406C0DA2EEF019A
+:103180000E942515FE01E894A59125913591459160
+:103190005591A6F3EF010E943C16FE019701A8018C
+:1031A000DA9469F7DF90EF90FF900F911F91CF9124
+:1031B000DF910895052E97FB1EF400940E94F118EC
+:1031C00057FD07D00E94FF1807FC03D04EF40C9463
+:1031D000F11850954095309521953F4F4F4F5F4FD7
+:1031E000089590958095709561957F4F8F4F9F4F73
+:1031F0000895EE0FFF1F0590F491E02D0994A1E2D0
+:103200001A2EAA1BBB1BFD010DC0AA1FBB1FEE1F60
+:10321000FF1FA217B307E407F50720F0A21BB30BAB
+:10322000E40BF50B661F771F881F991F1A9469F727
+:1032300060957095809590959B01AC01BD01CF0183
+:1032400008950F931F93CF93DF938230910510F46D
+:1032500082E090E0E091C603F091C70330E020E007
+:10326000B0E0A0E0309799F42115310509F44AC087
+:10327000281B390B24303105D8F58A819B816115D3
+:10328000710589F1FB0193838283FE0111C04081A6
+:1032900051810281138148175907E0F048175907F7
+:1032A00099F4109761F012960C93129713961C9351
+:1032B0003296CF01DF91CF911F910F910895009326
+:1032C000C6031093C703F4CF2115310551F04217FF
+:1032D000530738F0A901DB019A01BD01DF01F801B4
+:1032E000C1CFEF01F9CF9093C7038093C603CDCF31
+:1032F000FE01E20FF31F819391932250310939832C
+:103300002883D7CF2091C4033091C503232B41F4E8
+:1033100020910701309108013093C5032093C40325
+:1033200020910501309106012115310541F42DB799
+:103330003EB74091090150910A01241B350BE091E1
+:10334000C403F091C503E217F307A0F42E1B3F0B53
+:103350002817390778F0AC014E5F5F4F2417350707
+:1033600048F04E0F5F1F5093C5034093C4038193F1
+:1033700091939FCFF0E0E0E09CCFCF93DF93009755
+:10338000E9F0FC01329713821282A091C603B0913A
+:10339000C703ED0130E020E01097A1F420813181D6
+:1033A000820F931F2091C4033091C503281739075A
+:1033B00009F061C0F093C503E093C403DF91CF919E
+:1033C0000895EA01CE17DF07E8F54A815B819E0187
+:1033D00041155105B1F7E901FB83EA834991599100
+:1033E000C40FD51FEC17FD0761F48081918102960F
+:1033F000840F951FE90199838883828193819B8340
+:103400008A83F0E0E0E012968D919C9113970097EB
+:10341000B9F52D913C911197CD010296820F931F22
+:103420002091C4033091C5032817390739F6309726
+:1034300051F51092C7031092C603B093C503A09331
+:10344000C403BCCFD383C28340815181840F951FB5
+:10345000C817D90761F44E5F5F4F88819981480F83
+:10346000591F518340838A819B81938382832115D5
+:10347000310509F0B0CFF093C703E093C6039ECFA8
+:10348000FD01DC01C0CF13821282D7CFB0E0A0E0F3
+:10349000ECE4FAE10C94E0158C01009751F4CB01B7
+:1034A0000E9421198C01C801CDB7DEB7E0E10C9470
+:1034B000FC15FC01E60FF71F9C0122503109E217B1
+:1034C000F30708F49DC0D901CD91DC911197C6177F
+:1034D000D70798F0C530D10530F3CE010497861791
+:1034E000970708F3C61BD70B2297C193D1936D930F
+:1034F0007C93CF010E94BD19D6CF5B01AC1ABD0AE7
+:103500004C018C0E9D1EA091C603B091C703512C97
+:10351000412CF12CE12C109731F58091C40390914E
+:10352000C5038815990509F05CC04616570608F0D2
+:1035300058C08091050190910601009741F48DB724
+:103540009EB74091090150910A01841B950BE81721
+:10355000F90708F055C0F093C503E093C403F901DF
+:1035600071836083A0CF8D919C91119712966C907E
+:10357000129713967C901397A815B90559F56C010D
+:1035800042E0C40ED11CCA14DB0420F1AC014A197C
+:103590005B09DA011296159780F06282738251837B
+:1035A0004083D9016D937C93E114F10471F0D7014C
+:1035B0001396FC93EE93129776CF22968C0F9D1F55
+:1035C000F90191838083F301EFCFF093C703E09378
+:1035D000C60369CF4816590608F42C017D01D301B2
+:1035E0009ACFCB010E9421197C01009749F0AE01CE
+:1035F000B8010E94031BC8010E94BD19870153CF67
+:1036000010E000E050CFFB01DC0102C001900D9200
+:1036100041505040D8F70895FC018191861721F060
+:103620008823D9F7992708953197CF010895FB0191
+:10363000DC018D91019080190110D9F3990B089547
+:10364000FB01DC0101900D920020E1F70895FB01E0
+:10365000DC014150504030F08D910190801919F4F7
+:103660000020B9F7881B990B0895FB015191552350
+:10367000A9F0BF01DC014D9145174111E1F759F463
+:10368000CD010190002049F04D9140154111C9F341
+:10369000FB014111EFCF81E090E001970895F8948C
+:0236A000FFCF5A
+:1036A200CDCC3C40010000C8038000000000003E79
+:1036B200025E018B018B02FC019A01EE0100000007
+:1036C20000B3037F035D07360A920514055205FE17
+:1036D20004CC04AB04770456040004D1030D0A00A1
+:1036E200484F4D494E4700484F4D45440052005304
+:1036F2007069726F6772617068204D6F64652041F6
+:10370200637469766500417070204D6F6465204175
+:1037120063746976650054484554415F5245534588
+:103722005400484F4D450052455345545F54484557
+:103732005441004745545F56455253494F4E00533A
+:1037420045545F5350454544003B0049474E4F5254
+:1037520045443A20005461626C653A2044756E65B6
+:103762002057656176657200447269766572733AB4
+:1037720020445256383832350056657273696F6E7E
+:103782003A20312E342E30005245414459005350D4
+:103792004545445F53455400494E56414C49445FA8
+:1037A200535045454400494E56414C49445F434FAE
+:0637B2004D4D414E4400A4
+:00000001FF

+ 18 - 2
arduino_code_TMC2209/arduino_code_TMC2209.ino → firmware/arduino_code_TMC2209/arduino_code_TMC2209.ino

@@ -48,6 +48,10 @@ float userDefinedSpeed = maxSpeed; // Store user-defined speed
 // Running Mode
 int currentMode = MODE_APP; // Default mode is app mode.
 
+// FIRMWARE VERSION
+const char* firmwareVersion = "1.4.0";
+const char* motorType = "TMC2209";
+
 void setup()
 {
     // Set maximum speed and acceleration
@@ -72,6 +76,12 @@ void setup()
     homing();
 }
 
+void getVersion()
+{
+    Serial.println("Table: Dune Weaver");
+    Serial.println("Drivers: TMC2209");
+    Serial.println("Version: 1.4.0");
+}
 
 void resetTheta()
 {
@@ -107,6 +117,7 @@ void handleModeChange(int newMode) {
         Serial.println("Spirograph Mode Active");
         rotStepper.setMaxSpeed(userDefinedSpeed * 0.5); // Use 50% of user-defined speed
         inOutStepper.setMaxSpeed(userDefinedSpeed * 0.5);
+        isFirstCoordinates = false;
     } else if (newMode == MODE_APP) {
         Serial.println("App Mode Active");
         rotStepper.setMaxSpeed(userDefinedSpeed); // Restore user-defined speed
@@ -181,12 +192,17 @@ void appMode()
         String input = Serial.readStringUntil('\n');
 
         // Ignore invalid messages
-        if (input != "HOME" && input != "RESET_THETA" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
+        if (input != "HOME" && input != "RESET_THETA" && input != "GET_VERSION" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
         {
             Serial.print("IGNORED: ");
             Serial.println(input);
             return;
         }
+
+        if (input == "GET_VERSION") {
+            getVersion();
+        }
+
         if (input == "RESET_THETA")
         {
             resetTheta(); // Reset currentTheta
@@ -194,6 +210,7 @@ void appMode()
             Serial.println("READY");
             return;
         }
+
         if (input == "HOME")
         {
             homing();
@@ -222,7 +239,6 @@ void appMode()
                     inOutStepper.setMaxSpeed(newSpeed);
 
                     Serial.println("SPEED_SET");  
-                    Serial.println("R");
                 }
                 else
                 {

+ 894 - 0
firmware/arduino_code_TMC2209/arduino_code_TMC2209.ino.hex

@@ -0,0 +1,894 @@
+:100000000C948F000C94B7000C94B7000C94B700BC
+:100010000C94B7000C94B7000C94B7000C94B70084
+:100020000C94B7000C94B7000C94B7000C94B70074
+:100030000C94B7000C94B7000C94B7000C94B70064
+:100040000C94160B0C94B7000C94860B0C94600B5C
+:100050000C94B7000C94B7000C94B7000C94B70044
+:100060000C94B7000C94B70005A84CCDB2D44EB98F
+:100070003836A9020C50B9918688083CA6AAAA2A4B
+:10008000BE000000803F4E414E494E495459494EF2
+:1000900046CDCCCC3D0AD7233C17B7D13877CC2BF3
+:1000A000329595E6241FB14F0A000020410000C898
+:1000B0004200401C4620BCBE4CCA1B0E5AAEC59D19
+:1000C0007400000000230026002900000000002426
+:1000D0000027002A0000000000250028002B000453
+:1000E00004040404040404020202020202030303DF
+:1000F00003030301020408102040800102040810D9
+:100100002001020408102000000008000201000085
+:10011000030407000000000000000000B80B1124D9
+:100120001FBECFEFD8E0DEBFCDBF23E0A6E1B2E037
+:1001300001C01D92A83CB207E1F712E0A0E0B1E0D7
+:10014000E6EAF6E302C005900D92A631B107D9F7B1
+:1001500010E0CFE8D0E004C02197FE010E94FB1818
+:10016000CE38D107C9F70E94CA0C0C94511B0C94CD
+:100170000000833081F028F4813099F08230A9F0BA
+:1001800008958730A9F08830C9F08430B1F48091A7
+:1001900080008F7D03C0809180008F7780938000E6
+:1001A000089584B58F7784BD089584B58F7DFBCF86
+:1001B0008091B0008F778093B00008958091B00057
+:1001C0008F7DF9CF1F93CF93DF93282F30E0F90174
+:1001D000E95FFE4F8491F901ED50FF4FD491F90191
+:1001E000E152FF4FC491CC23A9F0162F81110E9438
+:1001F000B900EC2FF0E0EE0FFF1FEB52FF4FA5917F
+:10020000B4918FB7F894EC91111108C0D095DE230A
+:10021000DC938FBFDF91CF911F910895DE2BF8CF34
+:10022000CF93DF9390E0FC01ED50FF4F249181527A
+:100230009F4FFC0184918823C9F090E0880F991F9B
+:10024000FC01E553FF4FA591B491FC01EB52FF4F28
+:10025000C591D49161110DC09FB7F8948C912095F0
+:1002600082238C938881282328839FBFDF91CF919D
+:100270000895623051F49FB7F8943C91822F809595
+:1002800083238C93E8812E2BEFCF8FB7F894EC91DA
+:100290002E2B2C938FBFEACF8E50806480937C00EE
+:1002A00080917A00806480937A0080917A0086FD44
+:1002B000FCCF80917800909179000895AF92BF9221
+:1002C000CF92DF92EF92FF920F931F93CF93DF9322
+:1002D0006C017B018B01040F151FEB015E01AE1851
+:1002E000BF08C017D10759F06991D601ED91FC9173
+:1002F0000190F081E02DC6010995892B79F7C501A0
+:10030000DF91CF911F910F91FF90EF90DF90CF90F1
+:10031000BF90AF900895FC01538D448D252F30E0A0
+:10032000842F90E0821B930B541710F0CF96089502
+:1003300001970895FC01918D828D981761F0A28D2F
+:10034000AE0FBF2FB11D5D968C91928D9F5F9F73F5
+:10035000928F90E008958FEF9FEF08952FB7F89454
+:100360008091800290918102A0918202B0918302DB
+:100370002FBF80938C0290938D02A0938E02B09336
+:100380008F0284E892E00E949A0197FF26C02FB75F
+:10039000F8948091800290918102A0918202B091A4
+:1003A00083022FBF40918C0250918D0260918E028A
+:1003B00070918F02841B950BA60BB70B409188029E
+:1003C0005091890260918A0270918B02841795077F
+:1003D000A607B707B0F28FEF9FEF0895FC01918D4C
+:1003E000828D981731F0828DE80FF11D858D90E098
+:1003F00008958FEF9FEF0895FC01918D228D892F35
+:1004000090E0805C9F4F821B91098F73992708951C
+:1004100084E892E00E94FC0121E0892B09F420E0AD
+:10042000822F089580E090E0892B29F00E94080235
+:1004300081110C9400000895FC01A48DA80FB92F20
+:10044000B11DA35ABF4F2C91848D90E001968F73FC
+:100450009927848FA689B7892C93A089B1898C911B
+:10046000837080648C93938D848D981306C002886A
+:10047000F389E02D80818F7D80830895EF92FF9234
+:100480000F931F93CF93DF93EC0181E0888F9B8DB7
+:100490008C8D98131AC0E889F989808185FF15C071
+:1004A0009FB7F894EE89FF896083E889F989808194
+:1004B0008370806480839FBF81E090E0DF91CF9163
+:1004C0001F910F91FF90EF900895F62E0B8D10E085
+:1004D0000F5F1F4F0F731127E02E8C8D8E110CC0F4
+:1004E0000FB607FCFACFE889F989808185FFF5CF3F
+:1004F000CE010E941C02F1CFEB8DEC0FFD2FF11D00
+:10050000E35AFF4FF0829FB7F8940B8FEA89FB897B
+:1005100080818062CFCFCF93DF93EC01888D8823D9
+:10052000B9F0AA89BB89E889F9898C9185FD03C056
+:10053000808186FD0DC00FB607FCF7CF8C9185FF3B
+:10054000F2CF808185FFEDCFCE010E941C02E9CF62
+:10055000DF91CF9108954F925F926F927F92AF9209
+:10056000BF92CF92DF92EF92FF920F931F93CF93A0
+:10057000DF93EC015A018B014C8C5D8C6E8C7F8C6F
+:1005800073016201F7FAF094F7F8F0949A01AB0165
+:10059000C701B6010E942B18181664F09501A80136
+:1005A000C301B2010E94AE147301620187FD02C053
+:1005B0006501780120E030E0A901C701B6010E9481
+:1005C000AE14811117C01B821C821D821E82C88E30
+:1005D000D98EEA8EFB8EDF91CF911F910F91FF9004
+:1005E000EF90DF90CF90BF90AF907F906F905F9033
+:1005F0004F900895A701960160E074E284E799E4C2
+:100600000E9498169F770E9411176B837C838D83BD
+:100610009E8311E020E030E0A901C701B6010E94ED
+:100620002B1818160CF010E01A83D1CFCF92DF925E
+:10063000EF92FF92CF93DF93EC013FB7F894809154
+:100640007B0290917C02A0917D02B0917E0226B542
+:10065000A89B05C02F3F19F00196A11DB11D3FBFFA
+:10066000BA2FA92F982F88276C017D01C20ED11CAB
+:10067000E11CF11C42E0CC0CDD1CEE1CFF1C4A9579
+:10068000D1F788A599A5AAA5BBA5B701A601481BC6
+:10069000590B6A0B7B0B8B819C81AD81BE81481706
+:1006A00059076A077B0748F188899989AA89BB8914
+:1006B0002A812223F1F00196A11DB11D888B998B0F
+:1006C000AA8BBB8B488959896A897B89E881F98122
+:1006D0000484F585E02DCE010995C8A6D9A6EAA621
+:1006E000FBA681E0DF91CF91FF90EF90DF90CF905C
+:1006F00008950197A109B109E1CF80E0F3CFCF932D
+:10070000DF93FC012781222359F1EC0161E0808510
+:100710000E94100161E089850E9410018F81843060
+:1007200011F08830B1F461E08A850E94100161E027
+:100730008B850E9410018FA58F3F91F061E00E9490
+:1007400010016EA581E068278FA5DF91CF910C94F1
+:10075000E200833011F0863071F761E08A85E9CFDD
+:10076000DF91CF910895CF93DF93FC01278122235E
+:10077000A9F0EC010190F081E02D0284F385E02DD9
+:1007800060E009958FA58F3F49F061E00E9410015C
+:100790006EA58FA5DF91CF910C94E200DF91CF91F0
+:1007A0000895DC01ED91FC91228533854770552732
+:1007B0006627772741505109610971094730510572
+:1007C0006105710560F4FA01E851FC4F0C94FB18C7
+:1007D000F203F403F603F803FA03FC03FE0361E0FB
+:1007E000F901099465E0FCCF64E0FACF66E0F8CF48
+:1007F00062E0F6CF6AE0F4CF68E0F2CF69E0F0CFD4
+:10080000CF93DF93EC01CB01BA0126E030E040E06A
+:1008100050E00E94DC18623071058105910589F174
+:100820006CF46115710581059105D1F06130710598
+:1008300081059105F9F0DF91CF910895643071053C
+:100840008105910561F124F16530710581059105FE
+:1008500091F7E881F9810284F385E02D66E006C016
+:10086000E881F9810284F385E02D64E0CE01DF9117
+:10087000CF910994E881F9810284F385E02D65E048
+:10088000F5CFE881F9810284F385E02D61E0EECFB8
+:10089000E881F9810284F385E02D63E0E7CFE88108
+:1008A000F9810284F385E02D62E0E0CFDC01ED9177
+:1008B000FC910284F385E02D437055276627772746
+:1008C000423051056105710571F0433051056105F4
+:1008D000710559F0413051056105710511F065E070
+:1008E000099466E0FDCF6AE0FBCF69E0F9CFCF93D2
+:1008F000DF93EC01CB01BA0123E030E040E050E0AF
+:100900000E94DC18613071058105910599F0623013
+:10091000710581059105A9F0672B682B692BC1F43E
+:10092000E881F9810284F385E02D64E0CE01DF9156
+:10093000CF910994E881F9810284F385E02D61E08B
+:10094000F5CFE881F9810284F385E02D62E0EECFF6
+:10095000DF91CF910895DC01ED91FC910284F38544
+:10096000E02D4370552766277727423051056105F2
+:10097000710571F0433051056105710559F0413041
+:1009800051056105710511F062E0099463E0FDCF46
+:1009900061E0FBCF60E0F9CFCF93DF93EC01E8811A
+:1009A000F9810284F385E02D8A8160E0811162E0A3
+:1009B000CE010995E881F9810284F385E02D8A81D1
+:1009C00061E0811163E0CE0109958CA59DA582307F
+:1009D000910538F0880F991F880F991F0597019787
+:1009E000F1F7E881F9810284F385E02D8A8160E0E6
+:1009F000811162E0CE01DF91CF910994CF93DF9313
+:100A0000EC0120E030E0A901688D798D8A8D9B8D05
+:100A10000E942B1818162CF4E8A9F9A9DF91CF91A0
+:100A20000994EAA9FBA9FACFCF92DF92EF92FF9245
+:100A30000F931F93CF93DF93DC011796CC91C430B3
+:100A400039F0C83059F1C33019F0C63049F1C2E06D
+:100A50008C01085F1F4FF12CE12CC62ED12CD1E068
+:100A6000F8016481C6010E2C02C0959587950A9401
+:100A7000E2F780FD6D270F5F1F4F80810E94E2002B
+:100A8000BFEFEB1AFB0AEC1658F3DF91CF911F91E1
+:100A90000F91FF90EF90DF90CF900895C4E0D8CFF2
+:100AA000C3E0D6CFDC011796EC911797E93008F038
+:100AB00038C0F0E0E25AFA4F0C94FB1867056D0558
+:100AC000730579057F059105850591058B05ED91E8
+:100AD000FC910684F785E02D0994ED91FC91008846
+:100AE000F189E02DF9CFED91FC910288F389E02D99
+:100AF000F3CFED91FC910488F589E02DEDCFED91D8
+:100B0000FC910688F789E02DE7CFED91FC91008CF0
+:100B1000F18DE02DE1CFED91FC91028CF38DE02D74
+:100B2000DBCF08954F925F926F927F928F929F9248
+:100B3000AF92BF92CF92DF92EF92FF920F931F93EB
+:100B4000CF93DF93EC01CC88DD88EE88FF8888891D
+:100B50009989AA89BB89C81AD90AEA0AFB0A688D49
+:100B6000798D8A8D9B8D9B01AC010E9414154B01E0
+:100B70005C0168A179A18AA19BA19B01AC010E94A3
+:100B800027169B01AC01C501B4010E9498160E9472
+:100B90000A17C114D104E104F10409F0B6C06230AF
+:100BA0007105810591050CF0D0C01B821C821D824D
+:100BB0001E82188E198E1A8E1B8E1CAA1DAA1EAAA2
+:100BC0001FAAC12CD12C7601C701B601DF91CF91AC
+:100BD0001F910F91FF90EF90DF90CF90BF90AF905B
+:100BE0009F908F907F906F905F904F900895101618
+:100BF000110612061306B4F46C157D058E059F05CB
+:100C00001CF42A812111A1C09B01AC0188279927DE
+:100C1000DC01821B930BA40BB50B8CAB9DABAEAB75
+:100C2000BFAB93C0011511052105310509F48DC035
+:100C30006C157D058E059F050CF087C08A81882381
+:100C400009F483C030952095109501951F4F2F4FC3
+:100C50003F4F0CAB1DAB2EAB3FAB77C00115110561
+:100C60002105310509F471C08824992454018C1898
+:100C70009D08AE08BF08681579058A059B050CF02C
+:100C800064C08A81811161C0DDCFCCACDDACEEAC3B
+:100C9000FFACA7019601C701B6010E9427162B01E0
+:100CA0003C01C501B4010E94B51420E030E040E8E9
+:100CB00050E40E94141520E030E040E85FE30E9419
+:100CC00027169B01AC01C301B2010E9498169B013B
+:100CD000AC01C701B6010E9426163B016C01FE0162
+:100CE000E05CFF4FE080F180028113819701A80151
+:100CF0000E942B18181614F473018601C701D8013D
+:100D00008CAF9DAFAEAFBFAF3AC00CA91DA92EA945
+:100D10003FA91C141D041E041F040CF468CF1016F8
+:100D20001106120613060CF099CF0027112798011F
+:100D30000C191D092E093F096017710782079307D7
+:100D40000CF062CF2A8121115FCF8CA89DA8AEA89C
+:100D5000BFA881149104A104B10409F096CF88AD15
+:100D600099ADAAADBBAD8CAF9DAFAEAFBFAF81E0CB
+:100D70001C141D041E041F040CF080E08A833FEF46
+:100D8000831A930AA30AB30A8CAA9DAAAEAABFAA81
+:100D90008CAC9DACAEACBFACC501B4010E941117C8
+:100DA0006B017C01CB82DC82ED82FE82A501940185
+:100DB00060E074E284E799E40E949816688F798F66
+:100DC0008A8F9B8F2A812111FFCE9058688F798F4F
+:100DD0008A8F9B8FF9CECF92DF92EF92FF920F9383
+:100DE0001F93CF93DF93EC016A017B0120E030E099
+:100DF000A901CB01B6010E94AE1487FF04C0F7FA27
+:100E0000F094F7F8F094A70196016C8D7D8D8E8D8E
+:100E10009F8D0E94AE14882309F446C0CC8EDD8ECF
+:100E2000EE8EFF8E8E01005C1F4FA701960160E0E1
+:100E300074E284E799E40E949816F8016083718354
+:100E4000828393838CA99DA9AEA9BFA91816190600
+:100E50001A061B064CF5688D798D8A8D9B8D9B013A
+:100E6000AC010E9414156B017C0168A179A18AA1D3
+:100E70009BA19B01AC010E9427169B01AC01C701FD
+:100E8000B6010E9498160E940A176CAB7DAB8EAB20
+:100E90009FABCE01DF91CF911F910F91FF90EF900B
+:100EA000DF90CF900C949205DF91CF911F910F911D
+:100EB000FF90EF90DF90CF90089508952F923F928A
+:100EC0004F925F926F927F928F929F92AF92BF925A
+:100ED000CF92DF92EF92FF920F931F93CF93DF9306
+:100EE000CDB7DEB762970FB6F894DEBF0FBECDBFA9
+:100EF0006B017C012D873E874F87588B2AEA37E2AA
+:100F00004FE155E40E9414150E940A172B013C0181
+:100F100020E030E044EB55E46D857E858F8598892F
+:100F20000E9414150E940A174B015C0120912202B5
+:100F3000309123024091240250912502C701B6014D
+:100F40000E9426162BED3FE049EC50E40E949816D3
+:100F50009B01AC0160911A0270911B0280911C02EE
+:100F600090911D020E94271660931A0270931B0233
+:100F700080931C0290931D028091040181111AC07C
+:100F80002BED3FE049EC50E4C701B6010E949816F2
+:100F900020E030E04AE756E40E94141520E030E0FB
+:100FA00040E251E40E9498160E940A17861A970A96
+:100FB000A80AB90A49825A826B827C828D829E82FB
+:100FC000AF82B886209039039E012F5F3F4F2901E1
+:100FD00045E253E05A874987912C312C00E010E01C
+:100FE000812C2914C9F1D2016D917D918D919D9132
+:100FF0002D01E985FA85A190B190FA87E987D5019D
+:1010000050962D913D914D915C915397621B730BBE
+:10101000840B950B97FF07C090958095709561950F
+:101020007F4F8F4F9F4F0E94B514F501248D358D52
+:10103000468D578D0E9498163B015C01232D302F61
+:10104000412F582D0E942B18181624F4362C072DEA
+:101050001A2D8B2C9394C5CF20E030E0A901632D8D
+:10106000702F812F982D0E942B1818160CF06CC031
+:10107000912C80913903981608F066C0892D90E074
+:10108000FC01EE0FFF1FEE0FFF1F21E030E02C0FE1
+:101090003D1FE20FF31F4080518062807380AC01DE
+:1010A000440F551F5A8B498BFA01EB5DFC4FA08012
+:1010B000B180F50180899189A289B389A301920148
+:1010C000281B390B4A0B5B0BCA01B9010E94B514EE
+:1010D000232D302F412F582D0E94981669877A872B
+:1010E0008B879C87F50184899589A689B7894816DD
+:1010F00059066A067B0661F0448A558A668A778AB1
+:101100000190F081E02D0084F185E02DC501099565
+:10111000E989FA89EB5DFC4FA080B180F501208D53
+:10112000318D428D538D69857A858B859C850E9492
+:10113000AE14882339F049855A856B857C85C501B5
+:101140000E94AB02939495CF80E010E09091390318
+:10115000191720F5E12FF0E0EE0FFF1FEB5DFC4FBC
+:101160000190F081E02D84889588A688B788408911
+:1011700051896289738984169506A606B70661F0BF
+:1011800083819481A581B681892B8A2B8B2B19F0C1
+:10119000CF010E94160381E01F5FD8CF8111D4CF09
+:1011A000C0922202D0922302E0922402F092250201
+:1011B0002D853E854F85588920931E0230931F024E
+:1011C000409320025093210262960FB6F894DEBF3E
+:1011D0000FBECDBFDF91CF911F910F91FF90EF9088
+:1011E000DF90CF90BF90AF909F908F907F906F9047
+:1011F0005F904F903F902F900895FC010190002048
+:10120000E9F73197AF01481B590BBC0184E892E024
+:101210000C945E01CF93DF930E94FD08EC018DE3F7
+:1012200091E00E94FD088C0F9D1FDF91CF910895E2
+:1012300080E491E00E940A0920E030E04AE754ECA3
+:101240006091520370915303809154039091550320
+:101250000E94AE14882341F040E050E06AE774EC4D
+:101260008AE393E00E94AB0280913D0390913E039C
+:10127000A0913F03B0914003892B8A2B8B2B21F047
+:101280008AE393E00E94160360914A0370914B0336
+:1012900080914C0390914D030E94B51420E030E002
+:1012A00046EC55EC0E94AE141816F4F210924A0364
+:1012B00010924B0310924C0310924D0310924E0368
+:1012C00010924F03109250031092510310926E032C
+:1012D00010926F03109270031092710310923D03ED
+:1012E00010923E0310923F0310924003109252035B
+:1012F000109253031092540310925503109222023D
+:1013000010922302109224021092250210921E02C3
+:1013100010921F02109220021092210287E491E0A5
+:101320000C940A094F925F926F927F928F929F92D4
+:10133000AF92BF92CF92DF92EF92FF92CF93DF9363
+:10134000EC016A017B0120E030E0A901CB01B6018C
+:101350000E94AE1487FF04C0F7FAF094F7F8F094F7
+:1013600088A099A0AAA0BBA0A7019601C501B401BD
+:101370000E94AE14882309F44DC06CA97DA98EA9E2
+:101380009FA90E94B5142B013C01A7019601C5013C
+:10139000B4010E9498169B01AC01C301B2010E94E6
+:1013A00014150E940A176CAB7DAB8EAB9FABA701E7
+:1013B000960160E070E080E090E40E9498160E9440
+:1013C0005A1826E53EE04DE25FE30E94141520E046
+:1013D00034E244E759E40E94141568AF79AF8AAF4C
+:1013E0009BAFC8A2D9A2EAA2FBA2E881F98100843E
+:1013F000F185E02DCE01DF91CF91FF90EF90DF904E
+:10140000CF90BF90AF909F908F907F906F905F90A4
+:101410004F900994DF91CF91FF90EF90DF90CF90A4
+:10142000BF90AF909F908F907F906F905F904F9004
+:101430000895FC0124813581232B39F421E0FB013F
+:101440008081882349F020E007C0808191810E943B
+:10145000191B21E0892BB9F7822F0895FC018081A7
+:101460009181009711F00C94BF1908950C94BF1945
+:10147000FB0144815581FC01248135812417350706
+:1014800078F080819181009759F0FB016081718132
+:101490006115710529F00E94291B21E0892B09F0B3
+:1014A00020E0822F08950F931F93CF93DF93EC01D9
+:1014B00088819981009759F02A813B812617370747
+:1014C00030F081E0DF91CF911F910F9108958B0152
+:1014D0006F5F7F4F0E94481A009759F09983888365
+:1014E0001B830A832C813D81232B59F7FC01108239
+:1014F000E8CF80E0E7CFEF92FF920F931F93CF9357
+:10150000DF93EC017B018A01BA010E94530A288112
+:101510003981811114C02115310519F0C9010E94CA
+:10152000BF19198218821D821C821B821A82CE0169
+:10153000DF91CF911F910F91FF90EF9008951D8340
+:101540000C83B701C9010E94221BF1CFFC0111825B
+:1015500010821382128215821482FB0101900020F6
+:10156000E9F73197AF01461B570B0C947B0AAF92FA
+:10157000BF92CF92DF92EF92FF920F931F93CF9380
+:10158000DF93EC016B015A0179012417350720F430
+:101590008B2D5901E42EF82E6FE371E0CE010E94ED
+:1015A000A60AD60114960D911C91A016B10628F535
+:1015B000E016F10608F48701D601ED91FC91119730
+:1015C000E00FF11FF08010826D917C916A0D7B1D00
+:1015D00061157105F1F0FB0101900020E9F73197E9
+:1015E000AF01461B570BCE010E947B0AF60180819A
+:1015F0009181080F191FD801FC92CE01DF91CF9184
+:101600001F910F91FF90EF90DF90CF90BF90AF9020
+:10161000089588819981009711F00E94BF1919825D
+:1016200018821D821C821B821A82E0CF1F920F92A9
+:101630000FB60F9211242F933F938F939F93AF93E5
+:10164000BF938091800290918102A0918202B0911B
+:10165000830230917F0223E0230F2D3758F5019646
+:10166000A11DB11D20937F0280938002909381027F
+:10167000A0938202B093830280917B0290917C02BE
+:10168000A0917D02B0917E020196A11DB11D8093B3
+:101690007B0290937C02A0937D02B0937E02BF9167
+:1016A000AF919F918F913F912F910F900FBE0F900F
+:1016B0001F90189526E8230F0296A11DB11DD2CFC9
+:1016C0001F920F920FB60F9211242F933F934F93B7
+:1016D0005F936F937F938F939F93AF93BF93EF939A
+:1016E000FF9384E892E00E941C02FF91EF91BF916A
+:1016F000AF919F918F917F916F915F914F913F91AA
+:101700002F910F900FBE0F901F9018951F920F9260
+:101710000FB60F9211242F938F939F93EF93FF9304
+:10172000E0919402F09195028081E0919A02F0910B
+:101730009B0282FD1BC0908180919D028F5F8F7301
+:1017400020919E02821741F0E0919D02F0E0EC575B
+:10175000FD4F958F80939D02FF91EF919F918F9107
+:101760002F910F900FBE0F901F9018958081F4CF8E
+:101770008F929F92AF92BF92CF92DF92EF92FF92A1
+:101780000F931F93CF93DF93E4E8F2E0138212826A
+:1017900088EE93E0A0E0B0E084839583A683B783CE
+:1017A0008FE091E09183808385EC90E0958784873A
+:1017B00084EC90E09787868780EC90E0918B808B1B
+:1017C00081EC90E0938B828B82EC90E0958B848B04
+:1017D00086EC90E0978B868B118E128E138E148E72
+:1017E000EEE7F3E001E211E01183008388248394A3
+:1017F0008782108A118A128A138A148A158A168A95
+:10180000178A108E118E128E138E148E158E168ED0
+:10181000178E10A211A212A213A2C12CD12C80E803
+:10182000E82E8FE3F82EC4A2D5A2E6A2F7A2138277
+:10183000148215821682C1E0D0E0D5A7C4A79924EE
+:101840009A9497A610A611A612A613A682E08087E6
+:1018500095E0B92EB18624E0A22EA286B38616A604
+:1018600014AA15AA16AA17AA10AE11AE12AE13AE7C
+:1018700014AE15AE16AE17AEC092BE03D092BF0323
+:10188000E092C003F092C103128214861586168678
+:101890001786CF010E947F03B701A6018EE793E070
+:1018A0000E949209B701A6018EE793E00E94EB0621
+:1018B000EAE3F3E0118300838782108A118A128A97
+:1018C000138A148A158A168A178A108E118E128E20
+:1018D000138E148E158E168E178E10A211A212A2C0
+:1018E00013A2C4A2D5A2E6A2F7A213821482158283
+:1018F0001682D5A7C4A797A610A611A612A613A64E
+:1019000083E0808786E08187A286B38616A614AA24
+:1019100015AA16AA17AA10AE11AE12AE13AE14AEC7
+:1019200015AE16AE17AEC0927A03D0927B03E0924A
+:101930007C03F0927D031282148615861686178624
+:10194000CF010E947F03B701A6018AE393E00E94C2
+:101950009209B701A6018AE393E00E94EB06109278
+:10196000390380E090E0AAE7B4E4809321039093E8
+:101970002203A0932303B0932403DF91CF911F91FF
+:101980000F91FF90EF90DF90CF90BF90AF909F901E
+:101990008F900895CF93DF93CDB7DEB7A6970FB69C
+:1019A000F894DEBF0FBECDBF789484B5826084BD4D
+:1019B00084B5816084BD85B5826085BD85B5816053
+:1019C00085BD80916E00816080936E0010928100D1
+:1019D000809181008260809381008091810081608C
+:1019E000809381008091800081608093800080914D
+:1019F000B10084608093B1008091B00081608093D9
+:101A0000B00080917A00846080937A0080917A009F
+:101A1000826080937A0080917A00816080937A005E
+:101A200080917A00806880937A001092C10040E033
+:101A300050E06AE774E48EE793E00E94EB0640E032
+:101A400050E068E472E48EE793E00E94920940E07F
+:101A500050E06AE774E48AE393E00E94EB0640E01A
+:101A600050E068E472E48AE393E00E949209E09116
+:101A70003903EA3068F481E08E0F80933903F0E097
+:101A8000EE0FFF1FEB5DFC4F8EE793E091838083A9
+:101A9000E0913903EA3068F481E08E0F80933903D6
+:101AA000F0E0EE0FFF1FEB5DFC4F8AE393E09183C4
+:101AB000808362E08BE00E94100160E08EE00E9473
+:101AC000100160E08FE00E941001E0919402F0911B
+:101AD000950282E08083E0919002F0919102108261
+:101AE000E0919202F091930280E1808310929C0237
+:101AF000E0919802F091990286E08083E09196024D
+:101B0000F0919702808180618083E0919602F0914C
+:101B10009702808188608083E0919602F09197021D
+:101B2000808180688083E0919602F09197028081A5
+:101B30008F7D80838DE491E00E940A090E9418093C
+:101B4000E2E1F1E08491EEEFF0E00491EAEEF0E002
+:101B50001491112309F4C4C181110E94B900E12F2D
+:101B6000F0E0EE0FFF1FEF53FF4FA591B4918C9162
+:101B7000082391E080E009F090E0092F182F809170
+:101B8000790290917A0280179107B9F1013011051D
+:101B900009F0A9C18FE491E00E940A0920E030E039
+:101BA00040E05FE360912103709122038091230361
+:101BB000909124030E9414156B017C01BC01A601C5
+:101BC0008EE793E00E94EB06B701A6018AE393E05B
+:101BD0000E94EB0610920401609122027091230290
+:101BE000809124029091250220E030E0A9010E941A
+:101BF0005E0710937A0200937902809179029091A6
+:101C00007A028130910509F09BC18FE00E944C015E
+:101C1000BC01990F880B990B0E94B51420E030E0AD
+:101C200040EB50E40E94141520E030EC4FE754E400
+:101C30000E94981620E030E040E05FE30E94271603
+:101C400020E030E040E251E40E9414150E94301878
+:101C500020E030E040E251E40E9498166B017C01E4
+:101C600020E030E040E85FE30E94621720E030E0CF
+:101C700040E05FE30E942B1887FD55C1C701B60104
+:101C80000E94401723E333E343E75FE30E942716F4
+:101C90006B017C018090000190900101A0900201F5
+:101CA000B0900301409022025090230260902402E1
+:101CB00070902502AC019B01C501B4010E94AE14D5
+:101CC000882331F1A7019601C501B4010E942616AF
+:101CD000A30192010E9414159B01AC0160911602B0
+:101CE0007091170280911802909119020E94271694
+:101CF0006093160270931702809318029093190252
+:101D0000C0920001D0920101E0920201F092030121
+:101D10008EE00E944C01BC01990F880B990B0E9428
+:101D2000B51420E030E040E05FE30E94141520E0AD
+:101D300030EC4FE754E40E94981620E030E0A9010F
+:101D40000E94271620E030E040EA51E40E9414157A
+:101D50000E94301820E030E040EA51E40E949816DA
+:101D60006B017C01AC019B0160E070E080E89FE3C7
+:101D70000E94261620E030E040E05FE30E94141548
+:101D80004B015C01AC019B01C701B6010E94271603
+:101D90006B8F7C8F8D8F9E8F2CE739ED40E25DE35A
+:101DA000C301B2010E9427166B017C012BED3FE0BD
+:101DB00049EC50E40E94981660931A0270931B023B
+:101DC00080931C0290931D0280910001909101016B
+:101DD000A0910201B09103018B8B9C8BAD8BBE8BCC
+:101DE0008091160290911702A0911802B0911902E9
+:101DF0008F8B988FA98FBA8FA30192016B897C89F1
+:101E00008D899E890E9414152F89388D498D5A8D90
+:101E10000E9427160E9493169B01AC01C501B401D4
+:101E20000E9414152B8D3C8D4D8D5E8D0E942716C2
+:101E300060931E0270931F028093200290932102F0
+:101E40002B893C894D895E89C701B6010E94141512
+:101E50002F89388D498D5A8D0E9427160E9493161E
+:101E60009B01AC01C501B4010E9414152B8D3C8D62
+:101E70004D8D5E8D0E9427164B015C0120E030E005
+:101E8000A9010E94AE1487FD57C020E030E040E871
+:101E90005FE3C501B4010E942B18181634F4812C9D
+:101EA000912C30E8A32E3FE3B32EA5019401C70186
+:101EB000B6010E945E07C0922202D0922302E092F5
+:101EC0002402F092250280E090E0892B09F438CEBC
+:101ED0000E940802882309F433CE0E94000030CE0D
+:101EE00001E010E04CCE86E691E00E940A09C09025
+:101EF0002103D0902203E0902303F0902403B70144
+:101F0000A6018EE793E00E94EB06B701A6018AE3E3
+:101F100093E00E94EB0681E08093040186E791E064
+:101F20000E940A0959CEC701B6010E9440172DEC44
+:101F30003CEC4CEC5DE3AACE812C912C5401B5CF46
+:101F4000892B09F684E892E00E94FC011816190614
+:101F50000CF0F7C16FE371E0CE010D960E94A60A66
+:101F60000E94AE0197FD1EC08A309105D9F0898389
+:101F70001A8209891A890F5F1F4FB801CE010D9689
+:101F80000E94530A882361F32D853E8589899A89A9
+:101F9000BE016F5F7F4F820F931F0E94221B1A8B1F
+:101FA000098BDECF62E871E0CE010D960E94190A1E
+:101FB000811162C067E871E0CE010D960E94190A96
+:101FC00081115AC063E971E0CE010D960E94190A91
+:101FD000811152C06FE971E0CE0101960E94A60AFC
+:101FE000BE016F5F7F4FCE010D960E94380A10E050
+:101FF000811127C069EA71E0CE0107960E94A60A06
+:1020000029893A894B855C852417350708F42FC345
+:102010008D859E85009709F42AC36F8178856115A7
+:10202000710509F424C3241B350B820F931F0E94F2
+:10203000191B11E0892B09F410E0CE0107960E94CC
+:102040002E0ACE0101960E942E0A1123A9F08BEAD6
+:1020500091E00E94FD0849895A896D857E8584E852
+:1020600092E00E945E018DE391E00E94FD08CE01A6
+:102070000D960E942E0A27CF63E971E0CE010D96DE
+:102080000E94190A882361F085EB91E00E940A09F9
+:1020900088EC91E00E940A0989ED91E00E940A090A
+:1020A00067E871E0CE010D960E94190A882381F03D
+:1020B00081E08093040186E791E00E940A0986E7A7
+:1020C00091E00E940A0988EE91E00E940A09CFCFB0
+:1020D00062E871E0CE010D960E94190A882319F07A
+:1020E0000E941809C4CF6FE971E0CE0101960E94E9
+:1020F000A60ABE016F5F7F4FCE010D960E94380A7F
+:10210000182FCE0101960E942E0A112309F473C0E4
+:1021100009891A890115110509F46AC0ED84FE8444
+:1021200060E270E0C7010E940E1B009709F460C0D6
+:10213000AC014E195F094F3F540709F459C04F5F76
+:102140005F4F9801BE01635F7F4FCE0101960E94F1
+:10215000B70A89819A81009709F447C00E94391310
+:102160006B017C0120E030E040E85FE30E942B1827
+:1021700087FD3BC020E030E048EC52E4C701B601E7
+:102180000E94AE1418168CF120E030E048EC52E4C6
+:10219000C701B6010E94981620E030E04AE754E4F7
+:1021A0000E9414150E940A170E94B5146B017C014D
+:1021B000C0922103D0922203E0922303F0922403E1
+:1021C000BC01A6018EE793E00E94EB06B701A601D1
+:1021D0008AE393E00E94EB068EEE91E00E940A09EA
+:1021E000CE0101960E942E0A42CF88EF91E0F6CFF1
+:1021F00086E092E06ACF8091780281119EC028E249
+:10220000222E22E0322E10E000E0D12CC12C69EA0F
+:1022100071E0CE0101960E94A60A89899A89081761
+:10222000190770F48D849E8469817A81C401800FBE
+:10223000911F0E94371B7C01E818F908892B19F4BB
+:10224000EE24EA94FE2CCE0101960E942E0AAFEFF6
+:10225000EA16FA0609F46AC09701A801BE01635F95
+:102260007F4FCE0107960E94B70A8B859C85892BEC
+:1022700061F08F8098846CE270E0C4010E940E1BB4
+:102280008C0108191909892B11F40FEF1FEF980120
+:1022900050E040E0BE01695F7F4FCE0101960E9491
+:1022A000B70A89819A81412C512C3201009721F083
+:1022B0000E9439132B013C01CE0101960E942E0A87
+:1022C0002B853C85A8014F5F5F4FBE01695F7F4F43
+:1022D000CE0101960E94B70A89819A81812C912CA6
+:1022E0005401009721F00E9439134B015C01CE018B
+:1022F00001960E942E0AF10140825182628273820D
+:1023000084829582A682B782BFEFCB1ADB0A87014F
+:102310000F5F1F4FCE0107960E942E0AE8E02E0E97
+:10232000311CFAE0CF16D10409F071CFD092270208
+:10233000C092260281E080937802CE010D960E9421
+:102340002E0A80917802882309F4BDCD809126025F
+:1023500090912702181619060CF0B5CD8091220233
+:1023600090912302A0912402B09125028B8B9C8B2B
+:10237000AD8BBE8B80911E0290911F02A091200216
+:10238000B09121028F8B988FA98FBA8F88E2682E27
+:1023900082E0782E512C412C8091260290912702C8
+:1023A000481659060CF058C180910401882309F49D
+:1023B000C4C0C0902802D0902902E0902A02F09078
+:1023C0002B022AEA37E24FE155E4C701B6010E9429
+:1023D00014150E940A1760938E0370938F038093E5
+:1023E000900390939103609392037093930380936F
+:1023F0009403909395031092B2031092B30310923A
+:10240000B4031092B50310928103109282031092CC
+:1024100083031092840310929603109297031092F4
+:1024200098031092990320E030E04AE756E4609167
+:102430001A0270911B0280911C0290911D020E9451
+:10244000141520E030E040E251E40E9498164B0160
+:102450005C0160914A0370914B0380914C03909111
+:102460004D030E94B5149B01AC01C501B4010E944B
+:1024700027160E940A1760934A0370934B038093B8
+:102480004C0390934D0360934E0370934F038093DE
+:1024900050039093510310926E0310926F031092A9
+:1024A00070031092710310923D0310923E0310923C
+:1024B0003F03109240031092520310925303109264
+:1024C000540310925503C0922202D0922302E0924C
+:1024D0002402F092250210921A0210921B0210920E
+:1024E0001C0210921D0220912C0230912D0240916D
+:1024F0002E0250912F02C701B6010E945E07109272
+:102500000401D3018D919D910D90BC91A02D8B8BD9
+:102510009C8BAD8BBE8BD30114968D919D910D90AC
+:10252000BC91A02D8F8B988FA98FBA8FBFEF4B1ABC
+:102530005B0AE8E06E0E711C2FCF2B893C894D8918
+:102540005E89D3016D917D918D919C910E9426169B
+:102550006B8F7C8F8D8F9E8F2F89388D498D5A8DF3
+:10256000F30164817581868197810E9426166B0133
+:102570007C012B8D3C8D4D8D5E8DCA01B9010E9471
+:1025800014154B015C01A7019601C701B6010E9419
+:1025900014159B01AC01C501B4010E9427160E94CD
+:1025A0005A1820E030E0A9010E9498160E940A17EC
+:1025B0008B011616170614F001E010E0312C212CC7
+:1025C000C801012E000CAA0BBB0B8F8F98A3A9A3E7
+:1025D000BAA3B101032C000C880B990B0E94B5140F
+:1025E0004B015C016F8D78A189A19AA10E94B5145D
+:1025F0009B01AC01C501B4010E9498164B015C011E
+:10260000AC019B01C701B6010E9414152F89388DBA
+:10261000498D5A8D0E9427166BA37CA38DA39EA380
+:10262000A50194016B8D7C8D8D8D9E8D0E9414155E
+:102630002B893C894D895E890E9427162BA13CA1DC
+:102640004DA15EA10E945E079FEF291A390A02156B
+:1026500013050CF0BECF55CF1092780210922702CE
+:10266000109226028DE491E00E940A092CCC11E020
+:10267000E4CC662777270C943D13B0E0A0E0E3E4B8
+:10268000F3E10C94E6155C017B016115710519F00D
+:10269000DB018D939C9385010F5F1F4FF501D08166
+:1026A0008D2F90E00E948D146C01892BB9F5DD32DD
+:1026B000B9F50F5F1F4FD5011196DC91C1E05801AC
+:1026C000F1E0AF1AB10843E050E06EE870E0C501F8
+:1026D0000E949614892B69F5680182E0C80ED11C0E
+:1026E00045E050E069E870E0C6010E949614892B2D
+:1026F00021F4680197E0C90ED11CE114F10419F02E
+:10270000D701CD92DC9260E070E080E89FEFC111CC
+:10271000FFC060E070E080E89FE7FAC05801BBCFDF
+:10272000DB3229F485010E5F1F4FF501D181C0E036
+:10273000C6CF43E050E066E870E0C5010E94961401
+:10274000892BE9F0F80110E000E020E030E0A90179
+:102750005F01B0ED8B2E8D0E89E08815C8F19C2E9F
+:10276000689491F88C2F8870C2FF16C0811102C046
+:102770000F5F1F4F3196D501DC91C92DE9CFE114D0
+:10278000F10429F00E5F1F4FF7011183008360E011
+:1027900070E080EC9FE7BCC0882311F00150110964
+:1027A000A5E0B0E00E94D5159B01AC01220F331FBC
+:1027B000441F551F280D311D411D511D283999E910
+:1027C0003907490799E15907A8F2C6609C2ED2CF74
+:1027D000AEEF8A1206C0C3FD3CC09C2E689493F8ED
+:1027E000C9CFDF7DD534A9F580818D3239F4C06140
+:1027F000DF011296818162E070E006C0DF018B325A
+:10280000C1F3119661E070E080535D01A61AB70A2A
+:102810008A30F8F4E0E8CE16ECE0DE065CF4B601AF
+:10282000660F771F660F771FC60ED71ECC0CDD1CF8
+:10283000C80ED11C5D01FFEFAF1ABF0A8C91805307
+:102840008A30A8F1C4FF03C0D194C194D1080C0D03
+:102850001D1DC1FF09C0E114F10431F081E0A81A87
+:10286000B108D701AD92BC92CA01B9010E94B3145C
+:10287000C370C33009F490584B015C0120E030E094
+:10288000A9010E94AE14882309F440C0CDEBD0E02A
+:1028900017FF05C0119501951109C5EAD0E06E0139
+:1028A000B8E1CB1AD10880E2E82EF12C0FC0D50197
+:1028B000B1CFFE0125913591459154910E191F0913
+:1028C000C501B4010E9414154B015C01D501C4017E
+:1028D0000E151F0574F72497F594E794CC16DD06C2
+:1028E000A9F78A2F880F8B2F881F8F3F49F020E090
+:1028F00030E0A901C501B4010E94AE14811106C0E7
+:1029000082E290E09093C3038093C203C501B401B7
+:10291000CDB7DEB7ECE00C94021691110C94811542
+:10292000803219F089508550C8F70895FB01DC0109
+:102930004150504088F08D9181341CF08B350CF45F
+:10294000805E659161341CF06B350CF4605E861B13
+:10295000611171F3990B0895881BFCCF0E94F0144C
+:1029600008F481E00895E89409C097FB3EF490953F
+:102970008095709561957F4F8F4F9F4F9923A9F058
+:10298000F92F96E9BB279395F695879577956795E7
+:10299000B795F111F8CFFAF4BB0F11F460FF1BC02B
+:1029A0006F5F7F4F8F4F9F4F16C0882311F096E9BE
+:1029B00011C0772321F09EE8872F762F05C066236C
+:1029C00071F096E8862F70E060E02AF09A95660F25
+:1029D000771F881FDAF7880F9695879597F90895DE
+:1029E000990F0008550FAA0BE0E8FEEF1616170620
+:1029F000E807F907C0F012161306E407F50798F088
+:102A0000621B730B840B950B39F40A2661F0232BA0
+:102A1000242B252B21F408950A2609F4A140A6951C
+:102A20008FEF811D811D08950E9427150C949B1521
+:102A30000E948D1538F00E94941520F0952311F016
+:102A40000C9484150C948A1511240C94CF150E94B3
+:102A5000AC1570F3959FC1F3950F50E0551F629F21
+:102A6000F001729FBB27F00DB11D639FAA27F00DE7
+:102A7000B11DAA1F649F6627B00DA11D661F829F0E
+:102A80002227B00DA11D621F739FB00DA11D621FF3
+:102A9000839FA00D611D221F749F3327A00D611D10
+:102AA000231F849F600D211D822F762F6A2F1124F2
+:102AB0009F5750409AF0F1F088234AF0EE0FFF1F25
+:102AC000BB1F661F771F881F91505040A9F79E3F7C
+:102AD000510580F00C9484150C94CF155F3FE4F3FE
+:102AE000983ED4F3869577956795B795F795E795D2
+:102AF0009F5FC1F7FE2B880F911D9695879597F9DB
+:102B0000089599278827089597F99F6780E870E0CE
+:102B100060E008959FEF80EC089500240A94161653
+:102B2000170618060906089500240A9412161306BB
+:102B3000140605060895092E0394000C11F4882349
+:102B400052F0BB0F40F4BF2B11F460FF04C06F5F65
+:102B50007F4F8F4F9F4F089557FD9058440F551F3B
+:102B600059F05F3F71F04795880F97FB991F61F00F
+:102B70009F3F79F087950895121613061406551F86
+:102B8000F2CF4695F1DF08C0161617061806991FF2
+:102B9000F1CF86957105610508940895E894BB27E7
+:102BA00066277727CB0197F908950E941716A59FEE
+:102BB000900DB49F900DA49F800D911D1124089538
+:102BC0002F923F924F925F926F927F928F929F923D
+:102BD000AF92BF92CF92DF92EF92FF920F931F932B
+:102BE000CF93DF93CDB7DEB7CA1BDB0B0FB6F894DC
+:102BF000DEBF0FBECDBF09942A88398848885F841C
+:102C00006E847D848C849B84AA84B984C884DF808C
+:102C1000EE80FD800C811B81AA81B981CE0FD11D70
+:102C20000FB6F894DEBF0FBECDBFED010895A29F91
+:102C3000B001B39FC001A39F700D811D1124911D90
+:102C4000B29F700D811D1124911D08955058BB270E
+:102C5000AA270E943E160C949B150E948D1538F0F1
+:102C60000E94941520F039F49F3F19F426F40C9437
+:102C70008A150EF4E095E7FB0C948415E92F0E9469
+:102C8000AC1558F3BA17620773078407950720F04D
+:102C900079F4A6F50C94CE150EF4E0950B2EBA2F10
+:102CA000A02D0B01B90190010C01CA01A001112452
+:102CB000FF27591B99F0593F50F4503E68F11A16FE
+:102CC000F040A22F232F342F4427585FF3CF46958F
+:102CD00037952795A795F0405395C9F77EF41F16B1
+:102CE000BA0B620B730B840BBAF09150A1F0FF0F7B
+:102CF000BB1F661F771F881FC2F70EC0BA0F621F67
+:102D0000731F841F48F4879577956795B795F79556
+:102D10009E3F08F0B0CF9395880F08F09927EE0FEB
+:102D20009795879508950E94D617E3950C94FF1701
+:102D30000E94AC160C949B150E94941558F00E94AA
+:102D40008D1540F029F45F3F29F00C948415511142
+:102D50000C94CF150C948A150E94AC1568F3992336
+:102D6000B1F3552391F3951B550BBB27AA27621787
+:102D70007307840738F09F5F5F4F220F331F441F94
+:102D8000AA1FA9F335D00E2E3AF0E0E832D09150C8
+:102D90005040E695001CCAF72BD0FE2F29D0660FB5
+:102DA000771F881FBB1F261737074807AB07B0E8F8
+:102DB00009F0BB0B802DBF01FF2793585F4F3AF0FE
+:102DC0009E3F510578F00C9484150C94CF155F3F0D
+:102DD000E4F3983ED4F3869577956795B795F79584
+:102DE0009F5FC9F7880F911D9695879597F908956C
+:102DF000E1E0660F771F881FBB1F62177307840708
+:102E0000BA0720F0621B730B840BBA0BEE1F88F716
+:102E1000E09508950E9411176894B1110C94CF1594
+:102E200008950E94B41588F09F5798F0B92F9927FC
+:102E3000B751B0F0E1F0660F771F881F991F1AF0A5
+:102E4000BA95C9F714C0B13091F00E94CE15B1E027
+:102E500008950C94CE15672F782F8827B85F39F026
+:102E6000B93FCCF3869577956795B395D9F73EF43E
+:102E700090958095709561957F4F8F4F9F4F0895E6
+:102E80000E94131890F09F3748F4911116F00C949B
+:102E9000CF1560E070E080E89FEB089526F41B16E4
+:102EA000611D711D811D0C94AB170C94C6170E94F7
+:102EB0008D1520F019F00E94941550F40C948A1589
+:102EC0000C94CF15E92F0E94AC1588F35523B1F36C
+:102ED000E7FB6217730784079507A8F189F3E92FC9
+:102EE000FF2788232AF03197660F771F881FDAF7AC
+:102EF000952F5527442332F091505040220F331F15
+:102F0000441FD2F7BB27E91BF50B621B730B840B25
+:102F1000B109B1F222F4620F731F841FB11D319702
+:102F20002AF0660F771F881FBB1FEFCF91505040CC
+:102F300062F041F0882332F0660F771F881F9150AE
+:102F40005040C1F793950C94C61786957795679571
+:102F50009F5FD9F7F7CF882371F4772321F098503A
+:102F6000872B762F07C0662311F499270DC0905147
+:102F7000862B70E060E02AF09A95660F771F881F15
+:102F8000DAF7880F9695879597F908959F3F31F066
+:102F9000915020F4879577956795B795880F911D87
+:102FA0009695879597F908950C948A150E94B41503
+:102FB000D8F3E894E0E0BB279F57F0F02AED3FE01C
+:102FC00049EC06C0EE0FBB0F661F771F881F28F065
+:102FD000B23A62077307840728F0B25A620B730B88
+:102FE000840BE3959A9572F7803830F49A95BB0F6D
+:102FF000661F771F881FD2F790480C94C817EF936D
+:10300000E0FF07C0A2EA2AED3FE049EC5FEB0E9437
+:103010003E160E949B150F90039401FC9058E8E621
+:10302000F0E00C94A1180E94B415A0F0BEE7B91707
+:1030300088F4BB279F3860F41616B11D672F782FD0
+:103040008827985FF7CF869577956795B11D9395FB
+:103050009639C8F308950E94F01408F48FEF08958C
+:103060000E94B415E8F09E37E8F09639B8F49E381F
+:1030700048F4672F782F8827985FF9CF8695779542
+:10308000679593959539D0F3B62FB1706B0F711D7D
+:10309000811D20F487957795679593950C94AB17D0
+:1030A0000C94C6170C94CF1519F416F40C948A15C9
+:1030B0000C94C6170E94B415B8F39923C9F3B6F35C
+:1030C0009F57550B87FF0E949A180024A0E640EAFC
+:1030D000900180585695979528F4805C660F771F6D
+:1030E000881F20F026173707480730F4621B730B40
+:1030F000840B202931294A2BA69517940794202563
+:1031000031254A2758F7660F771F881F20F02617AA
+:103110003707480730F4620B730B840B200D311D09
+:10312000411DA09581F7B901842F9158880F96957C
+:103130008795089591505040660F771F881FD2F7EA
+:1031400008959F938F937F936F93FF93EF939B01CA
+:10315000AC010E941415EF91FF910E94B5182F91B8
+:103160003F914F915F910C941415DF93CF931F9370
+:103170000F93FF92EF92DF927B018C01689406C05F
+:10318000DA2EEF010E942715FE01E894A591259102
+:10319000359145915591A6F3EF010E943E16FE012F
+:1031A0009701A801DA9469F7DF90EF90FF900F91F3
+:1031B0001F91CF91DF910895052E97FB1EF4009487
+:1031C0000E94F31857FD07D00E94011907FC03D095
+:1031D0004EF40C94F31850954095309521953F4F3F
+:1031E0004F4F5F4F089590958095709561957F4FF3
+:1031F0008F4F9F4F0895EE0FFF1F0590F491E02D24
+:103200000994A1E21A2EAA1BBB1BFD010DC0AA1F27
+:10321000BB1FEE1FFF1FA217B307E407F50720F03F
+:10322000A21BB30BE40BF50B661F771F881F991FBA
+:103230001A9469F760957095809590959B01AC0103
+:10324000BD01CF0108950F931F93CF93DF93823079
+:10325000910510F482E090E0E091C603F091C7037D
+:1032600030E020E0B0E0A0E0309799F4211531057E
+:1032700009F44AC0281B390B24303105D8F58A815E
+:103280009B816115710589F1FB0193838283FE01A6
+:1032900011C0408151810281138148175907E0F024
+:1032A0004817590799F4109761F012960C931297EA
+:1032B00013961C933296CF01DF91CF911F910F91FE
+:1032C00008950093C6031093C703F4CF2115310569
+:1032D00051F04217530738F0A901DB019A01BD01F3
+:1032E000DF01F801C1CFEF01F9CF9093C7038093BD
+:1032F000C603CDCFFE01E20FF31F819391932250BD
+:10330000310939832883D7CF2091C4033091C50375
+:10331000232B41F420910701309108013093C5031C
+:103320002093C40320910501309106012115310538
+:1033300041F42DB73EB74091090150910A01241B79
+:10334000350BE091C403F091C503E217F307A0F435
+:103350002E1B3F0B2817390778F0AC014E5F5F4FEB
+:103360002417350748F04E0F5F1F5093C503409355
+:10337000C403819391939FCFF0E0E0E09CCFCF9383
+:10338000DF930097E9F0FC01329713821282A0913B
+:10339000C603B091C703ED0130E020E01097A1F41F
+:1033A00020813181820F931F2091C4033091C50386
+:1033B0002817390709F061C0F093C503E093C403EF
+:1033C000DF91CF910895EA01CE17DF07E8F54A8132
+:1033D0005B819E0141155105B1F7E901FB83EA8349
+:1033E00049915991C40FD51FEC17FD0761F48081F5
+:1033F00091810296840F951FE901998388838281C8
+:1034000093819B838A83F0E0E0E012968D919C91FA
+:1034100013970097B9F52D913C911197CD01029624
+:10342000820F931F2091C4033091C50328173907D9
+:1034300039F6309751F51092C7031092C603B09336
+:10344000C503A093C403BCCFD383C2834081518101
+:10345000840F951FC817D90761F44E5F5F4F8881AD
+:103460009981480F591F518340838A819B8193839F
+:1034700082832115310509F0B0CFF093C703E093A3
+:10348000C6039ECFFD01DC01C0CF13821282D7CFCD
+:10349000B0E0A0E0EEE4FAE10C94E2158C010097B4
+:1034A00051F4CB010E9423198C01C801CDB7DEB7BE
+:1034B000E0E10C94FE15FC01E60FF71F9C01225081
+:1034C0003109E217F30708F49DC0D901CD91DC91D1
+:1034D0001197C617D70798F0C530D10530F3CE0144
+:1034E00004978617970708F3C61BD70B2297C1933B
+:1034F000D1936D937C93CF010E94BF19D6CF5B010E
+:10350000AC1ABD0A4C018C0E9D1EA091C603B09151
+:10351000C703512C412CF12CE12C109731F58091EF
+:10352000C4039091C5038815990509F05CC046163F
+:10353000570608F058C08091050190910601009748
+:1035400041F48DB79EB74091090150910A01841B47
+:10355000950BE817F90708F055C0F093C503E09301
+:10356000C403F90171836083A0CF8D919C91119761
+:1035700012966C90129713967C901397A815B90524
+:1035800059F56C0142E0C40ED11CCA14DB0420F1D1
+:10359000AC014A195B09DA011296159780F0628234
+:1035A000738251834083D9016D937C93E114F104BC
+:1035B00071F0D7011396FC93EE93129776CF229673
+:1035C0008C0F9D1FF90191838083F301EFCFF0935E
+:1035D000C703E093C60369CF4816590608F42C01C7
+:1035E0007D01D3019ACFCB010E9423197C01009762
+:1035F00049F0AE01B8010E94051BC8010E94BF1925
+:10360000870153CF10E000E050CFFB01DC0102C086
+:1036100001900D9241505040D8F70895FC018191DE
+:10362000861721F08823D9F7992708953197CF017C
+:103630000895FB01DC018D91019080190110D9F3EF
+:10364000990B0895FB01DC0101900D920020E1F738
+:103650000895FB01DC014150504030F08D91019004
+:10366000801919F40020B9F7881B990B0895FB0104
+:1036700051915523A9F0BF01DC014D91451741112E
+:10368000E1F759F4CD010190002049F04D9140152A
+:103690004111C9F3FB014111EFCF81E090E00197A7
+:0636A0000895F894FFCF2D
+:1036A600CDCC3C40010000C8038000000000003E75
+:1036B600025E018B018B02FC019A01EE0100000003
+:1036C60000B3037F035D07360A920514055205FE13
+:1036D60004CC04AB04770456040004D1030D0A009D
+:1036E600484F4D494E4700484F4D45440052005300
+:1036F6007069726F6772617068204D6F64652041F2
+:10370600637469766500417070204D6F6465204171
+:1037160063746976650054484554415F5245534584
+:103726005400484F4D450052455345545F54484553
+:103736005441004745545F56455253494F4E005336
+:1037460045545F5350454544003B0049474E4F5250
+:1037560045443A20005461626C653A2044756E65B2
+:103766002057656176657200447269766572733AB0
+:1037760020544D43323230390056657273696F6E8C
+:103786003A20312E342E30005245414459005350D0
+:103796004545445F53455400494E56414C49445FA4
+:1037A600535045454400494E56414C49445F434FAA
+:0637B6004D4D414E4400A0
+:00000001FF

+ 49 - 26
esp32/esp32.ino → firmware/esp32/esp32.ino

@@ -5,15 +5,15 @@
 #define rotInterfaceType AccelStepper::DRIVER
 #define inOutInterfaceType AccelStepper::DRIVER
 
-#define ROT_PIN1 14
-#define ROT_PIN2 12
-#define ROT_PIN3 26
-#define ROT_PIN4 27
+#define ROT_PIN1 27
+#define ROT_PIN2 26
+#define ROT_PIN3 12
+#define ROT_PIN4 14
 
-#define INOUT_PIN1 16
-#define INOUT_PIN2 17
-#define INOUT_PIN3 18
-#define INOUT_PIN4 19
+#define INOUT_PIN1 19
+#define INOUT_PIN2 18
+#define INOUT_PIN3 17
+#define INOUT_PIN4 16
 
 
 #define rot_total_steps 12800
@@ -44,6 +44,10 @@ double maxSpeed = 500;
 double maxAcceleration = 5000;
 double subSteps = 1;
 
+// FIRMWARE VERSION
+const char* firmwareVersion = "1.4.0";
+const char* motorType = "esp32";
+
 int modulus(int x, int y) {
   return x < 0 ? ((x + 1) % y) + y - 1 : x % y;
 }
@@ -67,6 +71,13 @@ void setup()
     homing();
 }
 
+void getVersion()
+{
+    Serial.println("Table: Mini Dune Weaver");
+    Serial.println("Drivers: ULN2003");
+    Serial.println("Version: 1.4.0");
+}
+
 void loop()
 {
     // Check for incoming serial commands or theta-rho pairs
@@ -75,12 +86,16 @@ void loop()
         String input = Serial.readStringUntil('\n');
 
         // Ignore invalid messages
-        if (input != "HOME" && input != "RESET_THETA" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
+        if (input != "HOME" && input != "RESET_THETA"  && input != "GET_VERSION" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
         {
             Serial.println("IGNORED");
             return;
         }
 
+        if (input == "GET_VERSION") {
+            getVersion();
+        }
+
         // Example: The user calls "SET_SPEED 60" => 60% of maxSpeed
         if (input.startsWith("SET_SPEED"))
         {
@@ -102,7 +117,6 @@ void loop()
                     inOutStepper.setMaxSpeed(newSpeed);
 
                     Serial.println("SPEED_SET");  
-                    Serial.println("R");
                 }
                 else
                 {
@@ -173,19 +187,28 @@ void loop()
         double startTheta = currentTheta;
         double startRho = currentRho;
 
-        if (isFirstCoordinates) {
-          homing();
-          isFirstCoordinates = false;
-        }
-
         for (int i = 0; i < bufferCount; i++)
         {
  
-            interpolatePath(
-                startTheta, startRho,
-                buffer[i][0], buffer[i][1],
-                subSteps
-            );
+            if (isFirstCoordinates)
+            {
+                // Directly move to the first coordinate of the new pattern
+                long initialRotSteps = buffer[0][0] * (rot_total_steps / (2.0 * M_PI));
+                rotStepper.setCurrentPosition(initialRotSteps);
+                inOutStepper.setCurrentPosition(inOutStepper.currentPosition() + (totalRevolutions * rot_total_steps / gearRatio));
+
+                currentTheta = buffer[0][0];
+                totalRevolutions = 0;
+                movePolar(buffer[0][0], buffer[0][1]);
+                isFirstCoordinates = false; // Reset the flag after the first movement
+            } else
+            {
+                interpolatePath(
+                    startTheta, startRho,
+                    buffer[i][0], buffer[i][1],
+                    subSteps
+                );
+            }
             // Update the starting point for the next segment
             startTheta = buffer[i][0];
             startRho = buffer[i][1];
@@ -225,11 +248,6 @@ void homing()
 
 void movePolar(double theta, double rho)
 {
-    if (rho < 0.0) 
-        rho = 0.0;
-    else if (rho > 1.0) 
-        rho = 1.0;
-
     long rotSteps = lround(theta * (rot_total_steps / (2.0f * M_PI)));
     double revolutions = theta / (2.0 * M_PI);
     long offsetSteps = lround(revolutions * (rot_total_steps / gearRatio));
@@ -237,7 +255,12 @@ void movePolar(double theta, double rho)
     // Now inOutSteps is always derived from the absolute rho, not incrementally
     long inOutSteps = lround(rho * inOut_total_steps);
     
-    inOutSteps -= offsetSteps;
+    totalRevolutions += (theta - currentTheta) / (2.0 * M_PI);
+    
+    if (!isFirstCoordinates)
+    {
+        inOutSteps -= offsetSteps;
+    }
 
     long targetPositions[2] = {rotSteps, inOutSteps};
     multiStepper.moveTo(targetPositions);

BIN
firmware/esp32/esp32.ino.bin


+ 285 - 0
firmware/esp32_TMC2209/esp32_TMC2209.ino

@@ -0,0 +1,285 @@
+#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 22
+#define dirPin_rot 23
+#define stepPin_InOut 18
+#define dirPin_InOut 19
+
+
+#define rot_total_steps 16000.0
+#define inOut_total_steps 5760.0
+#define gearRatio 10
+
+#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;
+
+// Track the current position in polar coordinates
+float currentTheta = 0.0; // Current theta in radians
+float currentRho = 0.0;   // Current rho (0 to 1)
+bool isFirstCoordinates = true;
+float totalRevolutions = 0.0; // Tracks cumulative revolutions
+long maxSpeed = 1000;
+float maxAcceleration = 1000;
+long interpolationResolution = 1;
+float userDefinedSpeed = maxSpeed; // Store user-defined speed
+
+void setup()
+{
+    // Set maximum speed and acceleration
+    rotStepper.setMaxSpeed(maxSpeed);     // Adjust as needed
+    rotStepper.setAcceleration(maxAcceleration); // Adjust as needed
+
+    inOutStepper.setMaxSpeed(maxSpeed);     // Adjust as needed
+    inOutStepper.setAcceleration(maxAcceleration); // Adjust as needed
+
+    // Add steppers to MultiStepper
+    multiStepper.addStepper(rotStepper);
+    multiStepper.addStepper(inOutStepper);
+
+    // Initialize serial communication
+    Serial.begin(115200);
+    Serial.println("R");
+    homing();
+}
+
+void getVersion() {
+    Serial.println("Table: Dune Weaver");
+    Serial.println("Drivers: ESP32-TMC2209");
+    Serial.println("Version: 1.4.0");
+}
+
+
+void resetTheta()
+{
+    isFirstCoordinates = true; // Set flag to skip interpolation for the next movement
+    Serial.println("THETA_RESET"); // Notify Python
+}
+
+void loop() {
+        appMode();
+}
+
+void appMode()
+{
+    // 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 != "RESET_THETA" && input != "GET_VERSION" && !input.startsWith("SET_SPEED") && !input.endsWith(";"))
+        {
+            Serial.print("IGNORED: ");
+            Serial.println(input);
+            return;
+        }
+
+        if (input == "GET_VERSION")
+        {
+            getVersion();
+        }
+
+        if (input == "RESET_THETA")
+        {
+            resetTheta(); // Reset currentTheta
+            Serial.println("THETA_RESET"); // Notify Python
+            Serial.println("READY");
+            return;
+        }
+        if (input == "HOME")
+        {
+            homing();
+            return;
+        }
+
+
+        if (input.startsWith("SET_SPEED"))
+        {
+            // Parse out the speed value from the command string
+            int spaceIndex = input.indexOf(' ');
+            if (spaceIndex != -1)
+            {
+                String speedStr = input.substring(spaceIndex + 1);
+                float speedPercentage = speedStr.toFloat();
+
+                // Make sure the percentage is valid
+                if (speedPercentage >= 1.0 && speedPercentage <= 100.0)
+                {
+                    // Convert percentage to actual speed
+                    long newSpeed = (speedPercentage / 100.0) * maxSpeed;
+                    userDefinedSpeed = newSpeed;
+
+                    // Set the stepper speeds
+                    rotStepper.setMaxSpeed(newSpeed);
+                    inOutStepper.setMaxSpeed(newSpeed);
+
+                    Serial.println("SPEED_SET");  
+                }
+                else
+                {
+                    Serial.println("INVALID_SPEED");
+                }
+            }
+            else
+            {
+                Serial.println("INVALID_COMMAND");
+            }
+            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)
+    {
+        // Start interpolation from the current position
+        float startTheta = currentTheta;
+        float startRho = currentRho;
+
+        for (int i = 0; i < bufferCount; i++)
+        {
+            if (isFirstCoordinates)
+            {
+                // Directly move to the first coordinate of the new pattern
+                long initialRotSteps = buffer[0][0] * (rot_total_steps / (2.0 * M_PI));
+                rotStepper.setCurrentPosition(initialRotSteps);
+                inOutStepper.setCurrentPosition(inOutStepper.currentPosition() + (totalRevolutions * rot_total_steps / gearRatio));
+
+                currentTheta = buffer[0][0];
+                totalRevolutions = 0;
+                movePolar(buffer[0][0], buffer[0][1]);
+                isFirstCoordinates = false; // Reset the flag after the first movement
+            }
+              else
+              {
+                // Use interpolation for subsequent movements
+                interpolatePath(
+                    startTheta, startRho,
+                    buffer[i][0], buffer[i][1],
+                    interpolationResolution
+                );
+              }
+            // Update the starting point for the next segment
+            startTheta = buffer[i][0];
+            startRho = buffer[i][1];
+        }
+
+        batchComplete = false; // Reset batch flag
+        bufferCount = 0;       // Clear buffer
+        Serial.println("R");
+    }
+}
+
+void homing()
+{
+    Serial.println("HOMING");
+
+    // Move inOutStepper inward for homing
+    inOutStepper.setSpeed(-maxSpeed); // Adjust speed for homing
+    while (true)
+    {
+        inOutStepper.runSpeed();
+        if (inOutStepper.currentPosition() <= -inOut_total_steps * 1.1)
+        { // Adjust distance for homing
+            break;
+        }
+    }
+    inOutStepper.setCurrentPosition(0); // Set home position
+    currentTheta = 0.0;                 // Reset polar coordinates
+    currentRho = 0.0;
+    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 * rot_total_steps / gearRatio;    // 1600 steps inward or outward per revolution
+
+    // Update the total revolutions to keep track of the offset history
+    totalRevolutions += (theta - currentTheta) / (2.0 * M_PI);
+
+    // Apply the offset to the inout axis
+    if (!isFirstCoordinates) {
+        inOutSteps -= offsetSteps;
+    }
+
+    // Define target positions for both motors
+    long targetPositions[2];
+    targetPositions[0] = rotSteps;
+    targetPositions[1] = inOutSteps;
+
+    // Move both motors synchronously
+    multiStepper.moveTo(targetPositions);
+    multiStepper.runSpeedToPosition(); // Blocking call
+
+    // Update the current coordinates
+    currentTheta = theta;
+    currentRho = rho;
+}
+
+void interpolatePath(float startTheta, float startRho, float endTheta, float endRho, float stepSize)
+{
+    // Calculate the total distance in the polar coordinate system
+    float distance = sqrt(pow(endTheta - startTheta, 2) + pow(endRho - startRho, 2));
+    int numSteps = max(1, (int)(distance / stepSize)); // Ensure at least one step
+
+    for (int step = 0; step <= numSteps; step++)
+    {
+        float t = (float)step / numSteps; // Interpolation factor (0 to 1)
+        float interpolatedTheta = startTheta + t * (endTheta - startTheta);
+        float interpolatedRho = startRho + t * (endRho - startRho);
+
+        // Move to the interpolated theta-rho
+        movePolar(interpolatedTheta, interpolatedRho);
+    }
+}

BIN
firmware/esp32_TMC2209/esp32_TMC2209.ino.bin


+ 0 - 2
patterns/Petalar.thr

@@ -1,5 +1,3 @@
-0 0
-0 0
 628.31853072 1
 628.33754948 0.99705484
 628.35487426 0.98848153

+ 0 - 2
patterns/SimpleRadiance.thr

@@ -1,5 +1,3 @@
-0 0
-0 0
 125.66370614 0.2
 125.66370614 1
 6.28318531 0.8

+ 0 - 3
patterns/SwoopyRadiance.thr

@@ -1,6 +1,3 @@
-0 0
-0 0
-0 1
 -565.48667765 0.1
 -565.4475053 0.3020095
 -565.38295591 0.48462502

+ 2 - 2
requirements.txt

@@ -1,4 +1,4 @@
 flask
 pyserial
-numpy
-svgpathtools
+esptool
+tqdm

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 5 - 0
static/css/all.min.css


+ 484 - 48
static/style.css → static/css/style.css

@@ -2,18 +2,18 @@
     --background-primary: #f9f9f9;
     --background-secondary: #fff;
     --background-tertiary: #ddd;
-    --background-accent: rgba(74, 144, 226, 0.75);
-    --background-info: var(--background-accent);
-    --background-success: rgba(76, 175, 80, 0.8);
-    --background-warning: rgba(255, 152, 0, 0.8);
-    --background-error: rgba(229, 57, 53, 0.8);
+    --background-accent: #4e453fbf;
+    --background-info: var(--color-info);
+    --background-success: var(--color-success);
+    --background-warning: var(--color-warning);
+    --background-error: var( --color-error);
 
     --theme-primary: #6A9AD9;
     --theme-primary-hover: #A0CCF2;
     --theme-secondary: #C4B4A0;
     --theme-secondary-hover: #4E453F;
 
-    --color-info: var(--theme-primary);
+    --color-info: #6A9AD9CC;
     --color-success: #4CAF50CC;
     --color-warning: #FF9800CC;
     --color-error: #E53935CC;
@@ -33,6 +33,15 @@
     --transition-slow: 1s ease;
 }
 
+@font-face {
+    font-family: 'Roboto';
+    src: url('../webfonts/Roboto-VariableFont_wdth,wght.ttf') format('truetype');
+    font-weight: 100 900; /* Variable range of weights */
+    font-stretch: 75% 100%; /* Variable width range (optional) */
+    font-style: normal;
+}
+
+
 /* General
 
 /* General Styling */
@@ -62,7 +71,6 @@ header {
     display: flex;
     justify-content: center;
     align-items: center;
-
 }
 
 h1, h2 {
@@ -172,6 +180,85 @@ input, select {
     border-color: var(--theme-primary-hover);
 }
 
+
+/* Scrollable Selection Styles */
+.scrollable-selection {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    min-width: 50%;
+    position: relative;
+}
+
+.scroll-arrow {
+    border: none;
+    padding: 5px;
+    width: 100%;
+    cursor: pointer;
+    font-size: 1rem;
+    transition: background-color 0.3s;
+}
+
+.scroll-arrow:hover {
+    background: var(--theme-primary-hover);
+}
+
+.selection-container {
+    overflow: hidden;
+    height: 50px; /* Adjust based on visible area */
+    width: 100%;
+    position: relative;
+    border: 1px solid var(--border-primary);
+    background: var(--theme-primary);
+    color: var(--text-secondary);
+    border-radius: 5px;
+    font-weight: bold;
+}
+
+.nav-items {
+    position: absolute;
+    right: 0;
+    top: 0;
+    display: flex;
+    height: 100%;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.selection-container .nav-items > button {
+    height: 50%;
+    padding: 0;
+    width: 100% !important;
+}
+
+.selection-container .nav-items > button:hover {
+    background: var(--text-secondary);
+    color: var(--theme-primary);
+}
+
+.selection-items {
+    display: flex;
+    flex-direction: column;
+    transition: transform 0.3s ease;
+    padding-right: 30px;
+}
+
+.selection-item {
+    height: 50px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    padding: 5px;
+    transition: background-color 0.3s ease;
+}
+
+.selection-item:hover {
+    background: var(--theme-primary-hover);
+    color: var(--text-secondary);
+}
+
 /* Buttons */
 button {
     background: var(--theme-primary);
@@ -182,10 +269,13 @@ button {
     border-radius: 5px;
     cursor: pointer;
     font-size: 1rem;
-    transition: background 0.3s ease,color 0.3s ease;
+    transition: var(--transition-medium) all;
+    display: flex;
+    justify-content: center;
+    align-items: center;
 }
 
-button:not(.close-button, .fullscreen-button, .move-button, .remove-button):hover {
+button:not(.no-bg):hover{
     background: var(--background-info);
 }
 
@@ -244,10 +334,43 @@ section.main {
 
 section.debug {
     flex-direction: row;
-    align-items: center;
+    align-items: flex-end;
     justify-content: space-between;
 }
 
+section.version {
+    flex-direction: row;
+    justify-content: space-between;
+    flex-wrap: wrap;
+}
+
+section#settings-container > section{
+    padding-left: 0;
+    padding-right: 0;
+}
+
+
+.version .header {
+    width: 100%;
+}
+
+.version #motor_selection h3 {
+    width: 100%;
+    flex-grow: 1;
+}
+
+.version #motor_selection {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    width: 100%;
+}
+
+.version select#manual_motor_type {
+    margin: 0 20px 0 0;
+    flex: 1;
+}
+
 section.sticky {
     position: fixed;
     background-color: rgba(255, 255, 255, 0.5);
@@ -256,9 +379,9 @@ section.sticky {
     border-top: 1px solid var(--border-primary);
     box-shadow: var(--shadow-primary);
     transform: translateY(0);
-    transition: 250ms transform, 250ms height;
+    transition: var(--transition-medium) transform, var(--transition-medium) height;
     visibility: visible;
-    max-height: 60vh;
+    max-height: 75vh;
     width: 100%;
     z-index: 10;
 }
@@ -284,18 +407,22 @@ section.sticky.hidden {
 section .header {
     position: relative;
     display: flex;
-    justify-content: space-between;
+    justify-content: flex-end;
     align-items: center;
     margin-bottom: 10px;
+    width: 100%;
 }
 
 section .header h2 {
     flex-grow: 1;
 }
 
+section .header #open-settings-button:hover{
+    color: var(--theme-primary);
+}
+
 /* Close Button Styling */
-.close-button,
-.fullscreen-button {
+button.no-bg {
     background: none;
     border: none;
     font-size: 1.5rem;
@@ -324,7 +451,6 @@ section .header h2 {
 section .header .add-button {
     height: 35px;
     width: 35px;
-    font-size: 1.5rem;
     padding: 0;
 }
 
@@ -343,11 +469,24 @@ section .header .add-button {
     margin-bottom: 20px;
 }
 
-#add-to-playlist-container select,
-#add-to-playlist-container button {
+#add-to-playlist-container {
+    display: flex;
+    flex-wrap: wrap;
+}
+
+#add-to-playlist-container h3{
+    margin: 0 10px 0 0;
+    align-self: center;
+}
+
+#add-to-playlist-container select{
+    width: auto;
+    flex-grow: 1;
+    margin: 0;
+}
+
+#add-to-playlist-container .action-buttons {
     margin-top: 10px;
-    display: block;
-    width: 100%;
 }
 
 .playlist-parameters {
@@ -356,9 +495,9 @@ section .header .add-button {
     gap: 10px;
 }
 
-.playlist-parameters .row {
-    display: flex;
-    gap: 10px;
+.playlist-parameters .control-group button.small.cancel {
+    align-self: flex-end;
+    margin-bottom: 4px;
 }
 
 #clear_pattern {
@@ -411,11 +550,11 @@ section .header .add-button {
     padding: 10px;
     border-bottom: 1px solid var(--border-primary);
     cursor: pointer;
-    transition: background-color 0.3s ease;
+    transition: background-color var(--transition-medium);
 }
 
 .file-list li:hover {
-    background-color: #f1f1f1;
+    background-color: var(--background-tertiary);
 }
 
 .file-list li.selected {
@@ -468,10 +607,6 @@ section .header .add-button {
     transition: background 0.3s ease;
 }
 
-.rename-button:hover {
-    background: #285A8E;
-}
-
 /* Bottom Navigation */
 .bottom-nav {
     display: flex;
@@ -487,6 +622,7 @@ section .header .add-button {
 
 .tab-button {
     flex: 1;
+    height: 60px;
     padding: 20px 10px;
     text-align: center;
     font-size: 1rem;
@@ -512,10 +648,57 @@ section .header .add-button {
     gap: 10px;
     flex-wrap: wrap;
     width: 100%;
+    justify-content: space-between;
 }
 
-.action-buttons button {
-    flex: 1;
+.action-buttons .scrollable-selection {
+    width: calc(50% - 10px);
+}
+
+.action-buttons.square button {
+    padding: 5px;
+    aspect-ratio: 1 / 1;
+    width: calc(25% - 10px);
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.action-buttons.square button i{
+    font-size: 2.5rem;
+}
+
+button i + span{
+    font-size: 1.25rem;
+}
+
+button i + span{
+    margin-left: 5px;
+}
+
+.action-buttons.square button i + span{
+    margin: 3px;
+}
+
+button i + span.small {
+    font-size: 0.75rem;
+}
+
+.action-buttons button i + span{
+    display: block;
+}
+
+.action-buttons button.m {
+    width: calc(50% - 10px);
+}
+
+.action-buttons button.l {
+    width: 100%;
+}
+
+.action-buttons button.small {
+    flex: 0;
+    flex-basis: calc(25% - 10px);
 }
 
 .action-buttons button.cta {
@@ -527,10 +710,11 @@ button#debug_button {
     padding: 0;
     height: 40px;
     background: transparent;
+    color: var(--text-primary);
     font-size: 1.5rem;
     margin-left: 40px;
     flex: 0 0 auto;
-    transition: 250ms all;
+    transition: var(--transition-medium) all;
 }
 
 button#debug_button:hover,
@@ -538,8 +722,36 @@ button#debug_button.active {
     box-shadow: inset 0 0 4px var(--border-secondary);
 }
 
-#settings-tab button.cancel {
-    flex-basis: 100%;
+#device-tab .dropdown {
+    width: 50%;
+}
+
+#settings-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: #FFFFFF80;
+    backdrop-filter: blur(10px);
+    z-index: 1000;
+    overflow-y: auto;
+    display: none; /* Hidden by default */
+    flex-direction: column;
+}
+
+#settings-container.open{
+    display: flex;
+}
+
+#open-settings-button {
+    aspect-ratio: auto;
+}
+
+#open-settings-button span {
+    order: -1;
+    margin-right: 5px;
+
 }
 
 /* Preview Canvas */
@@ -550,7 +762,7 @@ button#debug_button.active {
     border: 1px solid var(--border-primary);
     background: var(--theme-secondary);
     border-radius: 100%;
-    padding: 30px;
+    padding: 15px;
 }
 
 #pattern-preview {
@@ -565,6 +777,182 @@ button#debug_button.active {
     max-width: calc(100vw - 30px);
 }
 
+.pre-execution {
+    width: 100%;
+    display: flex;
+}
+
+.pre-execution h3 {
+    flex-grow: 1;
+    margin: 0;
+    align-content: center;
+}
+
+.pre-execution .control-group {
+    width: auto;
+    flex-grow: 1;
+    margin: 0;
+}
+
+.pre-execution select {
+    margin: 0;
+}
+
+/* Currently Playing Section Styling */
+body.playing .bottom-nav {
+    height: 200px;
+    align-items: flex-end;
+}
+
+#currently-playing-container {
+    align-items: center;
+    background: var(--background-accent);
+    color: var(--text-secondary);
+}
+
+#currently-playing-container h3,
+#currently-playing-container .open-button
+{
+    color: var(--text-secondary);
+}
+
+#currently-playing-container h3 {
+    margin: 0;
+}
+
+body:not(.playing) #currently-playing-container {
+    display: none;
+}
+
+#currently-playing-container.open {
+    max-height: none;
+    bottom: 60px;
+}
+
+body.playing #currently-playing-container:not(.open) {
+    height: 140px;
+    overflow:hidden;
+    flex-direction: row;
+    flex-wrap: wrap;
+    bottom: 60px;
+}
+
+body.playing #currently-playing-container .header{
+    justify-content: center;
+    margin-bottom: 0;
+}
+
+body.playing #currently-playing-container .header .open-button {
+    width: 100%;
+    height: 20px;
+    padding-top: 10px;
+    margin: 0;
+}
+
+body.playing #currently-playing-preview #currentlyPlayingCanvas {
+    max-width:100px;
+    padding: 5px;
+}
+
+body.playing #currently-playing-container:not(.open) .header .fullscreen-button,
+body.playing #currently-playing-container:not(.open) .header .close-button,
+body.playing #currently-playing-container:not(.open) .header h3 {
+    display: none;
+}
+
+body.playing #currently-playing-container:not(.open) #currently-playing-details{
+    flex-grow: 1;
+    flex-basis: 50%;
+    align-items: flex-start;
+    margin: 0 0 0 10px;
+    overflow-y: auto;
+}
+
+body.playing #currently-playing-container:not(.open) .play-buttons button {
+    width: 50px;
+    height: 50px;
+    font-size: 1.5rem;
+}
+
+body.playing #currently-playing-container:not(.open) #progress-container {
+    width: 100%;
+}
+
+
+#currentlyPlayingCanvas {
+    width: 100%;
+    max-width: 300px;
+    aspect-ratio: 1/1;
+    border: 1px solid var(--border-primary);
+    background: var(--theme-secondary);
+    border-radius: 100%;
+    padding: 10px;
+}
+
+#currently-playing-details {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 15px;
+}
+
+#currently-playing-details p {
+    margin: 5px 0;
+    font-size: 1rem;
+}
+
+#progress-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    flex-wrap: wrap;
+}
+
+#play_progress {
+    width: auto;
+    flex-grow: 1;
+    height: 8px;
+    appearance: none;
+    background-color: var(--border-primary);
+    border-radius: 4px;
+    overflow: hidden;
+}
+
+#play_progress::-webkit-progress-bar {
+    background-color: var(--border-primary);
+}
+
+#play_progress::-webkit-progress-value {
+    background-color: var(--theme-primary);
+    transition: width 0.25s ease;
+}
+
+#play_progress_text {
+    font-size: 0.9rem;
+    margin-left: 10px;
+}
+
+.play-buttons {
+    display: flex;
+    gap: 20px;
+}
+
+.play-buttons button {
+    width: 75px;
+    height: 75px;
+    aspect-ratio: 1/1;
+    font-size: 3rem;
+    border: none;
+    cursor: pointer;
+    display: flex;
+    justify-content: center;
+}
+
+#pausePlayCurrent {
+    border-radius: 50%;
+}
+
 /* Debug Log */
 #status_log {
     background: #000;
@@ -658,9 +1046,9 @@ button#debug_button.active {
     background-color: var(--color-error);
 }
 
-
 #serial_ports_buttons {
-    display: inline-block;
+    display: flex;
+    gap: 10px;
 }
 
 .status.connected {
@@ -720,11 +1108,6 @@ input[type="number"]:focus {
     min-width: 200px;
 }
 
-#serial_status_container,
-#serial_ports_buttons {
-    display: inline-block;
-}
-
 /* Notification Styles */
 .notification {
     display: flex;
@@ -740,7 +1123,8 @@ input[type="number"]:focus {
     align-items: center;
     backdrop-filter: blur(2px);
     opacity: 0;
-    transition: opacity 250ms ease-in-out;
+    transition: opacity var(--transition-medium);
+    cursor: pointer;
 }
 .notification.show {
     opacity: 1; /* Fully visible */
@@ -748,9 +1132,10 @@ input[type="number"]:focus {
 
 .notification .close-button {
     color: var(--text-secondary);
-    font-size: 2rem;
+    font-size: 1.5rem;
     top: 0;
     right: 0;
+    position: absolute;
 }
 
 /* Notification Types */
@@ -809,6 +1194,11 @@ input[type="number"]:focus {
     button.cta:hover {
         background: var(--theme-primary);
     }
+
+    body.playing section.sticky{
+        bottom: 200px;
+    }
+
 }
 
 /* On larger screens, display all tabs in a 3-column grid */
@@ -818,10 +1208,7 @@ input[type="number"]:focus {
         grid-template-columns: repeat(3, 1fr);
         gap: 0 16px;
         height: calc(100vh - 60px);
-    }
-
-    .bottom-nav {
-        display: none;
+        padding: 0 15px;
     }
 
     #status_log {
@@ -835,6 +1222,14 @@ input[type="number"]:focus {
         bottom: 0;
     }
 
+    .bottom-nav .tab-button {
+        display: none;
+    }
+
+    .bottom-nav {
+        border-top: 0;
+    }
+
     /* Show all tabs in grid layout */
     .tab-content {
         display: flex !important; /* Always display tab-content */
@@ -846,4 +1241,45 @@ input[type="number"]:focus {
         overflow-x: hidden;
         position: relative;
     }
+
+    body.playing .app {
+        padding: 15px 0 150px 15px;
+        margin-bottom: -140px;
+    }
+
+    body.playing .bottom-nav {
+        height: 140px;
+    }
+
+    body:not(.playing) .bottom-nav {
+        display: none;
+    }
+
+    body.playing #currently-playing-container.open {
+        position: absolute;
+        bottom: 0;
+    }
+
+    body.playing #currently-playing-container:not(.open) #currently-playing-details {
+        flex-direction: row;
+        align-items: center;
+    }
+
+    #currently-playing-container h3 {
+        flex-grow: 1;
+    }
+
+    body.playing #currently-playing-container.open .header {
+        display: none;
+    }
+
+    #open-settings-button span {
+        opacity: 0;
+        transition: var(--transition-medium) opacity;
+    }
+
+    #open-settings-button:hover span {
+        opacity: 1;
+    }
+
 }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 5 - 0
static/fontawesome.min.css


+ 1 - 0
static/icons/chevron-down.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>

+ 1 - 0
static/icons/chevron-left.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>

+ 1 - 0
static/icons/chevron-right.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/></svg>

+ 1 - 0
static/icons/chevron-up.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>

+ 1 - 0
static/icons/pause.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M48 64C21.5 64 0 85.5 0 112L0 400c0 26.5 21.5 48 48 48l32 0c26.5 0 48-21.5 48-48l0-288c0-26.5-21.5-48-48-48L48 64zm192 0c-26.5 0-48 21.5-48 48l0 288c0 26.5 21.5 48 48 48l32 0c26.5 0 48-21.5 48-48l0-288c0-26.5-21.5-48-48-48l-32 0z"/></svg>

+ 1 - 0
static/icons/play.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80L0 432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>

+ 575 - 63
static/main.js → static/js/main.js

@@ -14,7 +14,7 @@ const LOG_TYPE = {
 };
 
 // Enhanced logMessage with notification system
-function logMessage(message, type = LOG_TYPE.DEBUG) {
+function logMessage(message, type = LOG_TYPE.DEBUG, clickTargetId = null) {
     const log = document.getElementById('status_log');
     const header = document.querySelector('header');
 
@@ -49,14 +49,46 @@ function logMessage(message, type = LOG_TYPE.DEBUG) {
 
     // Add a close button
     const closeButton = document.createElement('button');
-    closeButton.textContent = '×';
-    closeButton.className = 'close-button';
-    closeButton.onclick = () => {
+    closeButton.innerHTML = '<i class="fa-solid fa-xmark"></i>';
+    closeButton.className = 'close-button no-bg';
+    closeButton.onclick = (e) => {
+        e.stopPropagation(); // Prevent triggering the clickTarget when the close button is clicked
         notification.classList.remove('show');
         setTimeout(() => notification.remove(), 250); // Match transition duration
     };
     notification.appendChild(closeButton);
 
+    // Attach click event to the notification if a clickTargetId is provided
+    if (clickTargetId) {
+        notification.onclick = () => {
+            const target = document.getElementById(clickTargetId);
+            if (target) {
+                // Find the closest <main> parent
+                const parentMain = target.closest('main');
+                if (parentMain) {
+                    // Remove 'active' class from all <main> elements
+                    document.querySelectorAll('main').forEach((main) => {
+                        main.classList.remove('active');
+                    });
+                    // Add 'active' class to the parent <main>
+                    parentMain.classList.add('active');
+                    target.click();
+
+                    // Update tab buttons based on the parent <main> ID
+                    const parentId = parentMain.id; // e.g., "patterns-tab"
+                    const tabId = `nav-${parentId.replace('-tab', '')}`; // e.g., "nav-patterns"
+                    document.querySelectorAll('.tab-button').forEach((button) => {
+                        button.classList.remove('active');
+                    });
+                    const tabButton = document.getElementById(tabId);
+                    if (tabButton) {
+                        tabButton.classList.add('active');
+                    }
+                }
+            }
+        };
+    }
+
     // Append the notification to the header
     header.appendChild(notification);
 
@@ -213,7 +245,7 @@ async function runThetaRho() {
     }
 
     // Get the selected pre-execution action
-    const preExecutionAction = document.querySelector('input[name="pre_execution"]:checked').value;
+    const preExecutionAction = document.getElementById('pre_execution').value;
 
     logMessage(`Running file: ${selectedFile} with pre-execution action: ${preExecutionAction}...`);
     const response = await fetch('/run_theta_rho', {
@@ -241,6 +273,36 @@ async function stopExecution() {
     }
 }
 
+let isPaused = false;
+
+function togglePausePlay() {
+    const button = document.getElementById("pausePlayCurrent");
+
+    if (isPaused) {
+        // Resume execution
+        fetch('/resume_execution', { method: 'POST' })
+            .then(response => response.json())
+            .then(data => {
+                if (data.success) {
+                    isPaused = false;
+                    button.innerHTML = "<i class=\"fa-solid fa-pause\"></i>"; // 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 = "<i class=\"fa-solid fa-play\"></i>"; // 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);
@@ -291,7 +353,7 @@ async function removeCustomPattern(fileName) {
 }
 
 // Preview a Theta-Rho file
-async function previewPattern(fileName) {
+async function previewPattern(fileName, containerId = 'pattern-preview-container') {
     try {
         logMessage(`Fetching data to preview file: ${fileName}...`);
         const response = await fetch('/preview_thr', {
@@ -303,44 +365,65 @@ async function previewPattern(fileName) {
         const result = await response.json();
         if (result.success) {
             const coordinates = result.coordinates;
-            renderPattern(coordinates);
+
+            // Render the pattern in the specified container
+            const canvasId = containerId === 'currently-playing-container'
+                ? 'currentlyPlayingCanvas'
+                : 'patternPreviewCanvas';
+            renderPattern(coordinates, canvasId);
 
             // Update coordinate display
-            const firstCoord = coordinates[0];
-            const lastCoord = coordinates[coordinates.length - 1];
-            document.getElementById('first_coordinate').textContent = `First Coordinate: θ=${firstCoord[0]}, ρ=${firstCoord[1]}`;
-            document.getElementById('last_coordinate').textContent = `Last Coordinate: θ=${lastCoord[0]}, ρ=${lastCoord[1]}`;
+            const firstCoordElement = document.getElementById('first_coordinate');
+            const lastCoordElement = document.getElementById('last_coordinate');
+
+            if (firstCoordElement) {
+                const firstCoord = coordinates[0];
+                firstCoordElement.textContent = `First Coordinate: θ=${firstCoord[0]}, ρ=${firstCoord[1]}`;
+            } else {
+                logMessage('First coordinate element not found.', LOG_TYPE.WARNING);
+            }
+
+            if (lastCoordElement) {
+                const lastCoord = coordinates[coordinates.length - 1];
+                lastCoordElement.textContent = `Last Coordinate: θ=${lastCoord[0]}, ρ=${lastCoord[1]}`;
+            } else {
+                logMessage('Last coordinate element not found.', LOG_TYPE.WARNING);
+            }
 
             // Show the preview container
-            const previewContainer = document.getElementById('pattern-preview-container');
+            const previewContainer = document.getElementById(containerId);
             if (previewContainer) {
                 previewContainer.classList.remove('hidden');
                 previewContainer.classList.add('visible');
+            } else {
+                logMessage(`Preview container not found: ${containerId}`, LOG_TYPE.ERROR);
             }
-
-            // Close the "Add to Playlist" container if it is open
-            const addToPlaylistContainer = document.getElementById('add-to-playlist-container');
-            if (addToPlaylistContainer && !addToPlaylistContainer.classList.contains('hidden')) {
-                toggleSecondaryButtons('add-to-playlist-container'); // Hide the container
-            }
-
         } else {
             logMessage(`Failed to fetch preview for file: ${fileName}`, LOG_TYPE.WARNING);
         }
     } catch (error) {
-        logMessage(`Error previewing pattern: ${error.message}`, LOG_TYPE.WARNING);
+        logMessage(`Error previewing pattern: ${error.message}`, LOG_TYPE.ERROR);
     }
 }
 
 // Render the pattern on a canvas
-function renderPattern(coordinates) {
-    const canvas = document.getElementById('patternPreviewCanvas');
+function renderPattern(coordinates, canvasId) {
+    const canvas = document.getElementById(canvasId);
     if (!canvas) {
-        logMessage('Error: Canvas not found');
+        logMessage(`Canvas element not found: ${canvasId}`, LOG_TYPE.ERROR);
+        return;
+    }
+
+    if (!(canvas instanceof HTMLCanvasElement)) {
+        logMessage(`Element with ID "${canvasId}" is not a canvas.`, LOG_TYPE.ERROR);
         return;
     }
 
     const ctx = canvas.getContext('2d');
+    if (!ctx) {
+        logMessage(`Could not get 2D context for canvas: ${canvasId}`, LOG_TYPE.ERROR);
+        return;
+    }
 
     // Account for device pixel ratio
     const dpr = window.devicePixelRatio || 1;
@@ -367,7 +450,6 @@ function renderPattern(coordinates) {
         else ctx.lineTo(x, y);
     });
     ctx.stroke();
-    logMessage('Pattern preview rendered.');
 }
 
 
@@ -435,6 +517,37 @@ async function runClearOut() {
     await runFile('clear_from_out.thr');
 }
 
+async function runClearSide() {
+    await runFile('clear_sideway.thr');
+}
+
+let scrollPosition = 0;
+
+function scrollSelection(direction) {
+    const container = document.getElementById('clear_selection');
+    const itemHeight = 50; // Adjust based on CSS height
+    const maxScroll = container.children.length - 1;
+
+    // Update scroll position
+    scrollPosition += direction;
+    scrollPosition = Math.max(0, Math.min(scrollPosition, maxScroll));
+
+    // Update the transform to scroll items
+    container.style.transform = `translateY(-${scrollPosition * itemHeight}px)`;
+    setCookie('clear_action_index', scrollPosition, 365);
+}
+
+function executeClearAction(actionFunction) {
+    // Save the new action to a cookie (optional)
+    setCookie('clear_action', actionFunction, 365);
+
+    if (actionFunction && typeof window[actionFunction] === 'function') {
+        window[actionFunction](); // Execute the selected clear action
+    } else {
+        logMessage('No clear action selected or function not found.', LOG_TYPE.ERROR);
+    }
+}
+
 async function runFile(fileName) {
     const response = await fetch(`/run_theta_rho_file/${fileName}`, { method: 'POST' });
     const result = await response.json();
@@ -452,6 +565,7 @@ async function checkSerialStatus() {
     const statusElement = document.getElementById('serial_status');
     const statusHeaderElement = document.getElementById('serial_status_header');
     const serialPortsContainer = document.getElementById('serial_ports_container');
+    const selectElement = document.getElementById('serial_ports');
 
     const connectButton = document.querySelector('button[onclick="connectSerial()"]');
     const disconnectButton = document.querySelector('button[onclick="disconnectSerial()"]');
@@ -462,7 +576,7 @@ async function checkSerialStatus() {
         statusElement.textContent = `Connected to ${port}`;
         statusElement.classList.add('connected');
         statusElement.classList.remove('not-connected');
-        logMessage(`Reconnected to serial port: ${port}`);
+        logMessage(`Connected to serial port: ${port}`);
 
         // Update header status
         statusHeaderElement.classList.add('connected');
@@ -471,8 +585,15 @@ async function checkSerialStatus() {
         // 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';
+        disconnectButton.style.display = 'flex';
+        restartButton.style.display = 'flex';
+
+        // Preselect the connected port in the dropdown
+        const newOption = document.createElement('option');
+        newOption.value = port;
+        newOption.textContent = port;
+        selectElement.appendChild(newOption);
+        selectElement.value = port;
     } else {
         statusElement.textContent = 'Not connected';
         statusElement.classList.add('not-connected');
@@ -485,7 +606,7 @@ async function checkSerialStatus() {
 
         // Show Available Ports and the connect button
         serialPortsContainer.style.display = 'block';
-        connectButton.style.display = 'inline-block';
+        connectButton.style.display = 'flex';
         disconnectButton.style.display = 'none';
         restartButton.style.display = 'none';
 
@@ -518,6 +639,7 @@ async function connectSerial() {
     const result = await response.json();
     if (result.success) {
         logMessage(`Connected to serial port: ${port}`, LOG_TYPE.SUCCESS);
+
         // Refresh the status
         await checkSerialStatus();
     } else {
@@ -555,6 +677,200 @@ async function restartSerial() {
     }
 }
 
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+//  Firmware / Software Updater
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+async function fetchFirmwareInfo(motorType = null) {
+    const checkButton = document.getElementById("check_updates_button");
+    const motorTypeElement = document.getElementById("motor_type");
+    const currentVersionElement = document.getElementById("current_firmware_version");
+    const newVersionElement = document.getElementById("new_firmware_version");
+    const motorSelectionDiv = document.getElementById("motor_selection");
+    const updateButtonElement = document.getElementById("update_firmware_button");
+
+    try {
+        // Disable the button while fetching
+        checkButton.disabled = true;
+        checkButton.textContent = "Checking...";
+
+        // Prepare fetch options
+        const options = motorType
+            ? {
+                method: "POST",
+                headers: { "Content-Type": "application/json" },
+                body: JSON.stringify({ motorType }),
+            }
+            : { method: "GET" };
+
+        const response = await fetch("/get_firmware_info", options);
+        const data = await response.json();
+        if (data.success) {
+            const { installedVersion, installedType, inoVersion, updateAvailable } = data;
+
+            // Handle unknown motor type
+            if (!installedType || installedType === "Unknown") {
+                motorSelectionDiv.style.display = "flex"; // Show the dropdown
+                updateButtonElement.style.display = "none"; // Hide update button
+                checkButton.style.display = "none";
+            } else {
+                // Display motor type
+                motorTypeElement.textContent = `Type: ${installedType || "Unknown"}`;
+                // Pre-select the correct motor type in the dropdown
+                const motorSelect = document.getElementById("manual_motor_type");
+                if (motorSelect) {
+                    Array.from(motorSelect.options).forEach(option => {
+                        option.selected = option.value === installedType;
+                    });
+                }
+
+                // Display firmware versions
+                currentVersionElement.textContent = `Current version: ${installedVersion || "Unknown"}`;
+
+                if (updateAvailable) {
+                    newVersionElement.textContent = `Latest version: ${inoVersion}`;
+                    updateButtonElement.style.display = "block";
+                    checkButton.style.display = "none";
+                } else {
+                    newVersionElement.textContent = "You are up to date!";
+                    updateButtonElement.style.display = "none";
+                    checkButton.style.display = "none";
+                }
+            }
+        } else {
+            logMessage("Could not fetch firmware info.", LOG_TYPE.WARNING);
+            logMessage(data.error, LOG_TYPE.DEBUG);
+        }
+    } catch (error) {
+        logMessage("Could not fetch firmware info.", LOG_TYPE.WARNING);
+        logMessage(error.message, LOG_TYPE.DEBUG);
+    } finally {
+        // Re-enable the button after fetching
+        checkButton.disabled = false;
+        checkButton.textContent = "Check for Updates";
+    }
+}
+
+function setMotorType() {
+    const selectElement = document.getElementById("manual_motor_type");
+    const selectedMotorType = selectElement.value;
+
+    if (!selectedMotorType) {
+        logMessage("Please select a motor type before proceeding.", LOG_TYPE.WARNING);
+        return;
+    }
+
+    const motorSelectionDiv = document.getElementById("motor_selection");
+    motorSelectionDiv.style.display = "none";
+
+    // Call fetchFirmwareInfo with the selected motor type
+    fetchFirmwareInfo(selectedMotorType);
+}
+
+async function updateFirmware() {
+    const button = document.getElementById("update_firmware_button");
+    const motorTypeDropdown = document.getElementById("manual_motor_type");
+    const motorType = motorTypeDropdown ? motorTypeDropdown.value : null;
+
+    if (!motorType) {
+        logMessage("Motor type is not set. Please select a motor type.", LOG_TYPE.WARNING);
+        return;
+    }
+
+    button.disabled = true;
+    button.textContent = "Updating...";
+
+    try {
+        logMessage("Firmware update started...", LOG_TYPE.INFO);
+
+        const response = await fetch("/flash_firmware", {
+            method: "POST",
+            headers: { "Content-Type": "application/json" },
+            body: JSON.stringify({ motorType }),
+        });
+
+        const data = await response.json();
+        if (data.success) {
+            logMessage("Firmware updated successfully!", LOG_TYPE.SUCCESS);
+            // Refresh the firmware info to update current version
+            logMessage("Refreshing firmware info...");
+            await fetchFirmwareInfo();
+
+            // Display "You're up to date" message if versions match
+            const newVersionElement = document.getElementById("new_firmware_version");
+            const currentVersionElement = document.getElementById("current_firmware_version");
+            currentVersionElement.textContent = newVersionElement.innerHTML
+            newVersionElement.textContent = "You are up to date!";
+            const motorSelectionDiv = document.getElementById("motor_selection");
+            motorSelectionDiv.style.display = "none";
+        } else {
+            logMessage(`Firmware update failed: ${data.error}`, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error during firmware update: ${error.message}`, LOG_TYPE.ERROR);
+    } finally {
+        button.disabled = false; // Re-enable button
+        button.textContent = "Update Firmware";
+    }
+}
+
+async function checkForUpdates() {
+    try {
+        const response = await fetch('/check_software_update');
+        const data = await response.json();
+
+        // Handle updates available logic
+        if (data.updates_available) {
+            const updateButton = document.getElementById('update-software-btn');
+            const updateLinkElement = document.getElementById('update_link');
+            const tagLink = `https://github.com/tuanchris/dune-weaver/releases/tag/${data.latest_remote_tag}`;
+
+            updateButton.classList.remove('hidden'); // Show the button
+            logMessage("Software Update Available", LOG_TYPE.INFO, 'open-settings-button')
+
+            updateLinkElement.innerHTML = `<a href="${tagLink}" target="_blank">View Release Notes </a>`;
+            updateLinkElement.classList.remove('hidden'); // Show the link
+        }
+
+        // Update current and latest version in the UI
+        const currentVersionElem = document.getElementById('current_git_version');
+        const latestVersionElem = document.getElementById('latest_git_version');
+
+        currentVersionElem.textContent = `Current Version: ${data.latest_local_tag || 'Unknown'}`;
+        latestVersionElem.textContent = data.updates_available
+            ? `Latest Version: ${data.latest_remote_tag}`
+            : 'You are up to date!';
+
+    } catch (error) {
+        console.error('Error checking for updates:', error);
+    }
+}
+
+async function updateSoftware() {
+    const updateButton = document.getElementById('update-software-btn');
+
+    try {
+        // Disable the button and update the text
+        updateButton.disabled = true;
+        updateButton.querySelector('span').textContent = 'Updating...';
+
+        const response = await fetch('/update_software', { method: 'POST' });
+        const data = await response.json();
+
+        if (data.success) {
+            logMessage('Software updated successfully!', LOG_TYPE.SUCCESS);
+            window.location.reload(); // Reload the page after update
+        } else {
+            logMessage('Failed to update software: ' + data.error, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        console.error('Error updating software:', error);
+        logMessage('Failed to update software', LOG_TYPE.ERROR);
+    } finally {
+        // Re-enable the button and reset the text
+        updateButton.disabled = false;
+        updateButton.textContent = 'Update Software'; // Adjust to the original text
+    }
+}
 
 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 //  PART A: Loading / listing playlists from the server
@@ -575,6 +891,15 @@ function displayAllPlaylists(playlists) {
     const ul = document.getElementById('all_playlists');
     ul.innerHTML = ''; // Clear current list
 
+    if (playlists.length === 0) {
+        // Add a placeholder if the list is empty
+        const emptyLi = document.createElement('li');
+        emptyLi.textContent = "You don't have any playlists yet.";
+        emptyLi.classList.add('empty-placeholder'); // Optional: Add a class for styling
+        ul.appendChild(emptyLi);
+        return;
+    }
+
     playlists.forEach(playlistName => {
         const li = document.createElement('li');
         li.textContent = playlistName;
@@ -624,6 +949,13 @@ function openPlaylistEditor(playlistName) {
     loadPlaylist(playlistName);
 }
 
+function clearSchedule() {
+    document.getElementById("start_time").value = "";
+    document.getElementById("end_time").value = "";
+    document.getElementById('clear_time').style.display = 'none';
+    setCookie('start_time', '', 365);
+    setCookie('end_time', '', 365);
+}
 
 // Function to run the selected playlist with specified parameters
 async function runPlaylist() {
@@ -638,6 +970,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) {
@@ -645,6 +979,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 {
@@ -656,7 +1021,9 @@ async function runPlaylist() {
                 pause_time: pauseTime,
                 clear_pattern: clearPatternSelect,
                 run_mode: runMode,
-                shuffle: shuffle
+                shuffle: shuffle,
+                start_time: startTimeInput,
+                end_time: endTimeInput
             })
         });
 
@@ -682,10 +1049,6 @@ async function loadPlaylist(playlistName) {
         logMessage(`Loading playlist: ${playlistName}`);
         const response = await fetch(`/get_playlist?name=${encodeURIComponent(playlistName)}`);
 
-        if (!response.ok) {
-            throw new Error(`HTTP error! Status: ${response.status}`);
-        }
-
         const data = await response.json();
 
         if (!data.name) {
@@ -766,6 +1129,18 @@ function populatePlaylistDropdown() {
             // Retrieve the saved playlist from the cookie
             const savedPlaylist = getCookie('selected_playlist');
 
+            // Check if there are playlists available
+            if (playlists.length === 0) {
+                // Add a placeholder option if no playlists are available
+                const placeholderOption = document.createElement('option');
+                placeholderOption.value = '';
+                placeholderOption.textContent = 'No playlists available';
+                placeholderOption.disabled = true; // Prevent selection
+                placeholderOption.selected = true; // Set as default
+                select.appendChild(placeholderOption);
+                return;
+            }
+
             playlists.forEach(playlist => {
                 const option = document.createElement('option');
                 option.value = playlist;
@@ -782,13 +1157,13 @@ function populatePlaylistDropdown() {
             // Attach the onchange event listener after populating the dropdown
             select.addEventListener('change', function () {
                 const selectedPlaylist = this.value;
-                setCookie('selected_playlist', selectedPlaylist, 7); // Save to cookie
+                setCookie('selected_playlist', selectedPlaylist, 365); // Save to cookie
                 logMessage(`Selected playlist saved: ${selectedPlaylist}`);
             });
 
             logMessage('Playlist dropdown populated, event listener attached, and saved playlist restored.');
         })
-        .catch(error => logMessage(`Error fetching playlists: ${error.message}`, LOG_TYPE.ERROR));
+        .catch(error => logMessage(`Error fetching playlists: ${error.message}`));
 }
 populatePlaylistDropdown().then(() => {
     loadSettingsFromCookies(); // Restore selected playlist after populating the dropdown
@@ -824,6 +1199,7 @@ async function confirmAddPlaylist() {
 
             // Refresh the playlist list
             loadAllPlaylists();
+            populatePlaylistDropdown();
 
             // Hide the add playlist container
             toggleSecondaryButtons('add-playlist-container');
@@ -916,6 +1292,7 @@ async function deleteCurrentPlaylist() {
             logMessage(`Playlist "${playlistName}" deleted.`, LOG_TYPE.INFO);
             closeStickySection('playlist-editor');
             loadAllPlaylists();
+            populatePlaylistDropdown();
         } else {
             logMessage(`Failed to delete playlist: ${result.error}`,  LOG_TYPE.ERROR);
         }
@@ -957,7 +1334,7 @@ function refreshPlaylistUI() {
 
         // Move Up button
         const moveUpBtn = document.createElement('button');
-        moveUpBtn.textContent = '▲'; // Up arrow symbol
+        moveUpBtn.innerHTML = '<i class="fa-solid fa-turn-up"></i>'; // Up arrow symbol
         moveUpBtn.classList.add('move-button');
         moveUpBtn.onclick = () => {
             if (index > 0) {
@@ -972,7 +1349,7 @@ function refreshPlaylistUI() {
 
         // Move Down button
         const moveDownBtn = document.createElement('button');
-        moveDownBtn.textContent = '▼'; // Down arrow symbol
+        moveDownBtn.innerHTML = '<i class="fa-solid fa-turn-down"></i>'; // Down arrow symbol
         moveDownBtn.classList.add('move-button');
         moveDownBtn.onclick = () => {
             if (index < playlist.length - 1) {
@@ -987,7 +1364,7 @@ function refreshPlaylistUI() {
 
         // Remove button
         const removeBtn = document.createElement('button');
-        removeBtn.textContent = '✖';
+        removeBtn.innerHTML = '<i class="fa-solid fa-trash"></i>';
         removeBtn.classList.add('remove-button');
         removeBtn.onclick = () => {
             playlist.splice(index, 1);
@@ -1006,12 +1383,12 @@ function toggleSaveCancelButtons(show) {
     if (actionButtons) {
         // Show/hide all buttons except Save and Cancel
         actionButtons.querySelectorAll('button:not(.save-cancel)').forEach(button => {
-            button.style.display = show ? 'none' : 'inline-block';
+            button.style.display = show ? 'none' : 'flex';
         });
 
         // Show/hide Save and Cancel buttons
         actionButtons.querySelectorAll('.save-cancel').forEach(button => {
-            button.style.display = show ? 'inline-block' : 'none';
+            button.style.display = show ? 'flex' : 'none';
         });
     } else {
         logMessage('Error: Action buttons container not found.', LOG_TYPE.ERROR);
@@ -1134,7 +1511,7 @@ function closeStickySection(sectionId) {
         // Reset the fullscreen button text if it exists
         const fullscreenButton = section.querySelector('.fullscreen-button');
         if (fullscreenButton) {
-            fullscreenButton.textContent = '⛶'; // Reset to enter fullscreen icon/text
+            fullscreenButton.innerHtml = '<i class="fa-solid fa-compress"></i>'; // Reset to enter fullscreen icon/text
         }
 
         logMessage(`Closed section: ${sectionId}`);
@@ -1156,19 +1533,45 @@ function closeStickySection(sectionId) {
     }
 }
 
+// Function to open any sticky section
+function openStickySection(sectionId) {
+    const section = document.getElementById(sectionId);
+    if (section) {
+        // Toggle the 'open' class
+        section.classList.toggle('open');
+    } else {
+        logMessage(`Error: Section with ID "${sectionId}" not found`);
+    }
+}
+
 function attachFullScreenListeners() {
     // Add event listener to all fullscreen buttons
     document.querySelectorAll('.fullscreen-button').forEach(button => {
         button.addEventListener('click', function () {
             const stickySection = this.closest('.sticky'); // Find the closest sticky section
             if (stickySection) {
+                // Close all other sections
+                document.querySelectorAll('.sticky:not(#currently-playing-container)').forEach(section => {
+                    if (section !== stickySection) {
+                        section.classList.remove('fullscreen');
+                        section.classList.remove('visible');
+                        section.classList.add('hidden');
+
+                        // Reset the fullscreen button text for other sections
+                        const otherFullscreenButton = section.querySelector('.fullscreen-button');
+                        if (otherFullscreenButton) {
+                            otherFullscreenButton.innerHTML = '<i class="fa-solid fa-expand"></i>'; // Enter fullscreen icon/text
+                        }
+                    }
+                });
+
                 stickySection.classList.toggle('fullscreen'); // Toggle fullscreen class
 
                 // Update button icon or text
                 if (stickySection.classList.contains('fullscreen')) {
-                    this.textContent = '-'; // Exit fullscreen icon/text
+                    this.innerHTML = '<i class="fa-solid fa-compress"></i>'; // Exit fullscreen icon/text
                 } else {
-                    this.textContent = '⛶'; // Enter fullscreen icon/text
+                    this.innerHTML = '<i class="fa-solid fa-expand"></i>'; // Enter fullscreen icon/text
                 }
             } else {
                 console.error('Error: Fullscreen button is not inside a sticky section.');
@@ -1177,6 +1580,97 @@ function attachFullScreenListeners() {
     });
 }
 
+let lastPreviewedFile = null; // Track the last previewed file
+
+let updateInterval = null;
+
+async function updateCurrentlyPlaying() {
+    try {
+        if (!document.hasFocus()) return; // Stop execution if the page is not visible
+
+        const response = await fetch('/status');
+        const data = await response.json();
+
+        const currentlyPlayingSection = document.getElementById('currently-playing-container');
+        if (!currentlyPlayingSection) {
+            logMessage('Currently Playing section not found.', LOG_TYPE.ERROR);
+            return;
+        }
+
+        if (data.current_playing_file && !data.stop_requested) {
+            const { current_playing_file, execution_progress, pause_requested } = data;
+
+            // Strip './patterns/' prefix from the file name
+            const fileName = current_playing_file.replace('./patterns/', '');
+
+            if (!document.body.classList.contains('playing')) {
+                closeStickySection('pattern-preview-container')
+            }
+
+            // Show "Currently Playing" section
+            document.body.classList.add('playing');
+
+            // Update pattern preview only if the file is different
+            if (current_playing_file !== lastPreviewedFile) {
+                previewPattern(fileName, 'currently-playing-container');
+                lastPreviewedFile = current_playing_file;
+            }
+
+            // Update the filename display
+            const fileNameDisplay = document.getElementById('currently-playing-file');
+            if (fileNameDisplay) fileNameDisplay.textContent = fileName;
+
+            // Update progress bar
+            const progressBar = document.getElementById('play_progress');
+            const progressText = document.getElementById('play_progress_text');
+            if (execution_progress) {
+                const progressPercentage = (execution_progress[0] / execution_progress[1]) * 100;
+                progressBar.value = progressPercentage;
+                progressText.textContent = `${Math.round(progressPercentage)}% (${formatSecondsToHMS(execution_progress[2])})`;
+            } else {
+                progressBar.value = 0;
+                progressText.textContent = '0%';
+            }
+
+            // Update play/pause button
+            const pausePlayButton = document.getElementById('pausePlayCurrent');
+            if (pausePlayButton) pausePlayButton.innerHTML = pause_requested ? '<i class="fa-solid fa-play"></i>' : '<i class="fa-solid fa-pause"></i>';
+        } else {
+            document.body.classList.remove('playing');
+        }
+    } catch (error) {
+        logMessage(`Error updating "Currently Playing" section: ${error.message}`);
+    }
+}
+
+function formatSecondsToHMS(seconds) {
+    const hrs = Math.floor(seconds / 3600);
+    const mins = Math.floor((seconds % 3600) / 60);
+    const secs = Math.floor(seconds % 60);
+    return `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+}
+
+// Function to start or stop updates based on visibility
+function handleVisibilityChange() {
+    if (document.hasFocus()) {
+        // User is active, start updating
+        if (!updateInterval) {
+            updateCurrentlyPlaying(); // Run immediately
+            updateInterval = setInterval(updateCurrentlyPlaying, 5000); // Update every 5s
+        }
+    } else {
+        // User is inactive, stop updating
+        clearInterval(updateInterval);
+        updateInterval = null;
+    }
+}
+
+function toggleSettings() {
+    const settingsContainer = document.getElementById('settings-container');
+    if (settingsContainer) {
+        settingsContainer.classList.toggle('open');
+    }
+}
 
 // Utility function to manage cookies
 function setCookie(name, value, days) {
@@ -1197,28 +1691,33 @@ function getCookie(name) {
     return null;
 }
 
-
 // Save settings to cookies
 function saveSettingsToCookies() {
     // Save the pause time
     const pauseTime = document.getElementById('pause_time').value;
-    setCookie('pause_time', pauseTime, 7);
+    setCookie('pause_time', pauseTime, 365);
 
     // Save the clear pattern
     const clearPattern = document.getElementById('clear_pattern').value;
-    setCookie('clear_pattern', clearPattern, 7);
+    setCookie('clear_pattern', clearPattern, 365);
 
     // Save the run mode
     const runMode = document.querySelector('input[name="run_mode"]:checked').value;
-    setCookie('run_mode', runMode, 7);
+    setCookie('run_mode', runMode, 365);
 
     // Save shuffle playlist checkbox state
     const shufflePlaylist = document.getElementById('shuffle_playlist').checked;
-    setCookie('shuffle_playlist', shufflePlaylist, 7);
+    setCookie('shuffle_playlist', shufflePlaylist, 365);
 
     // Save pre-execution action
-    const preExecution = document.querySelector('input[name="pre_execution"]:checked').value;
-    setCookie('pre_execution', preExecution, 7);
+    const preExecution = document.getElementById('pre_execution').value;
+    setCookie('pre_execution', preExecution, 365);
+
+    // Save start and end times
+    const startTime = document.getElementById('start_time').value;
+    const endTime = document.getElementById('end_time').value;
+    setCookie('start_time', startTime, 365);
+    setCookie('end_time', endTime, 365);
 
     logMessage('Settings saved.');
 }
@@ -1252,16 +1751,21 @@ function loadSettingsFromCookies() {
     // Load the pre-execution action
     const preExecution = getCookie('pre_execution');
     if (preExecution !== null) {
-        document.querySelector(`input[name="pre_execution"][value="${preExecution}"]`).checked = true;
+        document.getElementById('pre_execution').value = preExecution;
     }
 
-    // Load the selected playlist
-    const selectedPlaylist = getCookie('selected_playlist');
-    if (selectedPlaylist !== null) {
-        const playlistDropdown = document.getElementById('select-playlist');
-        if (playlistDropdown && [...playlistDropdown.options].some(option => option.value === selectedPlaylist)) {
-            playlistDropdown.value = selectedPlaylist;
-        }
+    // Load start and end times
+    const startTime = getCookie('start_time');
+    if (startTime !== null) {
+        document.getElementById('start_time').value = startTime;
+    }
+    const endTime = getCookie('end_time');
+    if (endTime !== null) {
+        document.getElementById('end_time').value = endTime;
+    }
+
+    if (startTime && endTime ) {
+        document.getElementById('clear_time').style.display = 'block';
     }
 
     logMessage('Settings loaded from cookies.');
@@ -1276,16 +1780,16 @@ function attachSettingsSaveListeners() {
         input.addEventListener('change', saveSettingsToCookies);
     });
     document.getElementById('shuffle_playlist').addEventListener('change', saveSettingsToCookies);
-    document.querySelectorAll('input[name="pre_execution"]').forEach(input => {
-        input.addEventListener('change', saveSettingsToCookies);
-    });
+    document.getElementById('pre_execution').addEventListener('change', saveSettingsToCookies);
+    document.getElementById('start_time').addEventListener('change', saveSettingsToCookies);
+    document.getElementById('end_time').addEventListener('change', saveSettingsToCookies);
 }
 
 
 // Tab switching logic with cookie storage
 function switchTab(tabName) {
     // Store the active tab in a cookie
-    setCookie('activeTab', tabName, 7); // Store for 7 days
+    setCookie('activeTab', tabName, 365); // Store for 7 days
 
     // Deactivate all tab content
     document.querySelectorAll('.tab-content').forEach(tab => {
@@ -1314,6 +1818,8 @@ function switchTab(tabName) {
     }
 }
 
+document.addEventListener("visibilitychange", handleVisibilityChange);
+
 // Initialization
 document.addEventListener('DOMContentLoaded', () => {
     const activeTab = getCookie('activeTab') || 'patterns'; // Default to 'patterns' tab
@@ -1321,7 +1827,13 @@ document.addEventListener('DOMContentLoaded', () => {
     checkSerialStatus(); // Check serial connection status
     loadThetaRhoFiles(); // Load files on page load
     loadAllPlaylists(); // Load all playlists on page load
-    loadSettingsFromCookies(); // Load saved settings
     attachSettingsSaveListeners(); // Attach event listeners to save changes
     attachFullScreenListeners();
+
+    // Periodically check for currently playing status
+    if (document.hasFocus()) {
+        updateInterval = setInterval(updateCurrentlyPlaying, 5000);
+    }
+    checkForUpdates();
+    fetchFirmwareInfo();
 });

BIN
static/webfonts/Roboto-Italic-VariableFont_wdth,wght.ttf


BIN
static/webfonts/Roboto-VariableFont_wdth,wght.ttf


BIN
static/webfonts/fa-regular-400.ttf


BIN
static/webfonts/fa-regular-400.woff2


BIN
static/webfonts/fa-solid-900.ttf


BIN
static/webfonts/fa-solid-900.woff2


+ 252 - 79
templates/index.html

@@ -5,7 +5,8 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
     <title>Dune Weaver Controller</title>
     <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
-    <link rel="stylesheet" href="../static/style.css">
+    <link rel="stylesheet" href="../static/css/style.css">
+    <link rel="stylesheet" href="../static/css/all.min.css">
 </head>
 <body>
 <header>
@@ -17,14 +18,22 @@
         <section class="main">
             <div class="header">
                 <h2>Patterns</h2>
-                <button class="add-button cta" onclick="toggleSecondaryButtons('add-pattern-container')">+</button>
+                <button class="add-button cta" onclick="toggleSecondaryButtons('add-pattern-container')">
+                    <i class="fa-solid fa-plus"></i>
+                </button>
             </div>
             <div id="add-pattern-container" class="add-to-container hidden">
                 <div class="action-buttons">
                     <label for="upload_file">Upload pattern file (.thr):</label>
                     <input type="file" id="upload_file">
-                    <button class="cancel" onclick="toggleSecondaryButtons('add-pattern-container')">Cancel</button>
-                    <button class="cta" onclick="uploadThetaRho()">Upload</button>
+                    <button class="cta" onclick="uploadThetaRho()">
+                        <i class="fa-solid fa-file-arrow-up"></i>
+                        <span>Upload</span>
+                    </button>
+                    <button class="cancel" onclick="toggleSecondaryButtons('add-pattern-container')">
+                        <i class="fa-solid fa-xmark"></i>
+                        <span>Cancel</span>
+                    </button>
                 </div>
             </div>
             <input type="text" id="search_pattern" placeholder="Search files..." oninput="searchPatternFiles()">
@@ -35,8 +44,12 @@
         <section id="pattern-preview-container" class="sticky hidden">
             <div class="header">
                 <h2>Preview</h2>
-                <button class="fullscreen-button">⛶</button>
-                <button class="close-button" onclick="closeStickySection('pattern-preview-container')">&times;</button>
+                <button class="fullscreen-button no-bg">
+                    <i class="fa-solid fa-expand"></i>
+                </button>
+                <button class="close-button no-bg" onclick="closeStickySection('pattern-preview-container')">
+                    <i class="fa-solid fa-xmark"></i>
+                </button>
             </div>
             <div id="pattern-preview">
                 <canvas id="patternPreviewCanvas"></canvas>
@@ -46,18 +59,45 @@
 
             <!-- Action Buttons -->
             <div class="action-buttons">
-                <button onclick="runThetaRho()" class="cta" >Run</button>
-                <button id="toggle-playlist-button" onclick="toggleSecondaryButtons('add-to-playlist-container', populatePlaylistDropdown)">Add to Playlist</button>
-                <button onclick="removeCurrentPattern()" class="cancel remove-button hidden">Delete</button>
+                <div class="pre-execution">
+                    <h3>Pre-Execution Action</h3>
+                    <div class="control-group">
+                        <select id="pre_execution" name="pre_execution">
+                            <option value="none" selected>None</option>
+                            <option value="adaptive">Adaptive</option>
+                            <option value="clear_from_in">Clear From Center</option>
+                            <option value="clear_from_out">Clear From Perimeter</option>
+                            <option value="clear_sideway">Clear Sideway</option>
+                        </select>
+                    </div>
+                </div>
+                <button onclick="runThetaRho()" class="cta" >
+                    <i class="fa-solid fa-play"></i>
+                    <span>Play</span>
+                </button>
+                <button id="toggle-playlist-button" onclick="toggleSecondaryButtons('add-to-playlist-container', populatePlaylistDropdown)">
+                    <i class="fa-solid fa-list-ul"></i>
+                    <span class="small">Add to Playlist</span>
+                </button>
+                <button onclick="removeCurrentPattern()" class="cancel remove-button hidden">
+                    <i class="fa-solid fa-trash"></i>
+                    <span class="small">Delete</span>
+                </button>
             </div>
 
             <!-- Add to Playlist Section -->
             <div id="add-to-playlist-container" class="hidden">
-                <h2>Select Playlist</h2>
+                <h3>Select Playlist</h3>
                 <select id="select-playlist"></select>
                 <div class="action-buttons">
-                    <button onclick="toggleSecondaryButtons('add-to-playlist-container')" class="cancel">Cancel</button>
-                    <button onclick="saveToPlaylist()" class="cta">Save</button>
+                    <button onclick="saveToPlaylist()" class="cta">
+                        <i class="fa-solid fa-floppy-disk"></i>
+                        <span>Save</span>
+                    </button>
+                    <button onclick="toggleSecondaryButtons('add-to-playlist-container')" class="cancel">
+                        <i class="fa-solid fa-xmark"></i>
+                        <span class="small">Cancel</span>
+                    </button>
                 </div>
             </div>
         </section>
@@ -68,13 +108,21 @@
         <section>
             <div class="header">
                 <h2>Playlists</h2>
-                <button class="cta add-button" onclick="toggleSecondaryButtons('add-playlist-container')">+</button>
+                <button class="cta add-button" onclick="toggleSecondaryButtons('add-playlist-container')">
+                    <i class="fa-solid fa-plus"></i>
+                </button>
             </div>
             <div id="add-playlist-container" class="add-to-container hidden">
                 <input type="text" id="new_playlist_name" placeholder="Enter new playlist name" />
                 <div class="action-buttons">
-                    <button onclick="confirmAddPlaylist()" class="cta">Save</button>
-                    <button onclick="toggleSecondaryButtons('add-playlist-container')" class="cancel">Cancel</button>
+                    <button onclick="confirmAddPlaylist()" class="cta">
+                        <i class="fa-solid fa-floppy-disk"></i>
+                        <span>Save</span>
+                    </button>
+                    <button onclick="toggleSecondaryButtons('add-playlist-container')" class="cancel">
+                        <i class="fa-solid fa-xmark"></i>
+                        <span>Cancel</span>
+                    </button>
                 </div>
             </div>
             <ul id="all_playlists" class="file-list">
@@ -114,13 +162,28 @@
                         <label for="clear_pattern">Clear Pattern:</label>
                         <select id="clear_pattern">
                             <option value="none">None</option>
-                            <option value="clear_in">Clear from In</option>
-                            <option value="clear_out">Clear from Out</option>
-                            <option value="clear_sideway">Clear Sideway</option>
+                            <option value="adaptive">Adaptive</option>
+                            <option value="clear_from_in">Clear From Center</option>
+                            <option value="clear_from_out">Clear From Perimeter</option>
+                            <option value="clear_sideway">Clear Sideways</option>
                             <option value="random">Random</option>
                         </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>
+                    <button id="clear_time" onclick="clearSchedule()" style="display: none" class="small cancel">
+                        <i class="fa-solid fa-delete-left"></i>
+                    </button>
+                </div>
             </div>
         </section>
 
@@ -128,26 +191,51 @@
         <section id="playlist-editor" class="sticky hidden">
             <div class="header">
                 <h2 id="playlist_title">Playlist: <span id="playlist_name_display"></span></h2>
-                <button class="fullscreen-button" onclick="toggleFullscreen(this)">⛶</button>
-                <button class="close-button" onclick="closeStickySection('playlist-editor')">&times;</button>
+                <button class="fullscreen-button no-bg">
+                    <i class="fa-solid fa-expand"></i>
+                </button>
+                <button class="close-button no-bg" onclick="closeStickySection('playlist-editor')">
+                    <i class="fa-solid fa-xmark" ></i>
+                </button>
             </div>
             <ul id="playlist_items" class="file-list">
             </ul>
             <hr/>
             <div class="action-buttons">
-                <button onclick="runPlaylist()" class="cta">Play</button>
-                <button onclick="toggleSecondaryButtons('rename-playlist-container')">Rename</button>
-                <button onclick="deleteCurrentPlaylist()" class="cancel">Delete</button>
+                <button onclick="runPlaylist()" class="cta">
+                    <i class="fa-solid fa-play"></i>
+                    <span>Play</span>
+                </button>
+                <button onclick="toggleSecondaryButtons('rename-playlist-container')">
+                    <i class="fa-solid fa-pencil"></i>
+                    <span class="small">Rename</span>
+                </button>
+                <button onclick="deleteCurrentPlaylist()" class="cancel">
+                    <i class="fa-solid fa-trash"></i>
+                    <span class="small">Delete</span>
+                </button>
                 <!-- Save and Cancel buttons -->
-                <button onclick="savePlaylist()" class="save-cancel cta" style="display: none;">Save</button>
-                <button onclick="cancelPlaylistChanges()" class="save-cancel cancel" style="display: none;">Cancel</button>
+                <button onclick="savePlaylist()" class="save-cancel cta" style="display: none;">
+                    <i class="fa-solid fa-floppy-disk"></i>
+                    <span>Save</span>
+                </button>
+                <button onclick="cancelPlaylistChanges()" class="save-cancel cancel" style="display: none;">
+                    <i class="fa-solid fa-xmark"></i>
+                    <span class="small">Cancel</span>
+                </button>
             </div>
             <!-- Playlist Rename Section -->
             <div id="rename-playlist-container" class="hidden">
                 <input type="text" id="playlist_name_input" placeholder="Enter new playlist name">
                 <div class="action-buttons">
-                    <button onclick="confirmRenamePlaylist()" class="cta">Save</button>
-                    <button onclick="toggleSecondaryButtons('rename-playlist-container')" class="cancel">Cancel</button>
+                    <button onclick="confirmRenamePlaylist()" class="cta">
+                        <i class="fa-solid fa-floppy-disk"></i>
+                        <span>Save</span>
+                    </button>
+                    <button onclick="toggleSecondaryButtons('rename-playlist-container')" class="cancel">
+                        <i class="fa-solid fa-xmark"></i>
+                        <span>Cancel</span>
+                    </button>
                 </div>
             </div>
         </section>
@@ -155,33 +243,32 @@
 
     <!-- Device / Settings Tab -->
     <main class="tab-content" id="settings-tab">
-        <section>
-            <div class="header">
-                <h2>Serial Connection</h2>
-            </div>
-            <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()" class="cta">Connect</button>
-            </div>
-            <div id="serial_ports_buttons" class="button-group">
-                <button onclick="disconnectSerial()" class="cancel">Disconnect</button>
-                <button onclick="restartSerial()" class="warn">Restart</button>
-            </div>
-        </section>
-
         <section class="main">
             <div class="header">
                 <h2>Device Controls</h2>
+                <button id="open-settings-button" class="no-bg" onclick="toggleSettings()">
+                    <i class="fa-solid fa-gears"></i>
+                    <span class="small">Settings</span>
+                </button>
             </div>
-
-            <div class="action-buttons">
-                <button onclick="stopExecution()" class="cancel">Stop Current Pattern</button>
-                <button onclick="runClearIn()">Clear In</button>
-                <button onclick="runClearOut()">Clear Out</button>
-                <button onclick="moveToCenter()">Move to Center</button>
-                <button onclick="moveToPerimeter()">Move to Perimeter</button>
+            <div class="action-buttons square">
+                <button onclick="stopExecution()" class="cancel"><i class="fa-solid fa-stop"></i><span class="small">Stop</span></button>
+                <button onclick="moveToCenter()"><i class="fa-regular fa-circle-dot"></i><span class="small">Move to Center</span></button>
+                <button onclick="moveToPerimeter()"><i class="fa-solid fa-circle-notch"></i><span class="small">Move to Perimeter</span></button>
+                <button onclick="sendHomeCommand()" class="warn"><i class="fa-solid fa-house"></i><span class="small">Home</span></button>
+                <div class="scrollable-selection">
+                    <div class="selection-container">
+                        <div id="clear_selection" class="selection-items">
+                            <div id="runClearIn" class="selection-item" onclick="executeClearAction('runClearIn')">Clear From Center</div>
+                            <div id="runClearOut" class="selection-item" onclick="executeClearAction('runClearOut')">Clear From Perimeter</div>
+                            <div id="runClearSide" class="selection-item" onclick="executeClearAction('runClearSide')">Clear Sideways</div>
+                        </div>
+                        <div class="nav-items">
+                            <button class="scroll-arrow up-arrow" onclick="scrollSelection(-1)">▲</button>
+                            <button class="scroll-arrow down-arrow" onclick="scrollSelection(1)">▼</button>
+                        </div>
+                    </div>
+                </div>
             </div>
             <h3>Send to Coordinate</h3>
             <div class="control-group">
@@ -194,55 +281,141 @@
                     <input type="number" id="rho_input" placeholder="Rho">
                 </div>
                 <div class="item cta">
-                    <button onclick="sendCoordinate()">Send</button>
+                    <button onclick="sendCoordinate()">
+                        <i class="fa-solid fa-map-pin"></i><span class="small">Send</span>
+                    </button>
                 </div>
             </div>
-            <h3>Pre-Execution Action</h3>
-            <div class="control-group">
-                <label class="custom-input">
-                    <input type="radio" name="pre_execution" value="clear_in" id="clear_in">
-                    <span class="custom-radio"></span>
-                    Clear from In
-                </label>
-                <label class="custom-input">
-                    <input type="radio" name="pre_execution" value="clear_out" id="clear_out">
-                    <span class="custom-radio"></span>
-                    Clear from Out
-                </label>
-                <label class="custom-input">
-                    <input type="radio" name="pre_execution" value="none" id="no_action" checked>
-                    <span class="custom-radio"></span>
-                    None
-                </label>
-            </div>
             <div class="control-group">
                 <h3>Speed</h3>
                 <div class="item">
                     <input type="number" id="speed_input" placeholder="1-100" min="1" step="1" max="100">
                 </div>
                 <div class="item cta">
-                    <button class="small-button"  onclick="changeSpeed()">Set Speed</button>
+                    <button class="small-button" onclick="changeSpeed()">
+                        <i class="fa-solid fa-gauge-high"></i>
+                        <span class="small">Set Speed</span>
+                    </button>
                 </div>
             </div>
         </section>
 
-        <section class="debug">
-            <div id="github">
+        <section id="settings-container">
+            <div class="header">
+                <h2>Settings</h2>
+                <button class="close-button no-bg" onclick="toggleSettings()">
+                    <i class="fa-solid fa-xmark"></i>
+                </button>
+            </div>
+            <section>
+                <div class="header">
+                    <h2>Serial Connection</h2>
+                </div>
+                <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()" class="cta">
+                        <i class="fa-solid fa-link"></i>
+                        <span>Connect</span>
+                    </button>
+                </div>
+                <div id="serial_ports_buttons" class="button-group">
+                    <button onclick="disconnectSerial()" class="cancel">
+                        <i class="fa-solid fa-link-slash"></i><span>Disconnect</span></button>
+                    <button onclick="restartSerial()" class="warn">
+                        <i class="fa-solid fa-rotate-left"></i><span>Restart</span></button>
+                </div>
+            </section>
+
+            <section class="software version">
+                <div class="header">
+                    <h2>Software Version</h2>
+                </div>
+                <div id="git_version_info">
+                    <div id="current_git_version">Current Version: Unknown</div>
+                    <div id="latest_git_version">Latest Version: Unknown</div>
+                    <div id="update_link" class="hidden"></div>
+                </div>
+                <div class="button-group">
+                    <button id="update-software-btn" class="hidden cta" onclick="updateSoftware()">
+                        <i class="fa-solid fa-download"></i>
+                        <span>Update Software</span>
+                    </button>
+                </div>
+            </section>
+
+            <section class="firmware version">
+                <div class="header">
+                    <h2>Firmware Version</h2>
+                </div>
+                <div id="firmware_info">
+                    <div id="motor_type"></div>
+                    <div id="current_firmware_version"></div>
+                    <div id="new_firmware_version"></div>
+                </div>
+                <div class="button-group">
+                    <div id="motor_selection" style="display: none;">
+                        <h3>Select installed motor driver: </h3>
+                        <select id="manual_motor_type">
+                            <option value="TMC2209">Arduino TMC2209</option>
+                            <option value="DRV8825">Arduino DRV8825</option>
+                            <option value="esp32">ESP32</option>
+                            <option value="esp32_TMC2209">ESP32 TMC2209</option>
+                        </select>
+                        <button id="set_motor_type_button" onclick="setMotorType()">Set Motor Type</button>
+                    </div>
+                    <button id="check_updates_button" onclick="fetchFirmwareInfo()">Check for Updates</button>
+                    <button id="update_firmware_button" class="cta" style="display: none;" onclick="updateFirmware()">
+                        <i class="fa-solid fa-file-arrow-up"></i>
+                        <span>Update Firmware</span>
+                    </button>
+                </div>
+            </section>
+            <section class="debug main">
+                <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" onclick="toggleDebugLog()">🪲</button>
+                    <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" onclick="toggleDebugLog()">
+                    <i class="fa-solid fa-bug"></i>
+                </button>
+            </section>
         </section>
     </main>
 </div>
 
 <!-- Tab Navigation -->
 <nav class="bottom-nav">
+    <section id="currently-playing-container" class="sticky">
+        <div class="header">
+<!--            <button class="open-button no-bg" onclick="openStickySection('currently-playing-container')">^</button>-->
+        </div>
+        <div id="currently-playing-preview">
+            <canvas id="currentlyPlayingCanvas"></canvas>
+        </div>
+        <div id="currently-playing-details">
+            <h3 id="currently-playing-file"></h3>
+            <p id="currently-playing-position"></p>
+            <div class="play-buttons">
+                <button id="stopCurrent" onclick="stopExecution()" class="cancel">
+                    <i class="fa-solid fa-stop"></i>
+                </button>
+                <button id="pausePlayCurrent" class="cta" onclick="togglePausePlay()">
+                    <i class="fa-solid fa-pause"></i>
+                </button>
+            </div>
+        </div>
+        <div id="progress-container">
+            <progress id="play_progress" value="0" max="100"></progress>
+            <div id="play_progress_text">0%</div>
+        </div>
+    </section>
     <button class="tab-button" onclick="switchTab('patterns')" id="nav-patterns">Patterns</button>
     <button class="tab-button" onclick="switchTab('playlists')" id="nav-playlists">Playlists</button>
     <button class="tab-button" onclick="switchTab('settings')" id="nav-settings">Device</button>
@@ -252,6 +425,6 @@
     <!-- Messages will be appended here -->
 </div>
 
-<script src="../static/main.js"></script>
+<script src="../static/js/main.js"></script>
 </body>
 </html>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác