瀏覽代碼

Added transient message for alerts and improved code

Nicole Portas 1 月之前
父節點
當前提交
a2157f444c
共有 1 個文件被更改,包括 83 次插入43 次删除
  1. 83 43
      matrix_billboard.py

+ 83 - 43
matrix_billboard.py

@@ -254,9 +254,14 @@ class BillboardDaemon:
         self.stop_event = threading.Event()
         self.worker_thread = None
         self.lock = threading.Lock()
+        self.ambient_params = None
 
     def start_animation(self, params):
         with self.lock:
+            # If duration is 0, this request becomes our permanent ambient state
+            if float(params.get("duration", 0.0)) == 0.0:
+                self.ambient_params = dict(params)
+
             if self.worker_thread and self.worker_thread.is_alive():
                 self.stop_event.set()
                 self.worker_thread.join()
@@ -271,6 +276,7 @@ class BillboardDaemon:
 
     def stop_animation(self):
         with self.lock:
+            self.ambient_params = None
             if self.worker_thread and self.worker_thread.is_alive():
                 self.stop_event.set()
                 self.worker_thread.join()
@@ -293,6 +299,8 @@ class BillboardDaemon:
         bg_color2 = params.get("bg_color2", (0, 0, 128))
         
         brightness = max(0, min(255, params.get("brightness", 255))) / 255.0
+        duration = float(params.get("duration", 0.0))
+        end_time = (time.time() + duration) if duration > 0 else None
         
         seq = 0
         frame_idx = 0
@@ -303,6 +311,9 @@ class BillboardDaemon:
             blink = params.get("clock_blink", True)
             
             while not self.stop_event.is_set():
+                if end_time and time.time() >= end_time:
+                    break
+
                 if mode == "clock_analog":
                     base_mask = render_analog_clock(width, height)
                 else:
@@ -322,58 +333,65 @@ class BillboardDaemon:
                 
                 if self.stop_event.wait(0.1):
                     break
-            
-            clear_matrix(self.sock, ip, port, width, height)
-            return
-
-        text = params.get("text", "WOMAN CAVE")
-        speed = params.get("speed", 0.05)
-        loops = params.get("loops", 0)
-        sweep_pause = params.get("sweep_pause", 2.0)
+        else:
+            text = params.get("text", "WOMAN CAVE")
+            speed = params.get("speed", 0.05)
+            loops = params.get("loops", 0)
+            sweep_pause = params.get("sweep_pause", 2.0)
 
-        base_banner = render_base_text(text, font_path, height)
-        banner_width = base_banner.size[0]
-        loop_count = 0
+            base_banner = render_base_text(text, font_path, height)
+            banner_width = base_banner.size[0]
+            loop_count = 0
 
-        while not self.stop_event.is_set():
-            for x_pos in range(width, -banner_width - 1, -1):
-                if self.stop_event.is_set():
+            while not self.stop_event.is_set():
+                if end_time and time.time() >= end_time:
                     break
 
-                styled_banner = apply_text_effect(base_banner, effect, color1, color2, frame_idx)
-                
-                bg_layer = generate_effect_layer(width, height, bg_effect, bg_color, bg_color2, frame_idx).convert("RGB")
-                bg_layer.paste(styled_banner, (x_pos, 0), styled_banner)
-
-                if brightness < 1.0:
-                    bg_layer = ImageEnhance.Brightness(bg_layer).enhance(brightness)
-
-                send_ddp_frame(self.sock, bg_layer.tobytes(), ip, port, seq)
-                seq = (seq + 1) % 16
-                frame_idx += 1
-                
-                if self.stop_event.wait(speed):
-                    break
+                for x_pos in range(width, -banner_width - 1, -1):
+                    if self.stop_event.is_set() or (end_time and time.time() >= end_time):
+                        break
 
-            if sweep_pause > 0 and not self.stop_event.is_set():
-                pause_end = time.time() + sweep_pause
-                while time.time() < pause_end and not self.stop_event.is_set():
+                    styled_banner = apply_text_effect(base_banner, effect, color1, color2, frame_idx)
+                    
                     bg_layer = generate_effect_layer(width, height, bg_effect, bg_color, bg_color2, frame_idx).convert("RGB")
+                    bg_layer.paste(styled_banner, (x_pos, 0), styled_banner)
+
                     if brightness < 1.0:
                         bg_layer = ImageEnhance.Brightness(bg_layer).enhance(brightness)
