Tuan Nguyen пре 1 година
родитељ
комит
8f58119bdc
4 измењених фајлова са 276 додато и 133 уклоњено
  1. 206 11
      app.py
  2. 1 106
      static/style.css
  3. 7 16
      templates/index.html
  4. 62 0
      templates/playlist.html

+ 206 - 11
app.py

@@ -6,6 +6,7 @@ import random
 import threading
 import serial.tools.list_ports
 import math
+import json
 
 app = Flask(__name__)
 
@@ -25,6 +26,13 @@ ser_port = None  # Global variable to store the serial port name
 stop_requested = False
 serial_lock = threading.Lock()
 
+PLAYLISTS_FILE = os.path.join(os.getcwd(), "playlists.json")
+
+# 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 list_serial_ports():
     """Return a list of available serial ports."""
     ports = serial.tools.list_ports.comports()
@@ -182,7 +190,6 @@ def get_clear_pattern_file(pattern_name):
 def run_theta_rho_files(
     file_paths,
     pause_time=0,
-    clear_between_files=False,
     clear_pattern=None
 ):
     """
@@ -213,15 +220,12 @@ def run_theta_rho_files(
                 print("Execution stopped before running the next pattern or clear.")
                 break
 
-            # Conditionally run a "clear" pattern
-            if clear_between_files:
-                # If user explicitly wants no clear pattern, skip
-                if not clear_pattern:
-                    continue
-                # Otherwise, get the file (could be random or a known pattern)
-                clear_file_path = get_clear_pattern_file(clear_pattern)
-                print(f"Running clear pattern: {clear_file_path}")
-                run_theta_rho_file(clear_file_path)
+            if not clear_pattern:
+                continue
+            # Otherwise, get the file (could be random or a known pattern)
+            clear_file_path = get_clear_pattern_file(clear_pattern)
+            print(f"Running clear pattern: {clear_file_path}")
+            run_theta_rho_file(clear_file_path)
 
     print("All requested patterns completed (or stopped).")
 
@@ -335,7 +339,6 @@ def run_theta_rho():
             args=(files_to_run,),
             kwargs={
                 'pause_time': 0,
-                'clear_between_files': False,
                 'clear_pattern': None
             }
         ).start()
@@ -469,6 +472,198 @@ def serial_status():
         'connected': ser.is_open if ser else False,
         'port': ser_port  # Include the port name
     })
+    
+if not os.path.exists(PLAYLISTS_FILE):
+    with open(PLAYLISTS_FILE, "w") as f:
+        json.dump({}, f, indent=2)
+
+def load_playlists():
+    """
+    Load the entire playlists dictionary from the JSON file.
+    Returns something like: {
+        "My Playlist": ["file1.thr", "file2.thr"],
+        "Another": ["x.thr"]
+    }
+    """
+    with open(PLAYLISTS_FILE, "r") as f:
+        return json.load(f)
+
+def save_playlists(playlists_dict):
+    """
+    Save the entire playlists dictionary back to the JSON file.
+    """
+    with open(PLAYLISTS_FILE, "w") as f:
+        json.dump(playlists_dict, f, indent=2)
+
+@app.route("/list_all_playlists", methods=["GET"])
+def list_all_playlists():
+    """
+    Returns a list of all playlist names.
+    Example return: ["My Playlist", "Another Playlist"]
+    """
+    playlists_dict = load_playlists()
+    playlist_names = list(playlists_dict.keys())
+    return jsonify(playlist_names)
+
+@app.route("/get_playlist", methods=["GET"])
+def get_playlist():
+    """
+    GET /get_playlist?name=My%20Playlist
+    Returns: { "name": "My Playlist", "files": [... ] }
+    """
+    playlist_name = request.args.get("name", "")
+    if not playlist_name:
+        return jsonify({"error": "Missing playlist 'name' parameter"}), 400
+
+    playlists_dict = load_playlists()
+    if playlist_name not in playlists_dict:
+        return jsonify({"error": f"Playlist '{playlist_name}' not found"}), 404
+
+    files = playlists_dict[playlist_name]  # e.g. ["file1.thr", "file2.thr"]
+    return jsonify({
+        "name": playlist_name,
+        "files": files
+    })
+
+@app.route("/create_playlist", methods=["POST"])
+def create_playlist():
+    """
+    POST /create_playlist
+    Body: { "name": "My Playlist", "files": ["file1.thr", "file2.thr"] }
+    Creates or overwrites a playlist with the given name.
+    """
+    data = request.get_json()
+    if not data or "name" not in data or "files" not in data:
+        return jsonify({"success": False, "error": "Playlist 'name' and 'files' are required"}), 400
+
+    playlist_name = data["name"]
+    files = data["files"]
+
+    # Load all playlists
+    playlists_dict = load_playlists()
+
+    # Overwrite or create new
+    playlists_dict[playlist_name] = files
+
+    # Save changes
+    save_playlists(playlists_dict)
+
+    return jsonify({
+        "success": True,
+        "message": f"Playlist '{playlist_name}' created/updated"
+    })
+
+@app.route("/modify_playlist", methods=["POST"])
+def modify_playlist():
+    """
+    POST /modify_playlist
+    Body: { "name": "My Playlist", "files": ["file1.thr", "file2.thr"] }
+    Updates (or creates) the existing playlist with a new file list.
+    You can 404 if you only want to allow modifications to existing playlists.
+    """
+    data = request.get_json()
+    if not data or "name" not in data or "files" not in data:
+        return jsonify({"success": False, "error": "Playlist 'name' and 'files' are required"}), 400
+
+    playlist_name = data["name"]
+    files = data["files"]
+
+    # Load all playlists
+    playlists_dict = load_playlists()
+
+    # Optional: If you want to disallow creating a new playlist here:
+    # if playlist_name not in playlists_dict:
+    #     return jsonify({"success": False, "error": f"Playlist '{playlist_name}' not found"}), 404
+
+    # Overwrite or create new
+    playlists_dict[playlist_name] = files
+
+    # Save
+    save_playlists(playlists_dict)
+
+    return jsonify({"success": True, "message": f"Playlist '{playlist_name}' updated"})
+
+@app.route("/delete_playlist", methods=["DELETE"])
+def delete_playlist():
+    """
+    DELETE /delete_playlist
+    Body: { "name": "My Playlist" }
+    Removes the playlist from the single JSON file.
+    """
+    data = request.get_json()
+    if not data or "name" not in data:
+        return jsonify({"success": False, "error": "Missing 'name' field"}), 400
+
+    playlist_name = data["name"]
+
+    playlists_dict = load_playlists()
+    if playlist_name not in playlists_dict:
+        return jsonify({"success": False, "error": f"Playlist '{playlist_name}' not found"}), 404
+
+    # Remove from dict
+    del playlists_dict[playlist_name]
+    save_playlists(playlists_dict)
+
+    return jsonify({
+        "success": True,
+        "message": f"Playlist '{playlist_name}' deleted"
+    })
+    
+@app.route("/run_playlist", methods=["POST"])
+def run_playlist():
+    """
+    POST /run_playlist
+    Body (JSON):
+    {
+        "playlist_name": "My Playlist",
+        "pause_time": 1.0,                # Optional: seconds to pause between patterns
+        "clear_pattern": "random"          # Optional: "clear_in", "clear_out", "clear_sideway", or "random"
+    }
+    """
+    data = request.get_json()
+
+    # Validate input
+    if not data or "playlist_name" not in data:
+        return jsonify({"success": False, "error": "Missing 'playlist_name' field"}), 400
+
+    playlist_name = data["playlist_name"]
+    pause_time = data.get("pause_time", 0)
+    clear_pattern = data.get("clear_pattern", None)
+
+    # Validate pause_time
+    if not isinstance(pause_time, (int, float)) or pause_time < 0:
+        return jsonify({"success": False, "error": "'pause_time' must be a non-negative number"}), 400
+
+    valid_patterns = ["clear_in", "clear_out", "clear_sideway", "random"]
+    if clear_pattern not in valid_patterns and clear_pattern is not None:
+        return jsonify({"success": False, "error": f"'clear_pattern' must be one of {valid_patterns} or null"}), 400
+
+    # Load playlists
+    playlists = load_playlists()
+
+    if playlist_name not in playlists:
+        return jsonify({"success": False, "error": f"Playlist '{playlist_name}' not found"}), 404
+
+    file_paths = playlists[playlist_name]
+    file_paths = [os.path.join(THETA_RHO_DIR, file) for file in file_paths]
+
+    if not file_paths:
+        return jsonify({"success": False, "error": f"Playlist '{playlist_name}' is empty"}), 400
+
+    # Start the playlist execution in a separate thread
+    try:
+        threading.Thread(
+            target=run_theta_rho_files,
+            args=(file_paths,),
+            kwargs={
+                'pause_time': pause_time,
+                'clear_pattern': clear_pattern
+            },
+            daemon=True  # Daemonize thread to exit with the main program
+        ).start()
+        return jsonify({"success": True, "message": f"Playlist '{playlist_name}' is now running."})
+    except Exception as e:
+        return jsonify({"success": False, "error": str(e)}), 500
 
 if __name__ == '__main__':
     app.run(debug=True, host='0.0.0.0', port=8080)

+ 1 - 106
static/style.css

@@ -163,7 +163,7 @@ li.selected {
 }
 
 #theta_rho_files {
-    max-height: 507px;
+    max-height: 400px;
     overflow-y: auto;
     border: 1px solid #ddd;
     border-radius: 5px;
@@ -255,111 +255,6 @@ input[type="radio"]:checked {
     width: 80px;
 }
 
-/* Speed Control Section */
-.speed-control {
-    display: flex;
-    align-items: center;
-    gap: 15px;
-    margin-top: 10px;
-    background: #fff; /* Matches section background */
-    border: 1px solid #ddd;
-    border-radius: 8px;
-    padding: 15px;
-    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
-}
-
-.speed-control label {
-    font-weight: bold;
-    font-size: 1rem;
-    color: #333;
-    flex-shrink: 0;
-}
-
-.speed-control input[type="number"] {
-    width: 100px; /* Consistent input width */
-    padding: 8px;
-    font-size: 1rem;
-    border: 1px solid #ddd;
-    border-radius: 5px;
-    outline: none;
-    transition: all 0.3s ease;
-}
-
-.speed-control input[type="number"]:focus {
-    border-color: #4A90E2; /* Highlighted border */
-    box-shadow: 0 0 5px rgba(74, 144, 226, 0.5);
-}
-
-.speed-control button {
-    background: #4A90E2;
-    color: #fff;
-    padding: 10px 15px;
-    font-size: 1rem;
-    border: none;
-    border-radius: 5px;
-    cursor: pointer;
-    transition: all 0.3s ease;
-}
-
-.speed-control button:hover {
-    background: #357ABD;
-}
-
-#speed_status {
-    margin-top: 10px;
-    font-size: 0.9rem;
-    color: #444;
-}
-
-#serial_ports_container > * {
-    display: inline-block;
-}
-
-#serial_ports_container select {
-    margin: 10px;
-    flex-basis: 100px;
-    flex-grow: 0;
-}
-
-#serial_ports {
-    width: auto;
-    min-width: 200px;
-}
-
-#serial_status_container,
-#serial_ports_buttons {
-    display: inline-block;
-}
-
-.status.connected {
-    color: #4CAF50;
-    font-weight: bold;
-}
-
-.status.not-connected {
-    color: #E53935;
-    font-weight: bold;
-}
-
-.footer {
-    align-items: center;
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
-    margin-bottom: 20px;
-    width: 100%;
-}
-
-.footer #github {
-    align-content: center;
-    display: flex;
-    font-size: 0.8em;
-}
-
-.footer #github img {
-    margin: 0 5px
-}
-
 /* Responsive Layout for Small Screens */
 @media (max-width: 768px) {
     body {

+ 7 - 16
templates/index.html

@@ -3,12 +3,12 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Sand Table Controller</title>
+    <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">
 </head>
 <body>
-    <h1>Sand Table Controller</h1>
+    <h1>Dune Weaver Controller</h1>
     <div class="container">
         <!-- Left Column -->
         <div class="left-column">
@@ -24,6 +24,7 @@
                     <button onclick="disconnectSerial()">Disconnect</button>
                     <button onclick="restartSerial()">Restart</button>
                 </div>
+                <p id="serial_status" class="status">Status: Not connected</p>
             </div>
 
             <div class="section">
@@ -75,7 +76,8 @@
                     </label>
                 </div>
                 <div class="button-group">
-                    <button id="run_button" disabled>Run Selected Pattern</button>
+                    <button id="run_button" disabled>Run Selected File</button>
+                    <button onclick="stopExecution()" class="delete-button">Stop</button>
                 </div>
             </div>
             <div class="section">
@@ -88,20 +90,9 @@
                     </div>
                 </div>
             </div>
-        </div>
-
-        <div class="footer">
-            <div id="github">
-                <span>Help us improve! <a href="https://github.com/tuanchris/dune-weaver/pulls" target="_blank">Submit a Pull Request</a> or <a href="https://github.com/tuanchris/dune-weaver/issues/new" target="_blank">Report a Bug</a>.</span>
-                <a href="https://github.com/tuanchris/dune-weaver/issues" target="_blank">
-                    <img src="https://img.shields.io/github/issues/tuanchris/dune-weaver?style=flat-square" alt="GitHub Issues">
-                </a>
             </div>
-
-            <button id="debug_button" class="small-button" onclick="toggleDebugLog()">
-                Show Debug log
-            </button>
         </div>
+    </div>
 
         <div id="status_log">
             <!-- Messages will be appended here -->
@@ -538,7 +529,7 @@
         // Initial load of serial ports and Theta-Rho files
         loadSerialPorts();
         loadThetaRhoFiles();
-
+        
         document.getElementById('run_button').onclick = runThetaRho;
 
     </script>

+ 62 - 0
templates/playlist.html

@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Sand Table Playlist</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">
+</head>
+<body>
+    <h1>Manage and Run Playlists</h1>
+    <div class="container">
+
+        <!-- Section to build the playlist -->
+        <div class="left-column">
+            <div class="section">
+                <h2>Create Playlist</h2>
+                <p>Add .thr files to your playlist below.</p>
+                <input type="text" id="search_pattern" placeholder="Search files..." oninput="searchPatternFiles()">
+                <ul id="theta_rho_files"></ul>
+            </br>
+                <button onclick="addToPlaylist()">Add to Playlist</button>
+                <ul id="playlist_files"></ul>
+            </div>
+            <div class="section">
+                <h2>Preview</h2>
+                <canvas id="patternPreviewCanvas" style="width: 100%; height: 100%;"></canvas>
+                <p id="first_coordinate">First Coordinate: Not available</p>
+                <p id="last_coordinate">Last Coordinate: Not available</p>
+            </div>
+        </div>
+
+        <!-- Section to set parameters -->
+        <div class="right-column">
+            <div class="section">
+                <h2>Pattern Files</h2>
+                <label for="pause_time">Pause between patterns (min):</label>
+                <input type="number" id="pause_time" value="0" step="1" min="0">
+
+                <br><br>
+                <label for="clear_pattern">Clear Pattern:</label>
+                <select id="clear_pattern">
+                    <option value="no_clear">No Clear</option>
+                    <option value="clear_from_in">Clear from In</option>
+                    <option value="clear_from_out">Clear from Out</option>
+                    <option value="clear_sideway">Clear Sideway</option>
+                    <option value="random">Random</option>
+                </select>
+                <div class="button-group">
+                    <button onclick="runPlaylist()">Run Playlist</button>
+                    <button onclick="stopExecution()" class="delete-button">Stop</button>
+                </div>
+            </div>
+        </div>
+
+        <!-- Status/Log -->
+        <div id="status_log">
+            <h2>Playlist Status</h2>
+            <!-- Messages will be appended here -->
+        </div>
+    </div>
+</body>
+</html>