Просмотр исходного кода

Feature/firmware updater (#34)

* Added Firmware updater with versioning in ino files, endpoint to get current firmware version and a way to flash the firmware

* restore versionnumbers

* Fix for #21. Restarting function is not working

* Removed unneeded logic

* Restored Home button

* Added clear sideways pattern and unified clear options

* Remove options file for now

* ; typo fix

* fix version commit

* Integrated Firmware updater with new way of fetching firmware and motor driver versions

* Added proof of concept of Currently Playing

* Moved and improved Currently Playing

* Various styling improvements, moved font locally and added icon font

* Various styling improvements, moved settings to separate overlay.

* Icon fixes

* Software updater functionality and Updated Changelog

* Improved handling and notification of software update

* Disabled debug

* Installing correct required dependencies via DockerFile

* Minor styling fixes and ux improvements

* Using pre-compiled firmware files

* Added esp32 bin

* Used esp32dev board selection in arduino ide

* fix set_speed, allow for changing speed mid pattern

* add changelog

---------

Co-authored-by: Thokoop <11939567+Thokoop@users.noreply.github.com>
Tuan Nguyen 1 год назад
Родитель
Сommit
ec6781a3ef

+ 15 - 0
CHANGELOG.md

@@ -2,6 +2,21 @@
 
 All notable changes to this project will be documented in this file.
 
+## [1.4.0] Soft- and Firmware updater
+
+### Added
+
+- Added Play/pause button
+- Added schedule hours to run
+- Added playing progress bar
+- Added Software Updater to make it easier to update the app and get the latest firmware updates for the Arduino
+- Added Firmware updater to make it possible to flash the Firmware from the web app.
+- Included Icons in UI
+
+### Changed
+- Moved Roboto font files locally
+- Made notifications clickable with a 3rd optional parameter
+
 ## [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
 

+ 319 - 18
app.py

@@ -8,6 +8,7 @@ import serial.tools.list_ports
 import math
 import json
 from datetime import datetime
+import subprocess
 
 app = Flask(__name__)
 
@@ -30,12 +31,12 @@ pause_condition = threading.Condition()
 
 # Global variables to store device information
 arduino_table_name = None
-arduino_driver_type = None
+arduino_driver_type = 'Unknown'
 
 # Table status
 current_playing_file = None
 execution_progress = None
-firmware_version = None
+firmware_version = 'Unknown'
 current_playing_index = None
 current_playlist = None
 is_clearing = False
@@ -44,11 +45,115 @@ serial_lock = threading.Lock()
 
 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"
+}
+
 # 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()
@@ -56,7 +161,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, arduino_table_name, arduino_driver_type
+    global ser, ser_port, arduino_table_name, arduino_driver_type, firmware_version
 
     try:
         if port is None:
@@ -176,7 +281,7 @@ def send_command(command):
                 if response == "R":
                     print("Command execution completed.")
                     break
-                
+
 def wait_for_start_time(schedule_hours):
     """
     Keep checking every 30 seconds if the time is within the schedule to resume execution.
@@ -194,7 +299,7 @@ def wait_for_start_time(schedule_hours):
             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):
     """
@@ -221,7 +326,7 @@ def schedule_checker(schedule_hours):
         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()
 
@@ -234,7 +339,7 @@ def run_theta_rho_file(file_path, schedule_hours=None):
 
     coordinates = parse_theta_rho_file(file_path)
     total_coordinates = len(coordinates)
-    
+
     if total_coordinates < 2:
         print("Not enough coordinates for interpolation.")
         current_playing_file = None  # Clear tracking if failed
@@ -248,12 +353,12 @@ def run_theta_rho_file(file_path, schedule_hours=None):
         if stop_requested:
             print("Execution stopped by user after completing the current batch.")
             break
-        
+
         with pause_condition:
             while pause_requested:
                 print("Execution paused...")
                 pause_condition.wait()  # This will block execution until notified
-        
+
         batch = coordinates[i:i + batch_size]
         if i == 0:
             send_coordinate_batch(ser, batch)
@@ -314,9 +419,9 @@ def run_theta_rho_files(
     if shuffle:
         random.shuffle(file_paths)
         print("Playlist shuffled.")
-    
+
     current_playlist = file_paths
-    
+
     while True:
         for idx, path in enumerate(file_paths):
             current_playing_index = idx
@@ -604,6 +709,7 @@ def send_coordinate():
 
         # Send the coordinate to the Arduino
         send_coordinate_batch(ser, [(theta, rho)])
+        reset_theta()
         return jsonify({"success": True})
     except Exception as e:
         return jsonify({"success": False, "error": str(e)}), 500
@@ -621,7 +727,7 @@ def serial_status():
         'connected': ser.is_open if ser else False,
         'port': ser_port  # Include the port name
     })
-    
+
 @app.route('/pause_execution', methods=['POST'])
 def pause_execution():
     """Pause the current execution."""
@@ -638,16 +744,13 @@ def get_status():
         is_clearing = True
     else:
         is_clearing = False