-                        
+
                     send_ddp_frame(self.sock, bg_layer.tobytes(), ip, port, seq)
                     seq = (seq + 1) % 16
                     frame_idx += 1
                     
-                    if self.stop_event.wait(0.1):
+                    if self.stop_event.wait(speed):
                         break
 
-            loop_count += 1
-            if loops > 0 and loop_count >= loops:
-                break
+                if sweep_pause > 0 and not self.stop_event.is_set() and not (end_time and time.time() >= end_time):
+                    pause_end = time.time() + sweep_pause
+                    while time.time() < pause_end and not self.stop_event.is_set():
+                        if end_time and time.time() >= end_time:
+                            break
+                        bg_layer = generate_effect_layer(width, height, bg_effect, bg_color, bg_color2, frame_idx).convert("RGB")
+                        if brightness < 1.0:
+                            bg_layer = ImageEnhance.Brightness(bg_layer).enhance(brightness)
+                            
+                        send_ddp_frame(self.sock, bg_layer.tobytes(), ip, port, seq)
+                        seq = (seq + 1) % 16
+                        frame_idx += 1
+                        
+                        if self.stop_event.wait(0.1):
+                            break
+
+                loop_count += 1
+                if loops > 0 and loop_count >= loops:
+                    break
 
-        clear_matrix(self.sock, ip, port, width, height)
+        # State restoration check: if we stopped due to duration timeout, restore ambient mode seamlessly
+        if not self.stop_event.is_set():
+            if duration > 0 and self.ambient_params:
+                return self._render_loop(dict(self.ambient_params))
+            else:
+                clear_matrix(self.sock, ip, port, width, height)
 
 
 # --- HTML Documentation Template ---
@@ -469,6 +487,11 @@ HTML_DOCS_TEMPLATE = """<!DOCTYPE html>
                         <input type="range" id="sweep_pause" value="2.0" step="0.5" min="0.0" max="10.0">
                     </div>
 
+                    <div>
+                        <label>Alert Duration (0 = Continuous Ambient Mode): <span id="duration_val" class="val-display">0</span>s</label>
+                        <input type="range" id="duration" value="0" min="0" max="60" step="1">
+                    </div>
+
                     <div>
                         <label>Brightness (0-255): <span id="bright_val" class="val-display">255</span></label>
                         <input type="range" id="brightness" value="255" min="0" max="255" step="1">
@@ -557,6 +580,9 @@ HTML_DOCS_TEMPLATE = """<!DOCTYPE html>
         document.getElementById('sweep_pause').addEventListener('input', function() {
             document.getElementById('pause_val').innerText = this.value;
         });
+        document.getElementById('duration').addEventListener('input', function() {
+            document.getElementById('duration_val').innerText = this.value;
+        });
         document.getElementById('brightness').addEventListener('input', function() {
             document.getElementById('bright_val').innerText = this.value;
         });
@@ -654,7 +680,6 @@ HTML_DOCS_TEMPLATE = """<!DOCTYPE html>
                 el.style.display = (mode === "clock_digital") ? "grid" : "none";
             });
             
-            // Populate font dropdown strictly based on mode (monospace for digital clock)
             const fontSelect = document.getElementById("font");
             const currentFont = fontSelect.value;
             fontSelect.innerHTML = "";
@@ -683,6 +708,7 @@ HTML_DOCS_TEMPLATE = """<!DOCTYPE html>
             const mode = document.getElementById("mode").value;
             const effect = document.getElementById("effect").value;
             const bgEffect = document.getElementById("bg_effect").value;
