diff --git a/cmdline.py b/cmdline.py index d5aa6b4..c6b0240 100644 --- a/cmdline.py +++ b/cmdline.py @@ -25,7 +25,12 @@ from engine import config from engine.effects.controller import handle_effects_command from engine.terminal import CLR, CURSOR_OFF, CURSOR_ON, G_DIM, G_HI, RST, W_GHOST -TOPIC = config.NTFY_TOPIC +try: + CC_TOPIC = config.NTFY_CC_TOPIC +except AttributeError: + CC_TOPIC = "https://ntfy.sh/klubhaus_terminal_mainline_cc/json" + +TOPIC = CC_TOPIC def send_command(cmd: str) -> str: diff --git a/engine/config.py b/engine/config.py index 284877c..944976c 100644 --- a/engine/config.py +++ b/engine/config.py @@ -105,6 +105,7 @@ class Config: firehose: bool = False ntfy_topic: str = "https://ntfy.sh/klubhaus_terminal_mainline/json" + ntfy_cc_topic: str = "https://ntfy.sh/klubhaus_terminal_mainline_cc/json" ntfy_reconnect_delay: int = 5 message_display_secs: int = 30 @@ -148,6 +149,7 @@ class Config: mode="poetry" if "--poetry" in argv or "-p" in argv else "news", firehose="--firehose" in argv, ntfy_topic="https://ntfy.sh/klubhaus_terminal_mainline/json", + ntfy_cc_topic="https://ntfy.sh/klubhaus_terminal_mainline_cc/json", ntfy_reconnect_delay=5, message_display_secs=30, font_dir=font_dir, diff --git a/engine/controller.py b/engine/controller.py index e6e2e3d..7885a71 100644 --- a/engine/controller.py +++ b/engine/controller.py @@ -3,6 +3,7 @@ Stream controller - manages input sources and orchestrates the render stream. """ from engine.config import Config, get_config +from engine.effects.controller import handle_effects_command from engine.eventbus import EventBus from engine.events import EventType, StreamEvent from engine.mic import MicMonitor @@ -18,6 +19,7 @@ class StreamController: self.event_bus = event_bus self.mic: MicMonitor | None = None self.ntfy: NtfyPoller | None = None + self.ntfy_cc: NtfyPoller | None = None def initialize_sources(self) -> tuple[bool, bool]: """Initialize microphone and ntfy sources. @@ -35,7 +37,38 @@ class StreamController: ) ntfy_ok = self.ntfy.start() - return bool(mic_ok), ntfy_ok + self.ntfy_cc = NtfyPoller( + self.config.ntfy_cc_topic, + reconnect_delay=self.config.ntfy_reconnect_delay, + display_secs=5, + ) + self.ntfy_cc.subscribe(self._handle_cc_message) + ntfy_cc_ok = self.ntfy_cc.start() + + return bool(mic_ok), ntfy_ok and ntfy_cc_ok + + def _handle_cc_message(self, event) -> None: + """Handle incoming C&C message - like a serial port control interface.""" + import urllib.request + + cmd = event.body.strip() if hasattr(event, "body") else str(event).strip() + if not cmd.startswith("/"): + return + + response = handle_effects_command(cmd) + + topic_url = self.config.ntfy_cc_topic.replace("/json", "") + data = response.encode("utf-8") + req = urllib.request.Request( + topic_url, + data=data, + headers={"User-Agent": "mainline/0.1", "Content-Type": "text/plain"}, + method="POST", + ) + try: + urllib.request.urlopen(req, timeout=5) + except Exception: + pass def run(self, items: list) -> None: """Run the stream with initialized sources."""