-    
+
     return jsonify({
         "ser_port": ser_port,
         "stop_requested": stop_requested,
         "pause_requested": pause_requested,
         "current_playing_file": current_playing_file,
         "execution_progress": execution_progress,
-        "arduino_table_name": arduino_table_name,
-        "arduino_driver_type": arduino_driver_type,
-        "firmware_version": firmware_version,
         "current_playing_index": current_playing_index,
         "current_playlist": current_playlist,
         "is_clearing": is_clearing
@@ -858,7 +961,7 @@ def run_playlist():
     # Validate shuffle
     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:
@@ -931,7 +1034,205 @@ 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
+            ser.reset_input_buffer()
+            ser.reset_output_buffer()
+            ser.write(b"GET_VERSION\n")
+            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() == "esp32":
+            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}")
+
+    # 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
 if __name__ == '__main__':
     # Auto-connect to serial
     connect_to_serial()
-    app.run(debug=True, host='0.0.0.0', port=8080)
+    app.run(debug=False, host='0.0.0.0', port=8080)

+ 6 - 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
@@ -183,12 +187,13 @@ 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 == "RESET_THETA")
         {
             resetTheta(); // Reset currentTheta
@@ -224,7 +229,6 @@ void appMode()
                     inOutStepper.setMaxSpeed(newSpeed);
 
                     Serial.println("SPEED_SET");  
-                    Serial.println("R");
                 }
                 else
                 {

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

@@ -0,0 +1,893 @@
+:100000000C948F000C94B7000C94B7000C94B700BC
+:100010000C94B7000C94B7000C94B7000C94B70084
+:100020000C94B7000C94B7000C94B7000C94B70074
+:100030000C94B7000C94B7000C94B7000C94B70064
+:100040000C94160B0C94B7000C94860B0C94600B5C
+:100050000C94B7000C94B7000C94B7000C94B70044
+:100060000C94B7000C94B70005A84CCDB2D44EB98F
+:100070003836A9020C50B9918688083CA6AAAA2A4B
+:10008000BE000000803F4E414E494E495459494EF2
+:1000900046CDCCCC3D0AD7233C17B7D13877CC2BF3
+:1000A000329595E6241FB14F0A000020410000C898
+:1000B0004200401C4620BCBE4CCA1B0E5AAEC59D19
+:1000C0007400000000230026002900000000002426
+:1000D0000027002A0000000000250028002B000453
+:1000E00004040404040404020202020202030303DF
+:1000F00003030301020408102040800102040810D9
+:100100002001020408102000000008000201000085
+:10011000030407000000000000000000B80B1124D9
+:100120001FBECFEFD8E0DEBFCDBF23E0A6E1B2E037
+:1001300001C01D92A83CB207E1F712E0A0E0B1E0D7
+:10014000E6E9F6E302C005900D92A631B107D9F7B2
+:1001500010E0CFE8D0E004C02197FE010E94F31820
+:10016000CE38D107C9F70E94CA0C0C94491B0C94D5
+: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
+:10059000C701B6010E942318181664F09501A8013E
+:1005A000C301B2010E94A6147301620187FD02C05B
+:1005B0006501780120E030E0A901C701B6010E9481
+:1005C000A614811117C01B821C821D821E82C88E38
+:1005D000D98EEA8EFB8EDF91CF911F910F91FF9004
+:1005E000EF90DF90CF90BF90AF907F906F905F9033
+:1005F0004F900895A701960160E074E284E799E4C2
+:100600000E9490169F770E9409176B837C838D83CD
+:100610009E8311E020E030E0A901C701B6010E94ED
+:10062000231818160CF010E01A83D1CFCF92DF9266
+: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
+:1007C0006105710560F4FA01E851FC4F0C94F318CF
+:1007D000F203F403F603F803FA03FC03FE0361E0FB
+:1007E000F901099465E0FCCF64E0FACF66E0F8CF48
+:1007F00062E0F6CF6AE0F4CF68E0F2CF69E0F0CFD4
+:10080000CF93DF93EC01CB01BA0126E030E040E06A
+:1008100050E00E94D418623071058105910589F17C
+:100820006CF46115710581059105D1F06130710598
+:1008300081059105F9F0DF91CF910895643071053C
+:100840008105910561F124F16530710581059105FE
+:1008500091F7E881F9810284F385E02D66E006C016
+:10086000E881F9810284F385E02D64E0CE01DF9117
+:10087000CF910994E881F9810284F385E02D65E048
+:10088000F5CFE881F9810284F385E02D61E0EECFB8
+:10089000E881F9810284F385E02D63E0E7CFE88108
+:1008A000F9810284F385E02D62E0E0CFDC01ED9177
+:1008B000FC910284F385E02D437055276627772746
+:1008C000423051056105710571F0433051056105F4
+:1008D000710559F0413051056105710511F065E070
+:1008E000099466E0FDCF6AE0FBCF69E0F9CFCF93D2
+:1008F000DF93EC01CB01BA0123E030E040E050E0AF
+:100900000E94D418613071058105910599F062301B
+:10091000710581059105A9F0672B682B692BC1F43E
+:10092000E881F9810284F385E02D64E0CE01DF9156
+:10093000CF910994E881F9810284F385E02D61E08B
+:10094000F5CFE881F9810284F385E02D62E0EECFF6
+:10095000DF91CF910895DC01ED91FC910284F38544
+:10096000E02D4370552766277727423051056105F2
+:10097000710571F0433051056105710559F0413041
+:1009800051056105710511F062E0099463E0FDCF46
+:1009900061E0FBCF60E0F9CFCF93DF93EC01E8811A
+:1009A000F9810284F385E02D8A8160E0811162E0A3
+:1009B000CE010995E881F9810284F385E02D8A81D1
+:1009C00061E0811163E0CE0109958CA59DA582307F
+:1009D000910538F0880F991F880F991F0597019787
+:1009E000F1F7E881F9810284F385E02D8A8160E0E6
+:1009F000811162E0CE01DF91CF910994CF93DF9313
+:100A0000EC0120E030E0A901688D798D8A8D9B8D05
+:100A10000E94231818162CF4E8A9F9A9DF91CF91A8
+:100A20000994EAA9FBA9FACFCF92DF92EF92FF9245
+:100A30000F931F93CF93DF93DC011796CC91C430B3
+:100A400039F0C83059F1C33019F0C63049F1C2E06D
+:100A50008C01085F1F4FF12CE12CC62ED12CD1E068
+:100A6000F8016481C6010E2C02C0959587950A9401
+:100A7000E2F780FD6D270F5F1F4F80810E94E2002B
+:100A8000BFEFEB1AFB0AEC1658F3DF91CF911F91E1
+:100A90000F91FF90EF90DF90CF900895C4E0D8CFF2
+:100AA000C3E0D6CFDC011796EC911797E93008F038
+:100AB00038C0F0E0E25AFA4F0C94F31867056D0560
+:100AC000730579057F059105850591058B05ED91E8
+:100AD000FC910684F785E02D0994ED91FC91008846
+:100AE000F189E02DF9CFED91FC910288F389E02D99
+:100AF000F3CFED91FC910488F589E02DEDCFED91D8
+:100B0000FC910688F789E02DE7CFED91FC91008CF0
+:100B1000F18DE02DE1CFED91FC91028CF38DE02D74
+:100B2000DBCF08954F925F926F927F928F929F9248
+:100B3000AF92BF92CF92DF92EF92FF920F931F93EB
+:100B4000CF93DF93EC01CC88DD88EE88FF8888891D
+:100B50009989AA89BB89C81AD90AEA0AFB0A688D49
+:100B6000798D8A8D9B8D9B01AC010E940C154B01E8
+:100B70005C0168A179A18AA19BA19B01AC010E94A3
+:100B80001F169B01AC01C501B4010E9490160E9482
+:100B90000217C114D104E104F10409F0B6C06230B7
+:100BA0007105810591050CF0D0C01B821C821D824D
+:100BB0001E82188E198E1A8E1B8E1CAA1DAA1EAAA2
+:100BC0001FAAC12CD12C7601C701B601DF91CF91AC
+:100BD0001F910F91FF90EF90DF90CF90BF90AF905B
+:100BE0009F908F907F906F905F904F900895101618
+:100BF000110612061306B4F46C157D058E059F05CB
+:100C00001CF42A812111A1C09B01AC0188279927DE
+:100C1000DC01821B930BA40BB50B8CAB9DABAEAB75
+:100C2000BFAB93C0011511052105310509F48DC035
+:100C30006C157D058E059F050CF087C08A81882381
+:100C400009F483C030952095109501951F4F2F4FC3
+:100C50003F4F0CAB1DAB2EAB3FAB77C00115110561
+:100C60002105310509F471C08824992454018C1898
+:100C70009D08AE08BF08681579058A059B050CF02C
+:100C800064C08A81811161C0DDCFCCACDDACEEAC3B
+:100C9000FFACA7019601C701B6010E941F162B01E8
+:100CA0003C01C501B4010E94AD1420E030E040E8F1
+:100CB00050E40E940C1520E030E040E85FE30E9421
+:100CC0001F169B01AC01C301B2010E9490169B014B
+:100CD000AC01C701B6010E941E163B016C01FE016A
+:100CE000E05CFF4FE080F180028113819701A80151
+:100CF0000E942318181614F473018601C701D80145
+:100D00008CAF9DAFAEAFBFAF3AC00CA91DA92EA945
+:100D10003FA91C141D041E041F040CF468CF1016F8
+:100D20001106120613060CF099CF0027112798011F
+:100D30000C191D092E093F096017710782079307D7
+:100D40000CF062CF2A8121115FCF8CA89DA8AEA89C
+:100D5000BFA881149104A104B10409F096CF88AD15
+:100D600099ADAAADBBAD8CAF9DAFAEAFBFAF81E0CB
+:100D70001C141D041E041F040CF080E08A833FEF46
+:100D8000831A930AA30AB30A8CAA9DAAAEAABFAA81
+:100D90008CAC9DACAEACBFACC501B4010E940917D0
+:100DA0006B017C01CB82DC82ED82FE82A501940185
+:100DB00060E074E284E799E40E949016688F798F6E
+:100DC0008A8F9B8F2A812111FFCE9058688F798F4F
+:100DD0008A8F9B8FF9CECF92DF92EF92FF920F9383
+:100DE0001F93CF93DF93EC016A017B0120E030E099
+:100DF000A901CB01B6010E94A61487FF04C0F7FA2F
+:100E0000F094F7F8F094A70196016C8D7D8D8E8D8E
+:100E10009F8D0E94A614882309F446C0CC8EDD8ED7
+:100E2000EE8EFF8E8E01005C1F4FA701960160E0E1
+:100E300074E284E799E40E949016F801608371835C
+:100E4000828393838CA99DA9AEA9BFA91816190600
+:100E50001A061B064CF5688D798D8A8D9B8D9B013A
+:100E6000AC010E940C156B017C0168A179A18AA1DB
+:100E70009BA19B01AC010E941F169B01AC01C70105
+:100E8000B6010E9490160E9402176CAB7DAB8EAB30
+:100E90009FABCE01DF91CF911F910F91FF90EF900B
+:100EA000DF90CF900C949205DF91CF911F910F911D
+:100EB000FF90EF90DF90CF90089508952F923F928A
+:100EC0004F925F926F927F928F929F92AF92BF925A
+:100ED000CF92DF92EF92FF920F931F93CF93DF9306
+:100EE000CDB7DEB762970FB6F894DEBF0FBECDBFA9
+:100EF0006B017C012D873E874F87588B2AEA37E2AA
+:100F00004FE155E40E940C150E9402172B013C0191
+:100F100020E030E044EB55E46D857E858F8598892F
+:100F20000E940C150E9402174B015C0120912202C5
+:100F3000309123024091240250912502C701B6014D
+:100F40000E941E162BED3FE049EC50E40E949016E3
+:100F50009B01AC0160911A0270911B0280911C02EE
+:100F600090911D020E941F1660931A0270931B023B
+:100F700080931C0290931D028091040181111AC07C
+:100F80002BED3FE049EC50E4C701B6010E949016FA
+:100F900020E030E04AE756E40E940C1520E030E003
+:100FA00040E251E40E9490160E940217861A970AA6
+:100FB000A80AB90A49825A826B827C828D829E82FB
+:100FC000AF82B886209039039E012F5F3F4F2901E1
+:100FD00045E253E05A874987912C312C00E010E01C
+:100FE000812C2914C9F1D2016D917D918D919D9132
+:100FF0002D01E985FA85A190B190FA87E987D5019D
+:1010000050962D913D914D915C915397621B730BBE
+:10101000840B950B97FF07C090958095709561950F
+:101020007F4F8F4F9F4F0E94AD14F501248D358D5A
+:10103000468D578D0E9490163B015C01232D302F69
+:10104000412F582D0E942318181624F4362C072DF2
+:101050001A2D8B2C9394C5CF20E030E0A901632D8D
+:10106000702F812F982D0E94231818160CF06CC039
+:10107000912C80913903981608F066C0892D90E074
+:10108000FC01EE0FFF1FEE0FFF1F21E030E02C0FE1
+:101090003D1FE20FF31F4080518062807380AC01DE
+:1010A000440F551F5A8B498BFA01EB5DFC4FA08012
+:1010B000B180F50180899189A289B389A301920148
+:1010C000281B390B4A0B5B0BCA01B9010E94AD14F6
+:1010D000232D302F412F582D0E94901669877A8733
+:1010E0008B879C87F50184899589A689B7894816DD
+:1010F00059066A067B0661F0448A558A668A778AB1
+:101100000190F081E02D0084F185E02DC501099565
+:10111000E989FA89EB5DFC4FA080B180F501208D53
+:10112000318D428D538D69857A858B859C850E9492
+:10113000A614882339F049855A856B857C85C501BD
+:101140000E94AB02939495CF80E010E09091390318
+:10115000191720F5E12FF0E0EE0FFF1FEB5DFC4FBC
+:101160000190F081E02D84889588A688B788408911
+:1011700051896289738984169506A606B70661F0BF
+:1011800083819481A581B681892B8A2B8B2B19F0C1
+:10119000CF010E94160381E01F5FD8CF8111D4CF09
+:1011A000C0922202D0922302E0922402F092250201
+:1011B0002D853E854F85588920931E0230931F024E
+:1011C000409320025093210262960FB6F894DEBF3E
+:1011D0000FBECDBFDF91CF911F910F91FF90EF9088
+:1011E000DF90CF90BF90AF909F908F907F906F9047
+:1011F0005F904F903F902F900895FC010190002048
+:10120000E9F73197AF01481B590BBC0184E892E024
+:101210000C945E01CF93DF930E94FD08EC018DE3F7
+:1012200091E00E94FD088C0F9D1FDF91CF910895E2
+:1012300080E491E00E940A0920E030E44CE955EC9A
+:101240006091520370915303809154039091550320
+:101250000E94A614882341F040E050E46CE975EC4C
+:101260008AE393E00E94AB0280913D0390913E039C
+:10127000A0913F03B0914003892B8A2B8B2B21F047
+:101280008AE393E00E94160360914A0370914B0336
+:1012900080914C0390914D030E94AD1420E030E00A
+:1012A00046EC55EC0E94A6141816F4F210924A036C
+:1012B00010924B0310924C0310924D0310924E0368
+:1012C00010924F03109250031092510310926E032C
+:1012D00010926F03109270031092710310923D03ED
+:1012E00010923E0310923F0310924003109252035B
+:1012F000109253031092540310925503109222023D
+:1013000010922302109224021092250210921E02C3
+:1013100010921F02109220021092210287E491E0A5
+:101320000C940A094F925F926F927F928F929F92D4
+:10133000AF92BF92CF92DF92EF92FF92CF93DF9363
+:10134000EC016A017B0120E030E0A901CB01B6018C
+:101350000E94A61487FF04C0F7FAF094F7F8F094FF
+:1013600088A099A0AAA0BBA0A7019601C501B401BD
+:101370000E94A614882309F44DC06CA97DA98EA9EA
+:101380009FA90E94AD142B013C01A7019601C50144
+:10139000B4010E9490169B01AC01C301B2010E94EE
+:1013A0000C150E9402176CAB7DAB8EAB9FABA701F7
+:1013B000960160E070E080E090E40E9490160E9448
+:1013C000521826E53EE04DE25FE30E940C1520E056
+:1013D00034E244E759E40E940C1568AF79AF8AAF54
+:1013E0009BAFC8A2D9A2EAA2FBA2E881F98100843E
+:1013F000F185E02DCE01DF91CF91FF90EF90DF904E
+:10140000CF90BF90AF909F908F907F906F905F90A4
+:101410004F900994DF91CF91FF90EF90DF90CF90A4
+:10142000BF90AF909F908F907F906F905F904F9004
+:101430000895FC0124813581232B39F421E0FB013F
+:101440008081882349F020E007C0808191810E943B
+:10145000111B21E0892BB9F7822F0895FC018081AF
+:101460009181009711F00C94B71908950C94B71955
+:10147000FB0144815581FC01248135812417350706
+:1014800078F080819181009759F0FB016081718132
+:101490006115710529F00E94211B21E0892B09F0BB
+:1014A00020E0822F08950F931F93CF93DF93EC01D9
+:1014B00088819981009759F02A813B812617370747
+:1014C00030F081E0DF91CF911F910F9108958B0152
+:1014D0006F5F7F4F0E94401A009759F0998388836D
+:1014E0001B830A832C813D81232B59F7FC01108239
+:1014F000E8CF80E0E7CFEF92FF920F931F93CF9357
+:10150000DF93EC017B018A01BA010E94530A288112
+:101510003981811114C02115310519F0C9010E94CA
+:10152000B719198218821D821C821B821A82CE0171
+:10153000DF91CF911F910F91FF90EF9008951D8340
+:101540000C83B701C9010E941A1BF1CFFC01118263
+:1015500010821382128215821482FB0101900020F6
+:10156000E9F73197AF01461B570B0C947B0AAF92FA
+:10157000BF92CF92DF92EF92FF920F931F93CF9380
+:10158000DF93EC016B015A0179012417350720F430
+:101590008B2D5901E42EF82E6FE371E0CE010E94ED
+:1015A000A60AD60114960D911C91A016B10628F535
+:1015B000E016F10608F48701D601ED91FC91119730
+:1015C000E00FF11FF08010826D917C916A0D7B1D00
+:1015D00061157105F1F0FB0101900020E9F73197E9
+:1015E000AF01461B570BCE010E947B0AF60180819A
+:1015F0009181080F191FD801FC92CE01DF91CF9184
+:101600001F910F91FF90EF90DF90CF90BF90AF9020
+:10161000089588819981009711F00E94B719198265
+: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
+:101B30008F7D80838DE491E00E940A0980E691E028
+:101B40000E940A0981E791E00E940A0980E891E079
+:101B50000E940A090E941809E2E1F1E08491EEEF87
+:101B6000F0E00491EAEEF0E01491112309F4C2C10F
+:101B700081110E94B900E12FF0E0EE0FFF1FEF533B
+:101B8000FF4FA591B4918C91082391E080E009F07A
+:101B900090E0092F182F8091790290917A02801796
+:101BA0009107A9F10130110509F0A7C182E891E080
+:101BB0000E940A0920E030E040E05FE360912103E9
+:101BC0007091220380912303909124030E940C15AD
+:101BD0006B017C01BC01A6018EE793E00E94EB063D
+:101BE000B701A6018AE393E00E94EB06609122020E
+:101BF00070912302809124029091250220E030E030
+:101C0000A9010E945E0710937A02009379028091E5
+:101C1000790290917A028130910509F09BC18FE0A1
+:101C20000E944C01BC01990F880B990B0E94AD14C6
+:101C300020E030E040EB50E40E940C1520E030EC56
+:101C40004FE754E40E94901620E030E040E05FE36C
+:101C50000E941F1620E030E040E251E40E940C1583
+:101C60000E94281820E030E040E251E40E949016E3
+:101C70006B017C0120E030E040E85FE30E945A17EE
+:101C800020E030E040E05FE30E94231887FD55C16B
+:101C9000C701B6010E94381723E333E343E75FE34C
+:101CA0000E941F166B017C01809000019090010141
+:101CB000A0900201B09003014090220250902302B4
+:101CC0006090240270902502AC019B01C501B40113
+:101CD0000E94A614882331F1A7019601C501B40121
+:101CE0000E941E16A30192010E940C159B01AC01DB
+:101CF000609116027091170280911802909119025A
+:101D00000E941F16609316027093170280931802A8
+:101D100090931902C0920001D0920101E092020159
+:101D2000F09203018EE00E944C01BC01990F880BD8
+:101D3000990B0E94AD1420E030E040E05FE30E9488
+:101D40000C1520E030EC4FE754E40E94901620E0A0
+:101D500030E0A9010E941F1620E030E040EA51E483
+:101D60000E940C150E94281820E030E040EA51E45F
+:101D70000E9490166B017C01AC019B0160E070E059
+:101D800080E89FE30E941E1620E030E040E05FE321
+:101D90000E940C154B015C01AC019B01C701B6010F
+:101DA0000E941F166B8F7C8F8D8F9E8F2BED3FE0D7
+:101DB00049E45EE3C301B2010E941F166B017C017E
+:101DC0002BED3FE049EC50E40E94901660931A021C
+:101DD00070931B0280931C0290931D02809100015E
+:101DE00090910101A0910201B09103018B8B9C8B1A
+:101DF000AD8BBE8B8091160290911702A0911802B4
+:101E0000B09119028F8B988FA98FBA8FA30192017D
+:101E10006B897C898D899E890E940C152F89388D4C
+:101E2000498D5A8D0E941F160E948B169B01AC0192
+:101E3000C501B4010E940C152B8D3C8D4D8D5E8D1E
+:101E40000E941F1660931E0270931F02809320024F
+:101E5000909321022B893C894D895E89C701B60187
+:101E60000E940C152F89388D498D5A8D0E941F169E
+:101E70000E948B169B01AC01C501B4010E940C1598
+:101E80002B8D3C8D4D8D5E8D0E941F164B015C018C
+:101E900020E030E0A9010E94A61487FD57C020E091
+:101EA00030E040E85FE3C501B4010E942318181632
+:101EB00034F4812C912C30E8A32E3FE3B32EA501FE
+:101EC0009401C701B6010E945E07C0922202D0921F
+:101ED0002302E0922402F092250280E090E0892B18
+:101EE00009F43ACE0E940802882309F435CE0E94F4
+:101EF000000032CE01E010E04ECE89E991E00E9470
+:101F00000A09C0902103D0902203E0902303F090AF
+:101F10002403B701A6018EE793E00E94EB06B70108
+:101F2000A6018AE393E00E94EB0681E0809304011E
+:101F300089EA91E00E940A0959CEC701B6010E94C0
+:101F400038172DEC3CEC4CEC5DE3AACE812C912CA7
+:101F50005401B5CF892B09F684E892E00E94FC0178
+:101F6000181619060CF0E7C16FE371E0CE010D966B
+:101F70000E94A60A0E94AE0197FD1EC08A309105FC
+:101F8000D9F089831A8209891A890F5F1F4FB80116
+:101F9000CE010D960E94530A882361F32D853E855C
+:101FA00089899A89BE016F5F7F4F820F931F0E94BC
+:101FB0001A1B1A8B098BDECF65EB71E0CE010D96F3
+:101FC0000E94190A811162C06AEB71E0CE010D9680
+:101FD0000E94190A81115AC066EC71E0CE010D967B
+:101FE0000E94190A811152C062ED71E0CE01019682
+:101FF0000E94A60ABE016F5F7F4FCE010D960E9420
+:10200000380A10E0811127C06CED71E0CE0107960F
+:102010000E94A60A29893A894B855C8524173507D1
+:1020200008F41DC38D859E85009709F418C36F8140
+:1020300078856115710509F412C3241B350B820FD5
+:10204000931F0E94111B11E0892B09F410E0CE01AF
+:1020500007960E942E0ACE0101960E942E0A112395
+:10206000A9F08EED91E00E94FD0849895A896D859D
+:102070007E8584E892E00E945E018DE391E00E94FB
+:10208000FD08CE010D960E942E0A27CF6AEB71E063
+:10209000CE010D960E94190A882381F081E0809379
+:1020A000040189EA91E00E940A0989EA91E00E940C
+:1020B0000A0988EE91E00E940A09E3CF65EB71E01E
+:1020C000CE010D960E94190A882319F00E94180962
+:1020D000D8CF62ED71E0CE0101960E94A60ABE0142
+:1020E0006F5F7F4FCE010D960E94380A182FCE01E8
+:1020F00001960E942E0A112309F477C009891A89D2
+:102100000115110509F46EC0ED84FE8460E270E0F3
+:10211000C7010E94061B009709F464C0AC014E1968
+:102120005F094F3F540709F45DC04F5F5F4F98014F
+:10213000BE01635F7F4FCE0101960E94B70A89817D
+:102140009A81009709F44BC00E9431136B017C0106
+:1021500020E030E040E85FE30E94231887FD3FC0A5
+:1021600020E030E048EC52E4C701B6010E94A6141A
+:102170001816ACF120E030E048EC52E4C701B6019B
+:102180000E94901620E030E44CE955E40E940C15C2
+:102190000E9402170E94AD146B017C01C0922103C2
+:1021A000D0922203E0922303F0922403BC01A60103
+:1021B0008EE793E00E94EB06B701A6018AE393E065
+:1021C0000E94EB068EEE91E00E940A0980E891E001
+:1021D0000E940A09CE0101960E942E0A52CF88EF72
+:1021E00091E0F6CF86E092E066CF8091780281118F
+:1021F0009EC028E2222E22E0322E10E000E0D12CF8
+:10220000C12C6CED71E0CE0101960E94A60A89896D
+:102210009A890817190770F48D849E8469817A81E0
+:10222000C401800F911F0E942F1B7C01E818F90840
+:10223000892B19F4EE24EA94FE2CCE0101960E941B
+:102240002E0AAFEFEA16FA0609F46AC09701A80150
+:10225000BE01635F7F4FCE0107960E94B70A8B8550
+:102260009C85892B61F08F8098846CE270E0C401BA
+:102270000E94061B8C0108191909892B11F40FEF14
+:102280001FEF980150E040E0BE01695F7F4FCE0133
+:1022900001960E94B70A89819A81412C512C320102
+:1022A000009721F00E9431132B013C01CE010196D1
+:1022B0000E942E0A2B853C85A8014F5F5F4FBE010F
+:1022C000695F7F4FCE0101960E94B70A89819A818A
+:1022D000812C912C5401009721F00E9431134B0165
+:1022E0005C01CE0101960E942E0AF10140825182CA
+:1022F0006282738284829582A682B782BFEFCB1AF4
+:10230000DB0A87010F5F1F4FCE0107960E942E0A3E
+:10231000E8E02E0E311CFAE0CF16D10409F071CF9F
+:10232000D0922702C092260281E080937802CE01EB
+:102330000D960E942E0A80917802882309F4CDCD53
+:102340008091260290912702181619060CF0C5CD2F
+:102350008091220290912302A0912402B091250243
+:102360008B8B9C8BAD8BBE8B80911E0290911F023C
+:10237000A0912002B09121028F8B988FA98FBA8FE4
+:1023800088E2682E82E0782E512C412C8091260222
+:1023900090912702481659060CF056C1809104010D
+:1023A000882309F4C4C0C0902802D0902902E0908C
+:1023B0002A02F0902B022AEA37E24FE155E4C701E6
+:1023C000B6010E940C150E94021760938E03709351
+:1023D0008F03809390039093910360939203709383
+:1023E000930380939403909395031092B2031092F9
+:1023F000B3031092B4031092B503109281031092AC
+:102400008203109283031092840310929603109219
+:102410009703109298031092990360914A03709168
+:102420004B0380914C0390914D030E94AD144B01DE
+:102430005C0120E030E04AE756E460911A027091B6
+:102440001B0280911C0290911D020E940C1520E03D
+:1024500030E040E251E40E9490169B01AC01C501BE
+:10246000B4010E941E160E94021760934A037093E3
+:102470004B0380934C0390934D0360934E037093F2
+:102480004F03809350039093510310926E03109268
+:102490006F03109270031092710310923D0310921B
+:1024A0003E0310923F031092400310925203109289
+:1024B00053031092540310925503C0922202D092FB
+:1024C0002302E0922402F092250210921A02109246
+:1024D0001B0210921C0210921D0210920401209106
+:1024E0002C0230912D0240912E0250912F02C701F3
+:1024F000B6010E945E07D3018D919D910D90BC9114
+:10250000A02D8B8B9C8BAD8BBE8BD30114968D91A4
+:102510009D910D90BC91A02D8F8B988FA98FBA8F14
+:10252000BFEF4B1A5B0AE8E06E0E711C2FCF2B89B0
+:102530003C894D895E89D3016D917D918D919C91EE
+:102540000E941E166B8F7C8F8D8F9E8F2F89388DEA
+:10255000498D5A8DF30164817581868197810E942E
+:102560001E166B017C01AC019B010E940C154B01F6
+:102570005C012B8D3C8D4D8D5E8DCA01B9010E9491
+:102580000C159B01AC01C501B4010E941F160E94ED
+:10259000521820E030E0A9010E9490160E94021714
+:1025A0008B011616170614F001E010E0312C212CD7
+:1025B000C801012E000CAA0BBB0B8F8F98A3A9A3F7
+:1025C000BAA3B101032C000C880B990B0E94AD1427
+:1025D0004B015C016F8D78A189A19AA10E94AD1475
+:1025E0009B01AC01C501B4010E9490164B015C0136
+:1025F000AC019B01C701B6010E940C152F89388DD3
+:10260000498D5A8D0E941F166BA37CA38DA39EA398
+:10261000A50194016B8D7C8D8D8D9E8D0E940C1576
+:102620002B893C894D895E890E941F162BA13CA1F4
+:102630004DA15EA10E945E079FEF291A390A02157B
+:1026400013050CF0BECF57CF1092780210922702DC
+:102650001092260280E891E00E940A093ECC11E027
+:10266000F6CC662777270C943513B0E0A0E0EBE3B7
+:10267000F3E10C94DE155C017B016115710519F025
+:10268000DB018D939C9385010F5F1F4FF501D08176
+:102690008D2F90E00E9485146C01892BB9F5DD32F5
+:1026A000B9F50F5F1F4FD5011196DC91C1E05801BC
+:1026B000F1E0AF1AB10843E050E06EE870E0C50108
+:1026C0000E948E14892B69F5680182E0C80ED11C26
+:1026D00045E050E069E870E0C6010E948E14892B45
+:1026E00021F4680197E0C90ED11CE114F10419F03E
+:1026F000D701CD92DC9260E070E080E89FEFC111DD
+:10270000FFC060E070E080E89FE7FAC05801BBCFEF
+:10271000DB3229F485010E5F1F4FF501D181C0E046
+:10272000C6CF43E050E066E870E0C5010E948E1419
+:10273000892BE9F0F80110E000E020E030E0A90189
+:102740005F01B0ED8B2E8D0E89E08815C8F19C2EAF
+:10275000689491F88C2F8870C2FF16C0811102C056
+:102760000F5F1F4F3196D501DC91C92DE9CFE114E0
+:10277000F10429F00E5F1F4FF7011183008360E021
+:1027800070E080EC9FE7BCC0882311F00150110974
+:10279000A5E0B0E00E94CD159B01AC01220F331FD4
+:1027A000441F551F280D311D411D511D283999E920
+:1027B0003907490799E15907A8F2C6609C2ED2CF84
+:1027C000AEEF8A1206C0C3FD3CC09C2E689493F8FD
+:1027D000C9CFDF7DD534A9F580818D3239F4C06150
+:1027E000DF011296818162E070E006C0DF018B326A
+:1027F000C1F3119661E070E080535D01A61AB70A3B
+:102800008A30F8F4E0E8CE16ECE0DE065CF4B601BF
+:10281000660F771F660F771FC60ED71ECC0CDD1C08
+:10282000C80ED11C5D01FFEFAF1ABF0A8C91805317
+:102830008A30A8F1C4FF03C0D194C194D1080C0D13
+:102840001D1DC1FF09C0E114F10431F081E0A81A97
+:10285000B108D701AD92BC92CA01B9010E94AB1474
+:10286000C370C33009F490584B015C0120E030E0A4
+:10287000A9010E94A614882309F440C0CDEBD0E042
+:1028800017FF05C0119501951109C5EAD0E06E0149
+:10289000B8E1CB1AD10880E2E82EF12C0FC0D501A7
+:1028A000B1CFFE0125913591459154910E191F0923
+:1028B000C501B4010E940C154B015C01D501C40196
+:1028C0000E151F0574F72497F594E794CC16DD06D2
+:1028D000A9F78A2F880F8B2F881F8F3F49F020E0A0
+:1028E00030E0A901C501B4010E94A614811106C0FF
+:1028F00082E290E09093C3038093C203C501B401C8
+:10290000CDB7DEB7ECE00C94FA1591110C94791563
+:10291000803219F089508550C8F70895FB01DC0119
+:102920004150504088F08D9181341CF08B350CF46F
+:10293000805E659161341CF06B350CF4605E861B23
+:10294000611171F3990B0895881BFCCF0E94E81464
+:1029500008F481E00895E89409C097FB3EF490954F
+:102960008095709561957F4F8F4F9F4F9923A9F068
+:10297000F92F96E9BB279395F695879577956795F7
+:10298000B795F111F8CFFAF4BB0F11F460FF1BC03B
+:102990006F5F7F4F8F4F9F4F16C0882311F096E9CE
+:1029A00011C0772321F09EE8872F762F05C066237C
+:1029B00071F096E8862F70E060E02AF09A95660F35
+:1029C000771F881FDAF7880F9695879597F90895EE
+:1029D000990F0008550FAA0BE0E8FEEF1616170630
+:1029E000E807F907C0F012161306E407F50798F098
+:1029F000621B730B840B950B39F40A2661F0232BB1
+:102A0000242B252B21F408950A2609F4A140A6952C
+:102A10008FEF811D811D08950E941F150C94931541
+:102A20000E94851538F00E948C1520F0952311F036
+:102A30000C947C150C94821511240C94C7150E94DB
+:102A4000A41570F3959FC1F3950F50E0551F629F39
+:102A5000F001729FBB27F00DB11D639FAA27F00DF7
+:102A6000B11DAA1F649F6627B00DA11D661F829F1E
+:102A70002227B00DA11D621F739FB00DA11D621F03
+:102A8000839FA00D611D221F749F3327A00D611D20
+:102A9000231F849F600D211D822F762F6A2F112402
+:102AA0009F5750409AF0F1F088234AF0EE0FFF1F35
+:102AB000BB1F661F771F881F91505040A9F79E3F8C
+:102AC000510580F00C947C150C94C7155F3FE4F31E
+:102AD000983ED4F3869577956795B795F795E795E2
+:102AE0009F5FC1F7FE2B880F911D9695879597F9EB
+:102AF000089599278827089597F99F6780E870E0DF
+:102B000060E008959FEF80EC089500240A94161663
+:102B1000170618060906089500240A9412161306CB
+:102B2000140605060895092E0394000C11F4882359
+:102B300052F0BB0F40F4BF2B11F460FF04C06F5F75
+:102B40007F4F8F4F9F4F089557FD9058440F551F4B
+:102B500059F05F3F71F04795880F97FB991F61F01F
+:102B60009F3F79F087950895121613061406551F96
+:102B7000F2CF4695F1DF08C0161617061806991F02
+:102B8000F1CF86957105610508940895E894BB27F7
+:102B900066277727CB0197F908950E940F16A59F06
+:102BA000900DB49F900DA49F800D911D1124089548
+:102BB0002F923F924F925F926F927F928F929F924D
+:102BC000AF92BF92CF92DF92EF92FF920F931F933B
+:102BD000CF93DF93CDB7DEB7CA1BDB0B0FB6F894EC
+:102BE000DEBF0FBECDBF09942A88398848885F842C
+:102BF0006E847D848C849B84AA84B984C884DF809D
+:102C0000EE80FD800C811B81AA81B981CE0FD11D80
+:102C10000FB6F894DEBF0FBECDBFED010895A29FA1
+:102C2000B001B39FC001A39F700D811D1124911DA0
+:102C3000B29F700D811D1124911D08955058BB271E
+:102C4000AA270E9436160C9493150E94851538F019
+:102C50000E948C1520F039F49F3F19F426F40C944F
+:102C600082150EF4E095E7FB0C947C15E92F0E9489
+:102C7000A41558F3BA17620773078407950720F065
+:102C800079F4A6F50C94C6150EF4E0950B2EBA2F28
+:102C9000A02D0B01B90190010C01CA01A001112462
+:102CA000FF27591B99F0593F50F4503E68F11A160E
+:102CB000F040A22F232F342F4427585FF3CF46959F
+:102CC00037952795A795F0405395C9F77EF41F16C1
+:102CD000BA0B620B730B840BBAF09150A1F0FF0F8B
+:102CE000BB1F661F771F881FC2F70EC0BA0F621F77
+:102CF000731F841F48F4879577956795B795F79567
+:102D00009E3F08F0B0CF9395880F08F09927EE0FFB
+:102D10009795879508950E94CE17E3950C94F71721
+:102D20000E94A4160C9493150E948C1558F00E94D2
+:102D3000851540F029F45F3F29F00C947C15511162
+:102D40000C94C7150C9482150E94A41568F399235E
+:102D5000B1F3552391F3951B550BBB27AA27621797
+:102D60007307840738F09F5F5F4F220F331F441FA4
+:102D7000AA1FA9F335D00E2E3AF0E0E832D09150D8
+:102D80005040E695001CCAF72BD0FE2F29D0660FC5
+:102D9000771F881FBB1F261737074807AB07B0E808
+:102DA00009F0BB0B802DBF01FF2793585F4F3AF00E
+:102DB0009E3F510578F00C947C150C94C7155F3F2D
+:102DC000E4F3983ED4F3869577956795B795F79594
+:102DD0009F5FC9F7880F911D9695879597F908957C
+:102DE000E1E0660F771F881FBB1F62177307840718
+:102DF000BA0720F0621B730B840BBA0BEE1F88F727
+:102E0000E09508950E9409176894B1110C94C715B4
+:102E100008950E94AC1588F09F5798F0B92F992714
+:102E2000B751B0F0E1F0660F771F881F991F1AF0B5
+:102E3000BA95C9F714C0B13091F00E94C615B1E03F
+:102E400008950C94C615672F782F8827B85F39F03E
+:102E5000B93FCCF3869577956795B395D9F73EF44E
+:102E600090958095709561957F4F8F4F9F4F0895F6
+:102E70000E940B1890F09F3748F4911116F00C94B3
+:102E8000C71560E070E080E89FEB089526F41B16FC
+:102E9000611D711D811D0C94A3170C94BE170E9417
+:102EA000851520F019F00E948C1550F40C948215B1
+:102EB0000C94C715E92F0E94A41588F35523B1F38C
+:102EC000E7FB6217730784079507A8F189F3E92FD9
+:102ED000FF2788232AF03197660F771F881FDAF7BC
+:102EE000952F5527442332F091505040220F331F25
+:102EF000441FD2F7BB27E91BF50B621B730B840B36
+:102F0000B109B1F222F4620F731F841FB11D319712
+:102F10002AF0660F771F881FBB1FEFCF91505040DC
+:102F200062F041F0882332F0660F771F881F9150BE
+:102F30005040C1F793950C94BE1786957795679589
+:102F40009F5FD9F7F7CF882371F4772321F098504A
+:102F5000872B762F07C0662311F499270DC0905157
+:102F6000862B70E060E02AF09A95660F771F881F25
+:102F7000DAF7880F9695879597F908959F3F31F076
+:102F8000915020F4879577956795B795880F911D97
+:102F90009695879597F908950C9482150E94AC1523
+:102FA000D8F3E894E0E0BB279F57F0F02AED3FE02C
+:102FB00049EC06C0EE0FBB0F661F771F881F28F075
+:102FC000B23A62077307840728F0B25A620B730B98
+:102FD000840BE3959A9572F7803830F49A95BB0F7D
+:102FE000661F771F881FD2F790480C94C017EF9385
+:102FF000E0FF07C0A2EA2AED3FE049EC5FEB0E9448
+:1030000036160E9493150F90039401FC9058E8E641
+:10301000F0E00C9499180E94AC15A0F0BEE7B91727
+:1030200088F4BB279F3860F41616B11D672F782FE0
+:103030008827985FF7CF869577956795B11D93950B
+:103040009639C8F308950E94E81408F48FEF0895A4
+:103050000E94AC15E8F09E37E8F09639B8F49E3837
+:1030600048F4672F782F8827985FF9CF8695779552
+:10307000679593959539D0F3B62FB1706B0F711D8D
+:10308000811D20F487957795679593950C94A317E8
+:103090000C94BE170C94C71519F416F40C948215F1
+:1030A0000C94BE170E94AC15B8F39923C9F3B6F37C
+:1030B0009F57550B87FF0E9492180024A0E640EA14
+:1030C000900180585695979528F4805C660F771F7D
+:1030D000881F20F026173707480730F4621B730B50
+:1030E000840B202931294A2BA69517940794202573
+:1030F00031254A2758F7660F771F881F20F02617BB
+:103100003707480730F4620B730B840B200D311D19
+:10311000411DA09581F7B901842F9158880F96958C
+:103120008795089591505040660F771F881FD2F7FA
+:1031300008959F938F937F936F93FF93EF939B01DA
+:10314000AC010E940C15EF91FF910E94AD182F91D8
+:103150003F914F915F910C940C15DF93CF931F9388
+:103160000F93FF92EF92DF927B018C01689406C06F
+:10317000DA2EEF010E941F15FE01E894A59125911A
+:10318000359145915591A6F3EF010E943616FE0147
+:103190009701A801DA9469F7DF90EF90FF900F9103
+:1031A0001F91CF91DF910895052E97FB1EF4009497
+:1031B0000E94EB1857FD07D00E94F91807FC03D0B6
+:1031C0004EF40C94EB1850954095309521953F4F57
+:1031D0004F4F5F4F089590958095709561957F4F03
+:1031E0008F4F9F4F0895EE0FFF1F0590F491E02D34
+:1031F0000994A1E21A2EAA1BBB1BFD010DC0AA1F38
+:10320000BB1FEE1FFF1FA217B307E407F50720F04F
+:10321000A21BB30BE40BF50B661F771F881F991FCA
+:103220001A9469F760957095809590959B01AC0113
+:10323000BD01CF0108950F931F93CF93DF93823089
+:10324000910510F482E090E0E091C603F091C7038D
+:1032500030E020E0B0E0A0E0309799F4211531058E
+:1032600009F44AC0281B390B24303105D8F58A816E
+:103270009B816115710589F1FB0193838283FE01B6
+:1032800011C0408151810281138148175907E0F034
+:103290004817590799F4109761F012960C931297FA
+:1032A00013961C933296CF01DF91CF911F910F910E
+:1032B00008950093C6031093C703F4CF2115310579
+:1032C00051F04217530738F0A901DB019A01BD0103
+:1032D000DF01F801C1CFEF01F9CF9093C7038093CD
+:1032E000C603CDCFFE01E20FF31F819391932250CD
+:1032F000310939832883D7CF2091C4033091C50386
+:10330000232B41F420910701309108013093C5032C
+:103310002093C40320910501309106012115310548
+:1033200041F42DB73EB74091090150910A01241B89
+:10333000350BE091C403F091C503E217F307A0F445
+:103340002E1B3F0B2817390778F0AC014E5F5F4FFB
+:103350002417350748F04E0F5F1F5093C503409365
+:10336000C403819391939FCFF0E0E0E09CCFCF9393
+:10337000DF930097E9F0FC01329713821282A0914B
+:10338000C603B091C703ED0130E020E01097A1F42F
+:1033900020813181820F931F2091C4033091C50396
+:1033A0002817390709F061C0F093C503E093C403FF
+:1033B000DF91CF910895EA01CE17DF07E8F54A8142
+:1033C0005B819E0141155105B1F7E901FB83EA8359
+:1033D00049915991C40FD51FEC17FD0761F4808105
+:1033E00091810296840F951FE901998388838281D8
+:1033F00093819B838A83F0E0E0E012968D919C910B
+:1034000013970097B9F52D913C911197CD01029634
+:10341000820F931F2091C4033091C50328173907E9
+:1034200039F6309751F51092C7031092C603B09346
+:10343000C503A093C403BCCFD383C2834081518111
+:10344000840F951FC817D90761F44E5F5F4F8881BD
+:103450009981480F591F518340838A819B819383AF
+:1034600082832115310509F0B0CFF093C703E093B3
+:10347000C6039ECFFD01DC01C0CF13821282D7CFDD
+:10348000B0E0A0E0E6E4FAE10C94DA158C010097D4
+:1034900051F4CB010E941B198C01C801CDB7DEB7D6
+:1034A000E0E10C94F615FC01E60FF71F9C01225099
+:1034B0003109E217F30708F49DC0D901CD91DC91E1
+:1034C0001197C617D70798F0C530D10530F3CE0154
+:1034D00004978617970708F3C61BD70B2297C1934B
+:1034E000D1936D937C93CF010E94B719D6CF5B0126
+:1034F000AC1ABD0A4C018C0E9D1EA091C603B09162
+:10350000C703512C412CF12CE12C109731F58091FF
+:10351000C4039091C5038815990509F05CC046164F
+:10352000570608F058C08091050190910601009758
+:1035300041F48DB79EB74091090150910A01841B57
+:10354000950BE817F90708F055C0F093C503E09311
+:10355000C403F90171836083A0CF8D919C91119771
+:1035600012966C90129713967C901397A815B90534
+:1035700059F56C0142E0C40ED11CCA14DB0420F1E1
+:10358000AC014A195B09DA011296159780F0628244
+:10359000738251834083D9016D937C93E114F104CC
+:1035A00071F0D7011396FC93EE93129776CF229683
+:1035B0008C0F9D1FF90191838083F301EFCFF0936E
+:1035C000C703E093C60369CF4816590608F42C01D7
+:1035D0007D01D3019ACFCB010E941B197C0100977A
+:1035E00049F0AE01B8010E94FD1AC8010E94B71946
+:1035F000870153CF10E000E050CFFB01DC0102C097
+:1036000001900D9241505040D8F70895FC018191EE
+:10361000861721F08823D9F7992708953197CF018C
+:103620000895FB01DC018D91019080190110D9F3FF
+:10363000990B0895FB01DC0101900D920020E1F748
+:103640000895FB01DC014150504030F08D91019014
+:10365000801919F40020B9F7881B990B0895FB0114
+:1036600051915523A9F0BF01DC014D91451741113E
+:10367000E1F759F4CD010190002049F04D9140153A
+:103680004111C9F3FB014111EFCF81E090E00197B7
+:063690000895F894FFCF3D
+:10369600CDCC3C40010000C8038000000000003E85
+:1036A600025E018B018B02FC019A01EE0100000013
+:1036B60000B3037F035D07360A920514055205FE23
+:1036C60004CC04AB04770456040004D1030D0A00AD
+:1036D600484F4D494E4700484F4D4544005461629E
+:1036E6006C653A2044756E65205765617665720093
+:1036F600447269766572733A2044525638383235C8
+:103706000056657273696F6E3A20312E342E300082
+:103716005200537069726F6772617068204D6F64F2
+:10372600652041637469766500417070204D6F6451
+:1037360065204163746976650054484554415F527B
+:103746004553455400484F4D450052455345545F37
+:103756005448455441004745545F56455253494FD6
+:103766004E005345545F5350454544003B0049477E
+:103776004E4F5245443A2000524541445900535059
+:103786004545445F53455400494E56414C49445FB4
+:10379600535045454400494E56414C49445F434FBA
+:0637A6004D4D414E4400B0
+:00000001FF

+ 7 - 3
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
@@ -75,7 +79,6 @@ void setup()
     homing();
 }
 