+            const duration = parseFloat(document.getElementById("duration").value);
             
             let payload = {
                 mode: mode,
@@ -693,6 +719,7 @@ HTML_DOCS_TEMPLATE = """<!DOCTYPE html>
                 brightness: parseInt(document.getElementById("brightness").value)
             };
 
+            if (duration > 0) payload.duration = duration;
             if (effect.includes("gradient")) payload.color2 = document.getElementById("color2").value;
             if (bgEffect.includes("gradient")) payload.bg_color2 = document.getElementById("bg_color2").value;
 
@@ -725,7 +752,7 @@ HTML_DOCS_TEMPLATE = """<!DOCTYPE html>
 
         document.getElementById("mode").addEventListener("change", handleModeToggle);
         document.querySelectorAll("input, select").forEach(el => {
-            el.addEventListener("change", handleDisplayToggles);
+            el.addEventListener("change", handleModeToggle);
             el.addEventListener("input", () => {
                 updateCurlPreview();
                 updateFontPreview();
@@ -745,6 +772,7 @@ HTML_DOCS_TEMPLATE = """<!DOCTYPE html>
             const mode = document.getElementById("mode").value;
             const effect = document.getElementById("effect").value;
             const bgEffect = document.getElementById("bg_effect").value;
+            const duration = parseFloat(document.getElementById("duration").value);
             
             let payload = {
                 mode: mode,
@@ -755,6 +783,7 @@ HTML_DOCS_TEMPLATE = """<!DOCTYPE html>
                 brightness: parseInt(document.getElementById("brightness").value)
             };
             
+            if (duration > 0) payload.duration = duration;
             if (effect.includes("gradient")) payload.color2 = document.getElementById("color2").value;
             if (bgEffect.includes("gradient")) payload.bg_color2 = document.getElementById("bg_color2").value;
             
@@ -866,7 +895,7 @@ class DaemonHTTPHandler(BaseHTTPRequestHandler):
 
         if payload.get("action") in ["clear", "stop"]:
             self.daemon_instance.stop_animation()
-            return self._send_success_response("Matrix cleared and animation stopped.")
+            return self._send_success_response("Matrix cleared and ambient state wiped.")
 
         mode = str(payload.get("mode", "scroll")).lower()
         if mode not in ["scroll", "clock_digital", "clock_analog"]:
@@ -888,6 +917,13 @@ class DaemonHTTPHandler(BaseHTTPRequestHandler):
             parsed_params["clock_seconds"] = bool(payload.get("clock_seconds", True))
             parsed_params["clock_blink"] = bool(payload.get("clock_blink", True))
 
+        try:
+            parsed_params["duration"] = float(payload.get("duration", 0.0))
+            if parsed_params["duration"] < 0:
+                raise ValueError()
+        except ValueError:
+            return self._send_error_response("Field 'duration' must be a non-negative float (seconds).")
+
         effect = payload.get("effect", "solid").lower()
         if effect not in ["solid", "rainbow", "gradient-h", "gradient-v", "pulse"]:
             return self._send_error_response(f"Invalid effect '{effect}'.")
@@ -945,7 +981,9 @@ class DaemonHTTPHandler(BaseHTTPRequestHandler):
         if "port" in payload: parsed_params["port"] = int(payload["port"])
 
         self.daemon_instance.start_animation(parsed_params)
-        return self._send_success_response("Pushed payload to matrix.")
+        
+        msg = f"Temporary alert active for {parsed_params['duration']}s." if parsed_params['duration'] > 0 else "Set as permanent ambient state."
+        return self._send_success_response(msg)
 
     def _send_success_response(self, message):
         self.send_response(200)
@@ -976,6 +1014,7 @@ def main():
     parser.add_argument("--ip", type=str, default="192.168.195.216", help="WLED IP address")
     parser.add_argument("--port", type=int, default=4048, help="DDP UDP Port")
     parser.add_argument("-b", "--brightness", type=int, default=255, help="Global brightness scale (0-255)")
+    parser.add_argument("--duration", type=float, default=0.0, help="Alert duration in seconds (0 = permanent ambient state)")
     
     parser.add_argument("--no-date", action="store_true", help="Hide date in digital clock mode")
     parser.add_argument("--no-seconds", action="store_true", help="Hide seconds in digital clock mode")
@@ -987,7 +1026,7 @@ def main():
     
     parser.add_argument("--bg-effect", type=str, choices=["solid", "rainbow", "gradient-h", "gradient-v", "pulse"], default="solid", help="Background canvas effect")
     parser.add_argument("--bg-color", type=str, default="black", help="Background primary color")
-    parser.add_argument("--bg-color2", type=str, default="navy", help="Background secondary color for gradient effect")
+    parser.bg_color2 = parser.add_argument("--bg-color2", type=str, default="navy", help="Background secondary color for gradient effect")
     
     parser.add_argument("-s", "--speed", type=float, default=0.05, help="Scroll frame delay in seconds")
     parser.add_argument("--sweep-pause", type=float, default=2.0, help="Seconds to pause on blank screen after sweep")
@@ -1043,6 +1082,7 @@ def main():
                 "brightness": args.brightness,
                 "speed": args.speed,
                 "sweep_pause": args.sweep_pause,
+                "duration": args.duration,
                 "loops": args.loops,
                 "clock_date": not args.no_date,
                 "clock_seconds": not args.no_seconds,
@@ -1063,4 +1103,4 @@ def main():
             daemon_mgr.stop_animation()
 
 if __name__ == "__main__":
-    main()
+    main()