-
 void resetTheta()
 {
     isFirstCoordinates = true; // Set flag to skip interpolation for the next movement
@@ -184,12 +187,13 @@ 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 == "RESET_THETA")
         {
             resetTheta(); // Reset currentTheta
@@ -197,6 +201,7 @@ void appMode()
             Serial.println("READY");
             return;
         }
+
         if (input == "HOME")
         {
             homing();
@@ -225,7 +230,6 @@ void appMode()
                     inOutStepper.setMaxSpeed(newSpeed);
 
                     Serial.println("SPEED_SET");  
-                    Serial.println("R");
                 }
                 else
                 {

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

@@ -0,0 +1,893 @@
+:100000000C948F000C94B7000C94B7000C94B700BC
+:100010000C94B7000C94B7000C94B7000C94B70084
+:100020000C94B7000C94B7000C94B7000C94B70074
+:100030000C94B7000C94B7000C94B7000C94B70064
+:100040000C94160B0C94B7000C94860B0C94600B5C
+:100050000C94B7000C94B7000C94B7000C94B70044
+:100060000C94B7000C94B70005A84CCDB2D44EB98F
+:100070003836A9020C50B9918688083CA6AAAA2A4B
+:10008000BE000000803F4E414E494E495459494EF2
+:1000900046CDCCCC3D0AD7233C17B7D13877CC2BF3
+:1000A000329595E6241FB14F0A000020410000C898
+:1000B0004200401C4620BCBE4CCA1B0E5AAEC59D19
+:1000C0007400000000230026002900000000002426
+:1000D0000027002A0000000000250028002B000453
+:1000E00004040404040404020202020202030303DF
+:1000F00003030301020408102040800102040810D9
+:100100002001020408102000000008000201000085
+:10011000030407000000000000000000B80B1124D9
+:100120001FBECFEFD8E0DEBFCDBF23E0A6E1B2E037
+:1001300001C01D92A83CB207E1F712E0A0E0B1E0D7
+:10014000EAE9F6E302C005900D92A631B107D9F7AE
+:1001500010E0CFE8D0E004C02197FE010E94F5181E
+:10016000CE38D107C9F70E94CA0C0C944B1B0C94D3
+: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
+:10059000C701B6010E942518181664F09501A8013C
+:1005A000C301B2010E94A8147301620187FD02C059
+:1005B0006501780120E030E0A901C701B6010E9481
+:1005C000A814811117C01B821C821D821E82C88E36
+:1005D000D98EEA8EFB8EDF91CF911F910F91FF9004
+:1005E000EF90DF90CF90BF90AF907F906F905F9033
+:1005F0004F900895A701960160E074E284E799E4C2
+:100600000E9492169F770E940B176B837C838D83C9
+:100610009E8311E020E030E0A901C701B6010E94ED
+:10062000251818160CF010E01A83D1CFCF92DF9264
+: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
+:1007C0006105710560F4FA01E851FC4F0C94F518CD
+:1007D000F203F403F603F803FA03FC03FE0361E0FB
+:1007E000F901099465E0FCCF64E0FACF66E0F8CF48
+:1007F00062E0F6CF6AE0F4CF68E0F2CF69E0F0CFD4
+:10080000CF93DF93EC01CB01BA0126E030E040E06A
+:1008100050E00E94D618623071058105910589F17A
+:100820006CF46115710581059105D1F06130710598
+:1008300081059105F9F0DF91CF910895643071053C
+:100840008105910561F124F16530710581059105FE
+:1008500091F7E881F9810284F385E02D66E006C016
+:10086000E881F9810284F385E02D64E0CE01DF9117
+:10087000CF910994E881F9810284F385E02D65E048
+:10088000F5CFE881F9810284F385E02D61E0EECFB8
+:10089000E881F9810284F385E02D63E0E7CFE88108
+:1008A000F9810284F385E02D62E0E0CFDC01ED9177
+:1008B000FC910284F385E02D437055276627772746
+:1008C000423051056105710571F0433051056105F4
+:1008D000710559F0413051056105710511F065E070
+:1008E000099466E0FDCF6AE0FBCF69E0F9CFCF93D2
+:1008F000DF93EC01CB01BA0123E030E040E050E0AF
+:100900000E94D618613071058105910599F0623019
+:10091000710581059105A9F0672B682B692BC1F43E
+:10092000E881F9810284F385E02D64E0CE01DF9156
+:10093000CF910994E881F9810284F385E02D61E08B
+:10094000F5CFE881F9810284F385E02D62E0EECFF6
+:10095000DF91CF910895DC01ED91FC910284F38544
+:10096000E02D4370552766277727423051056105F2
+:10097000710571F0433051056105710559F0413041
+:1009800051056105710511F062E0099463E0FDCF46
+:1009900061E0FBCF60E0F9CFCF93DF93EC01E8811A
+:1009A000F9810284F385E02D8A8160E0811162E0A3
+:1009B000CE010995E881F9810284F385E02D8A81D1
+:1009C00061E0811163E0CE0109958CA59DA582307F
+:1009D000910538F0880F991F880F991F0597019787
+:1009E000F1F7E881F9810284F385E02D8A8160E0E6
+:1009F000811162E0CE01DF91CF910994CF93DF9313
+:100A0000EC0120E030E0A901688D798D8A8D9B8D05
+:100A10000E94251818162CF4E8A9F9A9DF91CF91A6
+:100A20000994EAA9FBA9FACFCF92DF92EF92FF9245
+:100A30000F931F93CF93DF93DC011796CC91C430B3
+:100A400039F0C83059F1C33019F0C63049F1C2E06D
+:100A50008C01085F1F4FF12CE12CC62ED12CD1E068
+:100A6000F8016481C6010E2C02C0959587950A9401
+:100A7000E2F780FD6D270F5F1F4F80810E94E2002B
+:100A8000BFEFEB1AFB0AEC1658F3DF91CF911F91E1
+:100A90000F91FF90EF90DF90CF900895C4E0D8CFF2
+:100AA000C3E0D6CFDC011796EC911797E93008F038
+:100AB00038C0F0E0E25AFA4F0C94F51867056D055E
+:100AC000730579057F059105850591058B05ED91E8
+:100AD000FC910684F785E02D0994ED91FC91008846
+:100AE000F189E02DF9CFED91FC910288F389E02D99
+:100AF000F3CFED91FC910488F589E02DEDCFED91D8
+:100B0000FC910688F789E02DE7CFED91FC91008CF0
+:100B1000F18DE02DE1CFED91FC91028CF38DE02D74
+:100B2000DBCF08954F925F926F927F928F929F9248
+:100B3000AF92BF92CF92DF92EF92FF920F931F93EB
+:100B4000CF93DF93EC01CC88DD88EE88FF8888891D
+:100B50009989AA89BB89C81AD90AEA0AFB0A688D49
+:100B6000798D8A8D9B8D9B01AC010E940E154B01E6
+:100B70005C0168A179A18AA19BA19B01AC010E94A3
+:100B800021169B01AC01C501B4010E9492160E947E
+:100B90000417C114D104E104F10409F0B6C06230B5
+:100BA0007105810591050CF0D0C01B821C821D824D
+:100BB0001E82188E198E1A8E1B8E1CAA1DAA1EAAA2
+:100BC0001FAAC12CD12C7601C701B601DF91CF91AC
+:100BD0001F910F91FF90EF90DF90CF90BF90AF905B
+:100BE0009F908F907F906F905F904F900895101618
+:100BF000110612061306B4F46C157D058E059F05CB
+:100C00001CF42A812111A1C09B01AC0188279927DE
+:100C1000DC01821B930BA40BB50B8CAB9DABAEAB75
+:100C2000BFAB93C0011511052105310509F48DC035
+:100C30006C157D058E059F050CF087C08A81882381
+:100C400009F483C030952095109501951F4F2F4FC3
+:100C50003F4F0CAB1DAB2EAB3FAB77C00115110561
+:100C60002105310509F471C08824992454018C1898
+:100C70009D08AE08BF08681579058A059B050CF02C
+:100C800064C08A81811161C0DDCFCCACDDACEEAC3B
+:100C9000FFACA7019601C701B6010E9421162B01E6
+:100CA0003C01C501B4010E94AF1420E030E040E8EF
+:100CB00050E40E940E1520E030E040E85FE30E941F
+:100CC00021169B01AC01C301B2010E9492169B0147
+:100CD000AC01C701B6010E9420163B016C01FE0168
+:100CE000E05CFF4FE080F180028113819701A80151
+:100CF0000E942518181614F473018601C701D80143
+:100D00008CAF9DAFAEAFBFAF3AC00CA91DA92EA945
+:100D10003FA91C141D041E041F040CF468CF1016F8
+:100D20001106120613060CF099CF0027112798011F
+:100D30000C191D092E093F096017710782079307D7
+:100D40000CF062CF2A8121115FCF8CA89DA8AEA89C
+:100D5000BFA881149104A104B10409F096CF88AD15
+:100D600099ADAAADBBAD8CAF9DAFAEAFBFAF81E0CB
+:100D70001C141D041E041F040CF080E08A833FEF46
+:100D8000831A930AA30AB30A8CAA9DAAAEAABFAA81
+:100D90008CAC9DACAEACBFACC501B4010E940B17CE
+:100DA0006B017C01CB82DC82ED82FE82A501940185
+:100DB00060E074E284E799E40E949216688F798F6C
+:100DC0008A8F9B8F2A812111FFCE9058688F798F4F
+:100DD0008A8F9B8FF9CECF92DF92EF92FF920F9383
+:100DE0001F93CF93DF93EC016A017B0120E030E099
+:100DF000A901CB01B6010E94A81487FF04C0F7FA2D
+:100E0000F094F7F8F094A70196016C8D7D8D8E8D8E
+:100E10009F8D0E94A814882309F446C0CC8EDD8ED5
+:100E2000EE8EFF8E8E01005C1F4FA701960160E0E1
+:100E300074E284E799E40E949216F801608371835A
+:100E4000828393838CA99DA9AEA9BFA91816190600
+:100E50001A061B064CF5688D798D8A8D9B8D9B013A
+:100E6000AC010E940E156B017C0168A179A18AA1D9
+:100E70009BA19B01AC010E9421169B01AC01C70103
+:100E8000B6010E9492160E9404176CAB7DAB8EAB2C
+:100E90009FABCE01DF91CF911F910F91FF90EF900B
+:100EA000DF90CF900C949205DF91CF911F910F911D
+:100EB000FF90EF90DF90CF90089508952F923F928A
+:100EC0004F925F926F927F928F929F92AF92BF925A
+:100ED000CF92DF92EF92FF920F931F93CF93DF9306
+:100EE000CDB7DEB762970FB6F894DEBF0FBECDBFA9
+:100EF0006B017C012D873E874F87588B2AEA37E2AA
+:100F00004FE155E40E940E150E9404172B013C018D
+:100F100020E030E044EB55E46D857E858F8598892F
+:100F20000E940E150E9404174B015C0120912202C1
+:100F3000309123024091240250912502C701B6014D
+:100F40000E9420162BED3FE049EC50E40E949216DF
+:100F50009B01AC0160911A0270911B0280911C02EE
+:100F600090911D020E94211660931A0270931B0239
+:100F700080931C0290931D028091040181111AC07C
+:100F80002BED3FE049EC50E4C701B6010E949216F8
+:100F900020E030E04AE756E40E940E1520E030E001
+:100FA00040E251E40E9492160E940417861A970AA2
+:100FB000A80AB90A49825A826B827C828D829E82FB
+:100FC000AF82B886209039039E012F5F3F4F2901E1
+:100FD00045E253E05A874987912C312C00E010E01C
+:100FE000812C2914C9F1D2016D917D918D919D9132
+:100FF0002D01E985FA85A190B190FA87E987D5019D
+:1010000050962D913D914D915C915397621B730BBE
+:10101000840B950B97FF07C090958095709561950F
+:101020007F4F8F4F9F4F0E94AF14F501248D358D58
+:10103000468D578D0E9492163B015C01232D302F67
+:10104000412F582D0E942518181624F4362C072DF0
+:101050001A2D8B2C9394C5CF20E030E0A901632D8D
+:10106000702F812F982D0E94251818160CF06CC037
+:10107000912C80913903981608F066C0892D90E074
+:10108000FC01EE0FFF1FEE0FFF1F21E030E02C0FE1
+:101090003D1FE20FF31F4080518062807380AC01DE
+:1010A000440F551F5A8B498BFA01EB5DFC4FA08012
+:1010B000B180F50180899189A289B389A301920148
+:1010C000281B390B4A0B5B0BCA01B9010E94AF14F4
+:1010D000232D302F412F582D0E94921669877A8731
+:1010E0008B879C87F50184899589A689B7894816DD
+:1010F00059066A067B0661F0448A558A668A778AB1
+:101100000190F081E02D0084F185E02DC501099565
+:10111000E989FA89EB5DFC4FA080B180F501208D53
+:10112000318D428D538D69857A858B859C850E9492
+:10113000A814882339F049855A856B857C85C501BB
+:101140000E94AB02939495CF80E010E09091390318
+:10115000191720F5E12FF0E0EE0FFF1FEB5DFC4FBC
+:101160000190F081E02D84889588A688B788408911
+:1011700051896289738984169506A606B70661F0BF
+:1011800083819481A581B681892B8A2B8B2B19F0C1
+:10119000CF010E94160381E01F5FD8CF8111D4CF09
+:1011A000C0922202D0922302E0922402F092250201
+:1011B0002D853E854F85588920931E0230931F024E
+:1011C000409320025093210262960FB6F894DEBF3E
+:1011D0000FBECDBFDF91CF911F910F91FF90EF9088
+:1011E000DF90CF90BF90AF909F908F907F906F9047
+:1011F0005F904F903F902F900895FC010190002048
+:10120000E9F73197AF01481B590BBC0184E892E024
+:101210000C945E01CF93DF930E94FD08EC018DE3F7
+:1012200091E00E94FD088C0F9D1FDF91CF910895E2
+:1012300080E491E00E940A0920E030E04AE754ECA3
+:101240006091520370915303809154039091550320
+:101250000E94A814882341F040E050E06AE774EC53
+:101260008AE393E00E94AB0280913D0390913E039C
+:10127000A0913F03B0914003892B8A2B8B2B21F047
+:101280008AE393E00E94160360914A0370914B0336
+:1012900080914C0390914D030E94AF1420E030E008
+:1012A00046EC55EC0E94A8141816F4F210924A036A
+:1012B00010924B0310924C0310924D0310924E0368
+:1012C00010924F03109250031092510310926E032C
+:1012D00010926F03109270031092710310923D03ED
+:1012E00010923E0310923F0310924003109252035B
+:1012F000109253031092540310925503109222023D
+:1013000010922302109224021092250210921E02C3
+:1013100010921F02109220021092210287E491E0A5
+:101320000C940A094F925F926F927F928F929F92D4
+:10133000AF92BF92CF92DF92EF92FF92CF93DF9363
+:10134000EC016A017B0120E030E0A901CB01B6018C
+:101350000E94A81487FF04C0F7FAF094F7F8F094FD
+:1013600088A099A0AAA0BBA0A7019601C501B401BD
+:101370000E94A814882309F44DC06CA97DA98EA9E8
+:101380009FA90E94AF142B013C01A7019601C50142
+:10139000B4010E9492169B01AC01C301B2010E94EC
+:1013A0000E150E9404176CAB7DAB8EAB9FABA701F3
+:1013B000960160E070E080E090E40E9492160E9446
+:1013C000541826E53EE04DE25FE30E940E1520E052
+:1013D00034E244E759E40E940E1568AF79AF8AAF52
+:1013E0009BAFC8A2D9A2EAA2FBA2E881F98100843E
+:1013F000F185E02DCE01DF91CF91FF90EF90DF904E
+:10140000CF90BF90AF909F908F907F906F905F90A4
+:101410004F900994DF91CF91FF90EF90DF90CF90A4
+:10142000BF90AF909F908F907F906F905F904F9004
+:101430000895FC0124813581232B39F421E0FB013F
+:101440008081882349F020E007C0808191810E943B
+:10145000131B21E0892BB9F7822F0895FC018081AD
+:101460009181009711F00C94B91908950C94B91951
+:10147000FB0144815581FC01248135812417350706
+:1014800078F080819181009759F0FB016081718132
+:101490006115710529F00E94231B21E0892B09F0B9
+:1014A00020E0822F08950F931F93CF93DF93EC01D9
+:1014B00088819981009759F02A813B812617370747
+:1014C00030F081E0DF91CF911F910F9108958B0152
+:1014D0006F5F7F4F0E94421A009759F0998388836B
+:1014E0001B830A832C813D81232B59F7FC01108239
+:1014F000E8CF80E0E7CFEF92FF920F931F93CF9357
+:10150000DF93EC017B018A01BA010E94530A288112
+:101510003981811114C02115310519F0C9010E94CA
+:10152000B919198218821D821C821B821A82CE016F
+:10153000DF91CF911F910F91FF90EF9008951D8340
+:101540000C83B701C9010E941C1BF1CFFC01118261
+:1015500010821382128215821482FB0101900020F6
+:10156000E9F73197AF01461B570B0C947B0AAF92FA
+:10157000BF92CF92DF92EF92FF920F931F93CF9380
+:10158000DF93EC016B015A0179012417350720F430
+:101590008B2D5901E42EF82E6FE371E0CE010E94ED
+:1015A000A60AD60114960D911C91A016B10628F535
+:1015B000E016F10608F48701D601ED91FC91119730
+:1015C000E00FF11FF08010826D917C916A0D7B1D00
+:1015D00061157105F1F0FB0101900020E9F73197E9
+:1015E000AF01461B570BCE010E947B0AF60180819A
+:1015F0009181080F191FD801FC92CE01DF91CF9184
+:101600001F910F91FF90EF90DF90CF90BF90AF9020
+:10161000089588819981009711F00E94B919198263
+: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
+:101B30008F7D80838DE491E00E940A0980E691E028
+:101B40000E940A0981E791E00E940A0980E891E079
+:101B50000E940A090E941809E2E1F1E08491EEEF87
+:101B6000F0E00491EAEEF0E01491112309F4C2C10F
+:101B700081110E94B900E12FF0E0EE0FFF1FEF533B
+:101B8000FF4FA591B4918C91082391E080E009F07A
+:101B900090E0092F182F8091790290917A02801796
+:101BA0009107A9F10130110509F0A7C182E891E080
+:101BB0000E940A0920E030E040E05FE360912103E9
+:101BC0007091220380912303909124030E940E15AB
+:101BD0006B017C01BC01A6018EE793E00E94EB063D
+:101BE000B701A6018AE393E00E94EB06609122020E
+:101BF00070912302809124029091250220E030E030
+:101C0000A9010E945E0710937A02009379028091E5
+:101C1000790290917A028130910509F09BC18FE0A1
+:101C20000E944C01BC01990F880B990B0E94AF14C4
+:101C300020E030E040EB50E40E940E1520E030EC54
+:101C40004FE754E40E94921620E030E040E05FE36A
+:101C50000E94211620E030E040E251E40E940E157F
+:101C60000E942A1820E030E040E251E40E949216DF
+:101C70006B017C0120E030E040E85FE30E945C17EC
+:101C800020E030E040E05FE30E94251887FD55C169
+:101C9000C701B6010E943A1723E333E343E75FE34A
+:101CA0000E9421166B017C0180900001909001013F
+:101CB000A0900201B09003014090220250902302B4
+:101CC0006090240270902502AC019B01C501B40113
+:101CD0000E94A814882331F1A7019601C501B4011F
+:101CE0000E942016A30192010E940E159B01AC01D7
+:101CF000609116027091170280911802909119025A
+:101D00000E942116609316027093170280931802A6
+:101D100090931902C0920001D0920101E092020159
+:101D2000F09203018EE00E944C01BC01990F880BD8
+:101D3000990B0E94AF1420E030E040E05FE30E9486
+:101D40000E1520E030EC4FE754E40E94921620E09C
+:101D500030E0A9010E94211620E030E040EA51E481
+:101D60000E940E150E942A1820E030E040EA51E45B
+:101D70000E9492166B017C01AC019B0160E070E057
+:101D800080E89FE30E94201620E030E040E05FE31F
+:101D90000E940E154B015C01AC019B01C701B6010D
+:101DA0000E9421166B8F7C8F8D8F9E8F2CE739EDD3
+:101DB00040E25DE3C301B2010E9421166B017C0188
+:101DC0002BED3FE049EC50E40E94921660931A021A
+:101DD00070931B0280931C0290931D02809100015E
+:101DE00090910101A0910201B09103018B8B9C8B1A
+:101DF000AD8BBE8B8091160290911702A0911802B4
+:101E0000B09119028F8B988FA98FBA8FA30192017D
+:101E10006B897C898D899E890E940E152F89388D4A
+:101E2000498D5A8D0E9421160E948D169B01AC018E
+:101E3000C501B4010E940E152B8D3C8D4D8D5E8D1C
+:101E40000E94211660931E0270931F02809320024D
+:101E5000909321022B893C894D895E89C701B60187
+:101E60000E940E152F89388D498D5A8D0E9421169A
+:101E70000E948D169B01AC01C501B4010E940E1594
+:101E80002B8D3C8D4D8D5E8D0E9421164B015C018A
+:101E900020E030E0A9010E94A81487FD57C020E08F
+:101EA00030E040E85FE3C501B4010E942518181630
+:101EB00034F4812C912C30E8A32E3FE3B32EA501FE
+:101EC0009401C701B6010E945E07C0922202D0921F
+:101ED0002302E0922402F092250280E090E0892B18
+:101EE00009F43ACE0E940802882309F435CE0E94F4
+:101EF000000032CE01E010E04ECE89E991E00E9470
+:101F00000A09C0902103D0902203E0902303F090AF
+:101F10002403B701A6018EE793E00E94EB06B70108
+:101F2000A6018AE393E00E94EB0681E0809304011E
+:101F300089EA91E00E940A0959CEC701B6010E94C0
+:101F40003A172DEC3CEC4CEC5DE3AACE812C912CA5
+:101F50005401B5CF892B09F684E892E00E94FC0178
+:101F6000181619060CF0E7C16FE371E0CE010D966B
+:101F70000E94A60A0E94AE0197FD1EC08A309105FC
+:101F8000D9F089831A8209891A890F5F1F4FB80116
+:101F9000CE010D960E94530A882361F32D853E855C
+:101FA00089899A89BE016F5F7F4F820F931F0E94BC
+:101FB0001C1B1A8B098BDECF65EB71E0CE010D96F1
+:101FC0000E94190A811162C06AEB71E0CE010D9680
+:101FD0000E94190A81115AC066EC71E0CE010D967B
+:101FE0000E94190A811152C062ED71E0CE01019682
+:101FF0000E94A60ABE016F5F7F4FCE010D960E9420
+:10200000380A10E0811127C06CED71E0CE0107960F
+:102010000E94A60A29893A894B855C8524173507D1
+:1020200008F41FC38D859E85009709F41AC36F813C
+:1020300078856115710509F414C3241B350B820FD3
+:10204000931F0E94131B11E0892B09F410E0CE01AD
+:1020500007960E942E0ACE0101960E942E0A112395
+:10206000A9F08EED91E00E94FD0849895A896D859D
+:102070007E8584E892E00E945E018DE391E00E94FB
+:10208000FD08CE010D960E942E0A27CF6AEB71E063
+:10209000CE010D960E94190A882381F081E0809379
+:1020A000040189EA91E00E940A0989EA91E00E940C
+:1020B0000A0988EE91E00E940A09E3CF65EB71E01E
+:1020C000CE010D960E94190A882319F00E94180962
+:1020D000D8CF62ED71E0CE0101960E94A60ABE0142
+:1020E0006F5F7F4FCE010D960E94380A182FCE01E8
+:1020F00001960E942E0A112309F477C009891A89D2
+:102100000115110509F46EC0ED84FE8460E270E0F3
+:10211000C7010E94081B009709F464C0AC014E1966
+:102120005F094F3F540709F45DC04F5F5F4F98014F
+:10213000BE01635F7F4FCE0101960E94B70A89817D
+:102140009A81009709F44BC00E9433136B017C0104
+:1021500020E030E040E85FE30E94251887FD3FC0A3
+:1021600020E030E048EC52E4C701B6010E94A81418
+:102170001816ACF120E030E048EC52E4C701B6019B
+:102180000E94921620E030E04AE754E40E940E15C7
+:102190000E9404170E94AF146B017C01C0922103BE
+:1021A000D0922203E0922303F0922403BC01A60103
+:1021B0008EE793E00E94EB06B701A6018AE393E065
+:1021C0000E94EB068EEE91E00E940A0980E891E001
+:1021D0000E940A09CE0101960E942E0A52CF88EF72
+:1021E00091E0F6CF86E092E066CF8091780281118F
+:1021F0009EC028E2222E22E0322E10E000E0D12CF8
+:10220000C12C6CED71E0CE0101960E94A60A89896D
+:102210009A890817190770F48D849E8469817A81E0
+:10222000C401800F911F0E94311B7C01E818F9083E
+:10223000892B19F4EE24EA94FE2CCE0101960E941B
+:102240002E0AAFEFEA16FA0609F46AC09701A80150
+:10225000BE01635F7F4FCE0107960E94B70A8B8550
+:102260009C85892B61F08F8098846CE270E0C401BA
+:102270000E94081B8C0108191909892B11F40FEF12
+:102280001FEF980150E040E0BE01695F7F4FCE0133
+:1022900001960E94B70A89819A81412C512C320102
+:1022A000009721F00E9433132B013C01CE010196CF
+:1022B0000E942E0A2B853C85A8014F5F5F4FBE010F
+:1022C000695F7F4FCE0101960E94B70A89819A818A
+:1022D000812C912C5401009721F00E9433134B0163
+:1022E0005C01CE0101960E942E0AF10140825182CA
+:1022F0006282738284829582A682B782BFEFCB1AF4
+:10230000DB0A87010F5F1F4FCE0107960E942E0A3E
+:10231000E8E02E0E311CFAE0CF16D10409F071CF9F
+:10232000D0922702C092260281E080937802CE01EB
+:102330000D960E942E0A80917802882309F4CDCD53
+:102340008091260290912702181619060CF0C5CD2F
+:102350008091220290912302A0912402B091250243
+:102360008B8B9C8BAD8BBE8B80911E0290911F023C
+:10237000A0912002B09121028F8B988FA98FBA8FE4
+:1023800088E2682E82E0782E512C412C8091260222
+:1023900090912702481659060CF058C1809104010B
+:1023A000882309F4C4C0C0902802D0902902E0908C
+:1023B0002A02F0902B022AEA37E24FE155E4C701E6
+:1023C000B6010E940E150E94041760938E0370934D
+:1023D0008F03809390039093910360939203709383
+:1023E000930380939403909395031092B2031092F9
+:1023F000B3031092B4031092B503109281031092AC
+:102400008203109283031092840310929603109219
+:102410009703109298031092990320E030E04AE766
+:1024200056E460911A0270911B0280911C029091F7
+:102430001D020E940E1520E030E040E251E40E94AF
+:1024400092164B015C0160914A0370914B0380919D
+:102450004C0390914D030E94AF149B01AC01C50148
+:10246000B4010E9421160E94041760934A037093DE
+:102470004B0380934C0390934D0360934E037093F2
+:102480004F03809350039093510310926E03109268
+:102490006F03109270031092710310923D0310921B
+:1024A0003E0310923F031092400310925203109289
+:1024B00053031092540310925503C0922202D092FB
+:1024C0002302E0922402F092250210921A02109246
+:1024D0001B0210921C0210921D0220912C023091BE
+:1024E0002D0240912E0250912F02C701B6010E9489
+:1024F0005E0710920401D3018D919D910D90BC91C6
+:10250000A02D8B8B9C8BAD8BBE8BD30114968D91A4
+:102510009D910D90BC91A02D8F8B988FA98FBA8F14
+:10252000BFEF4B1A5B0AE8E06E0E711C2FCF2B89B0
+:102530003C894D895E89D3016D917D918D919C91EE
+:102540000E9420166B8F7C8F8D8F9E8F2F89388DE8
+:10255000498D5A8DF30164817581868197810E942E
+:1025600020166B017C012B8D3C8D4D8D5E8DCA013B
+:10257000B9010E940E154B015C01A7019601C7012C
+:10258000B6010E940E159B01AC01C501B4010E9469
+:1025900021160E94541820E030E0A9010E949216F2
+:1025A0000E9404178B011616170614F001E010E0C4
+:1025B000312C212CC801012E000CAA0BBB0B8F8FD4
+:1025C00098A3A9A3BAA3B101032C000C880B990B03
+:1025D0000E94AF144B015C016F8D78A189A19AA173
+:1025E0000E94AF149B01AC01C501B4010E94921678
+:1025F0004B015C01AC019B01C701B6010E940E15A5
+:102600002F89388D498D5A8D0E9421166BA37CA38A
+:102610008DA39EA3A50194016B8D7C8D8D8D9E8DC8
+:102620000E940E152B893C894D895E890E942116D6
+:102630002BA13CA14DA15EA10E945E079FEF291A2C
+:10264000390A021513050CF0BECF55CF109278024F
+:10265000109227021092260280E891E00E940A0957
+:102660003CCC11E0F4CC662777270C943713B0E00C
+:10267000A0E0EDE3F3E10C94E0155C017B01611552
+:10268000710519F0DB018D939C9385010F5F1F4F3E
+:10269000F501D0818D2F90E00E9487146C01892B69
+:1026A000B9F5DD32B9F50F5F1F4FD5011196DC91F9
+:1026B000C1E05801F1E0AF1AB10843E050E06EE824
+:1026C00070E0C5010E949014892B69F5680182E0D1
+:1026D000C80ED11C45E050E069E870E0C6010E94D8
+:1026E0009014892B21F4680197E0C90ED11CE114E4
+:1026F000F10419F0D701CD92DC9260E070E080E83F
+:102700009FEFC111FFC060E070E080E89FE7FAC072
+:102710005801BBCFDB3229F485010E5F1F4FF50155
+:10272000D181C0E0C6CF43E050E066E870E0C5016B
+:102730000E949014892BE9F0F80110E000E020E0FD
+:1027400030E0A9015F01B0ED8B2E8D0E89E0881578
+:10275000C8F19C2E689491F88C2F8870C2FF16C027
+:10276000811102C00F5F1F4F3196D501DC91C92D39
+:10277000E9CFE114F10429F00E5F1F4FF701118337
+:10278000008360E070E080EC9FE7BCC0882311F01C
+:1027900001501109A5E0B0E00E94CF159B01AC01EA
+:1027A000220F331F441F551F280D311D411D511D80
+:1027B000283999E93907490799E15907A8F2C6600C
+:1027C0009C2ED2CFAEEF8A1206C0C3FD3CC09C2E19
+:1027D000689493F8C9CFDF7DD534A9F580818D3217
+:1027E00039F4C061DF011296818162E070E006C0B9
+:1027F000DF018B32C1F3119661E070E080535D011F
+:10280000A61AB70A8A30F8F4E0E8CE16ECE0DE0645
+:102810005CF4B601660F771F660F771FC60ED71ED2
+:10282000CC0CDD1CC80ED11C5D01FFEFAF1ABF0A36
+:102830008C9180538A30A8F1C4FF03C0D194C19415
+:10284000D1080C0D1D1DC1FF09C0E114F10431F0C8
+:1028500081E0A81AB108D701AD92BC92CA01B901B2
+:102860000E94AD14C370C33009F490584B015C0151
+:1028700020E030E0A9010E94A814882309F440C098
+:10288000CDEBD0E017FF05C0119501951109C5EA00
+:10289000D0E06E01B8E1CB1AD10880E2E82EF12C2D
+:1028A0000FC0D501B1CFFE012591359145915491CD
+:1028B0000E191F09C501B4010E940E154B015C01E0
+:1028C000D501C4010E151F0574F72497F594E794FC
+:1028D000CC16DD06A9F78A2F880F8B2F881F8F3F14
+:1028E00049F020E030E0A901C501B4010E94A8141C
+:1028F000811106C082E290E09093C3038093C203EB
+:10290000C501B401CDB7DEB7ECE00C94FC15911114
+:102910000C947B15803219F089508550C8F70895C2
+:10292000FB01DC014150504088F08D9181341CF056
+:102930008B350CF4805E659161341CF06B350CF4C2
+:10294000605E861B611171F3990B0895881BFCCFA3
+:102950000E94EA1408F481E00895E89409C097FB06
+:102960003EF490958095709561957F4F8F4F9F4F66
+:102970009923A9F0F92F96E9BB279395F6958795AA
+:1029800077956795B795F111F8CFFAF4BB0F11F46D
+:1029900060FF1BC06F5F7F4F8F4F9F4F16C0882314
+:1029A00011F096E911C0772321F09EE8872F762F4A
+:1029B00005C0662371F096E8862F70E060E02AF08B
+:1029C0009A95660F771F881FDAF7880F9695879577
+:1029D00097F90895990F0008550FAA0BE0E8FEEF4C
+:1029E00016161706E807F907C0F012161306E407D3
+:1029F000F50798F0621B730B840B950B39F40A26CC
+:102A000061F0232B242B252B21F408950A2609F4A9
+:102A1000A140A6958FEF811D811D08950E9421156B
+:102A20000C9495150E94871538F00E948E1520F0A1
+:102A3000952311F00C947E150C94841511240C949C
+:102A4000C9150E94A61570F3959FC1F3950F50E02C
+:102A5000551F629FF001729FBB27F00DB11D639F50
+:102A6000AA27F00DB11DAA1F649F6627B00DA11DF6
+:102A7000661F829F2227B00DA11D621F739FB00D9C
+:102A8000A11D621F839FA00D611D221F749F33270C
+:102A9000A00D611D231F849F600D211D822F762FA5
+:102AA0006A2F11249F5750409AF0F1F088234AF082
+:102AB000EE0FFF1FBB1F661F771F881F91505040EE
+:102AC000A9F79E3F510580F00C947E150C94C91512
+:102AD0005F3FE4F3983ED4F3869577956795B79575
+:102AE000F795E7959F5FC1F7FE2B880F911D96958F
+:102AF000879597F9089599278827089597F99F67EB
+:102B000080E870E060E008959FEF80EC0895002475
+:102B10000A941616170618060906089500240A9442
+:102B200012161306140605060895092E0394000CC8
+:102B300011F4882352F0BB0F40F4BF2B11F460FF57
+:102B400004C06F5F7F4F8F4F9F4F089557FD905880
+:102B5000440F551F59F05F3F71F04795880F97FB61
+:102B6000991F61F09F3F79F087950895121613061B
+:102B70001406551FF2CF4695F1DF08C0161617064A
+:102B80001806991FF1CF869571056105089408957F
+:102B9000E894BB2766277727CB0197F908950E9411
+:102BA0001116A59F900DB49F900DA49F800D911DAF
+:102BB000112408952F923F924F925F926F927F92CD
+:102BC0008F929F92AF92BF92CF92DF92EF92FF923D
+:102BD0000F931F93CF93DF93CDB7DEB7CA1BDB0BE9
+:102BE0000FB6F894DEBF0FBECDBF09942A8839888E
+:102BF00048885F846E847D848C849B84AA84B98495
+:102C0000C884DF80EE80FD800C811B81AA81B981A0
+:102C1000CE0FD11D0FB6F894DEBF0FBECDBFED01B4
+:102C20000895A29FB001B39FC001A39F700D811DA5
+:102C30001124911DB29F700D811D1124911D0895C5
+:102C40005058BB27AA270E9438160C9495150E944D
+:102C5000871538F00E948E1520F039F49F3F19F443
+:102C600026F40C9484150EF4E095E7FB0C947E1585
+:102C7000E92F0E94A61558F3BA1762077307840755
+:102C8000950720F079F4A6F50C94C8150EF4E0959C
+:102C90000B2EBA2FA02D0B01B90190010C01CA0116
+:102CA000A0011124FF27591B99F0593F50F4503EC1
+:102CB00068F11A16F040A22F232F342F4427585FB3
+:102CC000F3CF469537952795A795F0405395C9F7CB
+:102CD0007EF41F16BA0B620B730B840BBAF0915083
+:102CE000A1F0FF0FBB1F661F771F881FC2F70EC022
+:102CF000BA0F621F731F841F48F4879577956795F5
+:102D0000B795F7959E3F08F0B0CF9395880F08F0E0
+:102D10009927EE0F9795879508950E94D017E39510
+:102D20000C94F9170E94A6160C9495150E948E1506
+:102D300058F00E94871540F029F45F3F29F00C9469
+:102D40007E1551110C94C9150C9484150E94A6157A
+:102D500068F39923B1F3552391F3951B550BBB27CA
+:102D6000AA2762177307840738F09F5F5F4F220F0F
+:102D7000331F441FAA1FA9F335D00E2E3AF0E0E806
+:102D800032D091505040E695001CCAF72BD0FE2F50
+:102D900029D0660F771F881FBB1F261737074807E4
+:102DA000AB07B0E809F0BB0B802DBF01FF2793589C
+:102DB0005F4F3AF09E3F510578F00C947E150C94CD
+:102DC000C9155F3FE4F3983ED4F3869577956795F0
+:102DD000B795F7959F5FC9F7880F911D96958795D1
+:102DE00097F90895E1E0660F771F881FBB1F6217F0
+:102DF00073078407BA0720F0621B730B840BBA0BAE
+:102E0000EE1F88F7E09508950E940B176894B111A2
+:102E10000C94C91508950E94AE1588F09F5798F03C
+:102E2000B92F9927B751B0F0E1F0660F771F881FCF
+:102E3000991F1AF0BA95C9F714C0B13091F00E94E9
+:102E4000C815B1E008950C94C815672F782F88270E
+:102E5000B85F39F0B93FCCF3869577956795B39510
+:102E6000D9F73EF490958095709561957F4F8F4F7F
+:102E70009F4F08950E940D1890F09F3748F49111CC
+:102E800016F00C94C91560E070E080E89FEB08959F
+:102E900026F41B16611D711D811D0C94A5170C9441
+:102EA000C0170E94871520F019F00E948E1550F46B
+:102EB0000C9484150C94C915E92F0E94A61588F36B
+:102EC0005523B1F3E7FB6217730784079507A8F151
+:102ED00089F3E92FFF2788232AF03197660F771FA0
+:102EE000881FDAF7952F5527442332F09150504030
+:102EF000220F331F441FD2F7BB27E91BF50B621BC0
+:102F0000730B840BB109B1F222F4620F731F841F9B
+:102F1000B11D31972AF0660F771F881FBB1FEFCFB7
+:102F20009150504062F041F0882332F0660F771FD5
+:102F3000881F91505040C1F793950C94C017869507
+:102F4000779567959F5FD9F7F7CF882371F477233B
+:102F500021F09850872B762F07C0662311F499270C
+:102F60000DC09051862B70E060E02AF09A95660FB4
+:102F7000771F881FDAF7880F9695879597F9089538
+:102F80009F3F31F0915020F4879577956795B795DD
+:102F9000880F911D9695879597F908950C9484153F
+:102FA0000E94AE15D8F3E894E0E0BB279F57F0F0FD
+:102FB0002AED3FE049EC06C0EE0FBB0F661F771FFE
+:102FC000881F28F0B23A62077307840728F0B25AC4
+:102FD000620B730B840BE3959A9572F7803830F48B
+:102FE0009A95BB0F661F771F881FD2F790480C94E5
+:102FF000C217EF93E0FF07C0A2EA2AED3FE049ECD9
+:103000005FEB0E9438160E9495150F90039401FC07
+:103010009058E8E6F0E00C949B180E94AE15A0F0E2
+:10302000BEE7B91788F4BB279F3860F41616B11DA8
+:10303000672F782F8827985FF7CF869577956795C4
+:10304000B11D93959639C8F308950E94EA1408F4C7
+:103050008FEF08950E94AE15E8F09E37E8F096399C
+:10306000B8F49E3848F4672F782F8827985FF9CFF7
+:1030700086957795679593959539D0F3B62FB1706E
+:103080006B0F711D811D20F487957795679593953A
+:103090000C94A5170C94C0170C94C91519F416F4C8
+:1030A0000C9484150C94C0170E94AE15B8F39923A4
+:1030B000C9F3B6F39F57550B87FF0E94941800245D
+:1030C000A0E640EA900180585695979528F4805CD8
+:1030D000660F771F881F20F026173707480730F440
+:1030E000621B730B840B202931294A2BA695179458
+:1030F0000794202531254A2758F7660F771F881F28
+:1031000020F026173707480730F4620B730B840B47
+:10311000200D311D411DA09581F7B901842F9158D3
+:10312000880F96958795089591505040660F771FA8
+:10313000881FD2F708959F938F937F936F93FF9388
+:10314000EF939B01AC010E940E15EF91FF910E943D
+:10315000AF182F913F914F915F910C940E15DF9313
+:10316000CF931F930F93FF92EF92DF927B018C011D
+:10317000689406C0DA2EEF010E942115FE01E89442
+:10318000A5912591359145915591A6F3EF010E94A6
+:103190003816FE019701A801DA9469F7DF90EF90E5
+:1031A000FF900F911F91CF91DF910895052E97FB0E
+:1031B0001EF400940E94ED1857FD07D00E94FB18E2
+:1031C00007FC03D04EF40C94ED18509540953095C3
+:1031D00021953F4F4F4F5F4F089590958095709583
+:1031E00061957F4F8F4F9F4F0895EE0FFF1F059002
+:1031F000F491E02D0994A1E21A2EAA1BBB1BFD013C
+:103200000DC0AA1FBB1FEE1FFF1FA217B307E407C5
+:10321000F50720F0A21BB30BE40BF50B661F771F1D
+:10322000881F991F1A9469F76095709580959095FD
+:103230009B01AC01BD01CF0108950F931F93CF9364
+:10324000DF938230910510F482E090E0E091C603B4
+:10325000F091C70330E020E0B0E0A0E0309799F4AF
+:103260002115310509F44AC0281B390B24303105DA
+:10327000D8F58A819B816115710589F1FB019383E2
+:103280008283FE0111C04081518102811381481760
+:103290005907E0F04817590799F4109761F0129612
+:1032A0000C93129713961C933296CF01DF91CF9116
+:1032B0001F910F9108950093C6031093C703F4CF95
+:1032C0002115310551F04217530738F0A901DB01F0
+:1032D0009A01BD01DF01F801C1CFEF01F9CF909351
+:1032E000C7038093C603CDCFFE01E20FF31F819386
+:1032F00091932250310939832883D7CF2091C40379
+:103300003091C503232B41F420910701309108012E
+:103310003093C5032093C403209105013091060129
+:103320002115310541F42DB73EB740910901509167
+:103330000A01241B350BE091C403F091C503E21789
+:10334000F307A0F42E1B3F0B2817390778F0AC01C8
+:103350004E5F5F4F2417350748F04E0F5F1F5093A5
+:10336000C5034093C403819391939FCFF0E0E0E0C5
+:103370009CCFCF93DF930097E9F0FC013297138243
+:103380001282A091C603B091C703ED0130E020E0A6
+:103390001097A1F420813181820F931F2091C403E3
+:1033A0003091C5032817390709F061C0F093C503B0
+:1033B000E093C403DF91CF910895EA01CE17DF07B0
+:1033C000E8F54A815B819E0141155105B1F7E9019C
+:1033D000FB83EA8349915991C40FD51FEC17FD0770
+:1033E00061F4808191810296840F951FE901998390
+:1033F0008883828193819B838A83F0E0E0E0129648
+:103400008D919C9113970097B9F52D913C9111974F
+:10341000CD010296820F931F2091C4033091C50302
+:103420002817390739F6309751F51092C7031092D3
+:10343000C603B093C503A093C403BCCFD383C28398
+:1034400040815181840F951FC817D90761F44E5FE1
+:103450005F4F88819981480F591F518340838A812A
+:103460009B81938382832115310509F0B0CFF093BE
+:10347000C703E093C6039ECFFD01DC01C0CF1382DA
+:103480001282D7CFB0E0A0E0E8E4FAE10C94DC15BA
+:103490008C01009751F4CB010E941D198C01C801C9
+:1034A000CDB7DEB7E0E10C94F815FC01E60FF71F8D
+:1034B0009C0122503109E217F30708F49DC0D9019D
+:1034C000CD91DC911197C617D70798F0C530D1057B
+:1034D00030F3CE0104978617970708F3C61BD70B66
+:1034E0002297C193D1936D937C93CF010E94B91918
+:1034F000D6CF5B01AC1ABD0A4C018C0E9D1EA0916B
+:10350000C603B091C703512C412CF12CE12C10972C
+:1035100031F58091C4039091C5038815990509F090
+:103520005CC04616570608F058C08091050190917E
+:103530000601009741F48DB79EB740910901509163
+:103540000A01841B950BE817F90708F055C0F093A2
+:10355000C503E093C403F90171836083A0CF8D910B
+:103560009C91119712966C90129713967C901397DA
+:10357000A815B90559F56C0142E0C40ED11CCA1456
+:10358000DB0420F1AC014A195B09DA0112961597A8
+:1035900080F06282738251834083D9016D937C9362
+:1035A000E114F10471F0D7011396FC93EE93129796
+:1035B00076CF22968C0F9D1FF90191838083F301B2
+:1035C000EFCFF093C703E093C60369CF48165906BF
+:1035D00008F42C017D01D3019ACFCB010E941D1963
+:1035E0007C01009749F0AE01B8010E94FF1AC801A2
+:1035F0000E94B919870153CF10E000E050CFFB01C2
+:10360000DC0102C001900D9241505040D8F708955E
+:10361000FC018191861721F08823D9F79927089515
+:103620003197CF010895FB01DC018D910190801944
+:103630000110D9F3990B0895FB01DC0101900D9263
+:103640000020E1F70895FB01DC014150504030F0CB
+:103650008D910190801919F40020B9F7881B990BFE
+:103660000895FB0151915523A9F0BF01DC014D9153
+:1036700045174111E1F759F4CD010190002049F0BF
+:103680004D9140154111C9F3FB014111EFCF81E08C
+:0A36900090E001970895F894FFCF31
+:10369A00CDCC3C40010000C8038000000000003E81
+:1036AA00025E018B018B02FC019A01EE010000000F
+:1036BA0000B3037F035D07360A920514055205FE1F
+:1036CA0004CC04AB04770456040004D1030D0A00A9
+:1036DA00484F4D494E4700484F4D4544005461629A
+:1036EA006C653A2044756E6520576561766572008F
+:1036FA00447269766572733A20544D4332323039D6
+:10370A000056657273696F6E3A20312E342E30007E
+:10371A005200537069726F6772617068204D6F64EE
+:10372A00652041637469766500417070204D6F644D
+:10373A0065204163746976650054484554415F5277
+:10374A004553455400484F4D450052455345545F33
+:10375A005448455441004745545F56455253494FD2
+:10376A004E005345545F5350454544003B0049477A
+:10377A004E4F5245443A2000524541445900535055
+:10378A004545445F53455400494E56414C49445FB0
+:10379A00535045454400494E56414C49445F434FB6
+:0637AA004D4D414E4400AC
+:00000001FF

+ 5 - 2
esp32/esp32.ino → firmware/esp32/esp32.ino

@@ -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;
 }
@@ -78,7 +82,7 @@ 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;
@@ -105,7 +109,6 @@ void loop()
                     inOutStepper.setMaxSpeed(newSpeed);
 
                     Serial.println("SPEED_SET");  
-                    Serial.println("R");
                 }
                 else
                 {

BIN
firmware/esp32/esp32.ino.bin


+ 2 - 1
requirements.txt

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

Разница между файлами не показана из-за своего большого размера
+ 5 - 0
static/css/all.min.css


+ 458 - 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,37 @@ 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;
+}
+
+.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 +373,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 +401,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 +445,6 @@ section .header h2 {
 section .header .add-button {
     height: 35px;
     width: 35px;
-    font-size: 1.5rem;
     padding: 0;
 }
 
@@ -343,11 +463,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 +489,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 +544,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 +601,6 @@ section .header .add-button {
     transition: background 0.3s ease;
 }
 
-.rename-button:hover {
-    background: #285A8E;
-}
-
 /* Bottom Navigation */
 .bottom-nav {
     display: flex;
@@ -487,6 +616,7 @@ section .header .add-button {
 
 .tab-button {
     flex: 1;
+    height: 60px;
     padding: 20px 10px;
     text-align: center;
     font-size: 1rem;
@@ -512,10 +642,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 +704,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 +716,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 +756,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 +771,162 @@ button#debug_button.active {
     max-width: calc(100vw - 30px);
 }
 
+
+/* 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: scroll;
+}
+
+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 +1020,9 @@ button#debug_button.active {
     background-color: var(--color-error);
 }
 
-
 #serial_ports_buttons {
-    display: inline-block;
+    display: flex;
+    gap: 10px;
 }
 
 .status.connected {
@@ -720,11 +1082,6 @@ input[type="number"]:focus {
     min-width: 200px;
 }
 
-#serial_status_container,
-#serial_ports_buttons {
-    display: inline-block;
-}
-
 /* Notification Styles */
 .notification {
     display: flex;
@@ -740,7 +1097,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 +1106,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 +1168,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 +1182,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 +1196,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 +1215,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;
+    }
+
 }

Разница между файлами не показана из-за своего большого размера
+ 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>

+ 478 - 64
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', {
@@ -244,7 +276,7 @@ async function stopExecution() {
 let isPaused = false;
 
 function togglePausePlay() {
-    const button = document.getElementById("pausePlayButton");
+    const button = document.getElementById("pausePlayCurrent");
 
     if (isPaused) {
         // Resume execution
@@ -253,7 +285,7 @@ function togglePausePlay() {
             .then(data => {
                 if (data.success) {
                     isPaused = false;
-                    button.innerHTML = ""; // Change to pause icon
+                    button.innerHTML = "<i class=\"fa-solid fa-pause\"></i>"; // Change to pause icon
                 }
             })
             .catch(error => console.error("Error resuming execution:", error));
@@ -264,7 +296,7 @@ function togglePausePlay() {
             .then(data => {
                 if (data.success) {
                     isPaused = true;
-                    button.innerHTML = ""; // Change to play icon
+                    button.innerHTML = "<i class=\"fa-solid fa-play\"></i>"; // Change to play icon
                 }
             })
             .catch(error => console.error("Error pausing execution:", error));
@@ -321,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', {
@@ -333,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;
@@ -397,7 +450,6 @@ function renderPattern(coordinates) {
         else ctx.lineTo(x, y);
     });
     ctx.stroke();
-    logMessage('Pattern preview rendered.');
 }
 
 
@@ -465,6 +517,37 @@ async function runClearOut() {
     await runFile('clear_from_out.thr');
 }
 
+async function runClearSide() {
+    await runFile('side_wiper.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();
@@ -482,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()"]');
@@ -501,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');
@@ -515,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';
 
@@ -548,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 {
@@ -585,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
@@ -605,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;
@@ -657,6 +952,9 @@ function openPlaylistEditor(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
@@ -751,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) {
@@ -835,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;
@@ -851,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
@@ -893,6 +1199,7 @@ async function confirmAddPlaylist() {
 
             // Refresh the playlist list
             loadAllPlaylists();
+            populatePlaylistDropdown();
 
             // Hide the add playlist container
             toggleSecondaryButtons('add-playlist-container');
@@ -985,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);
         }
@@ -1026,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) {
@@ -1041,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) {
@@ -1056,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);
@@ -1075,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);
@@ -1203,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}`);
@@ -1225,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.');
@@ -1246,6 +1580,72 @@ function attachFullScreenListeners() {
     });
 }
 
+let lastPreviewedFile = null; // Track the last previewed file
+
+async function updateCurrentlyPlaying() {
+    try {
+        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 last previewed 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)}%`;
+            } 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 toggleSettings() {
+    const settingsContainer = document.getElementById('settings-container');
+    if (settingsContainer) {
+        settingsContainer.classList.toggle('open');
+    }
+}
 
 // Utility function to manage cookies
 function setCookie(name, value, days) {
@@ -1266,28 +1666,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.');
 }
@@ -1321,16 +1726,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.');
@@ -1345,16 +1755,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 => {
@@ -1390,7 +1800,11 @@ 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();
+    fetchFirmwareInfo();
+    checkForUpdates();
+
+    // Periodically check for currently playing status
+    setInterval(updateCurrentlyPlaying, 3000);
 });

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


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


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


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


+ 233 - 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,33 @@
 
             <!-- 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>
+                <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 +96,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,9 +150,9 @@
                         <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="clear_in">Clear From Center</option>
+                            <option value="clear_out">Clear From Perimeter</option>
+                            <option value="clear_sideway">Clear Sideways</option>
                             <option value="random">Random</option>
                         </select>
                     </div>
@@ -131,9 +167,10 @@
                         <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>
-                <button onclick="clearSchedule()" class="cancel">Clear</button>
-
             </div>
         </section>
 
@@ -141,26 +178,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>
@@ -168,34 +230,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 Execution</button>
-                <button id="pausePlayButton" onclick="togglePausePlay()" class="cancel">⏸</button> <!-- Default: Pause Icon -->
-                <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-solid 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">
@@ -208,26 +268,19 @@
                     <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>
+                <select id="pre_execution" name="pre_execution">
+                    <option value="none" selected>None</option>
+                    <option value="clear_in">Clear From Center</option>
+                    <option value="clear_out">Clear From Perimeter</option>
+                    <option value="clear_sideway">Clear Sideway</option>
+                </select>
             </div>
             <div class="control-group">
                 <h3>Speed</h3>
@@ -235,28 +288,129 @@
                     <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>
+                        </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>
@@ -266,6 +420,6 @@
     <!-- Messages will be appended here -->
 </div>
 
-<script src="../static/main.js"></script>
+<script src="../static/js/main.js"></script>
 </body>
 </html>

Некоторые файлы не были показаны из-за большого количества измененных файлов