diff --git a/octoprint_livegcodecontrol/__init__.py b/octoprint_livegcodecontrol/__init__.py index e1a400e..1324544 100644 --- a/octoprint_livegcodecontrol/__init__.py +++ b/octoprint_livegcodecontrol/__init__.py @@ -4,10 +4,103 @@ import octoprint.plugin import logging # Import the logging module import re # Import the regular expression module +import threading +import time +import math + +class LedWorker(threading.Thread): + def __init__(self, printer, logger): + super(LedWorker, self).__init__() + self._printer = printer + self._logger = logger + self.daemon = True + self.running = True + self.paused = False + + # Configuration + self.colors = ["#FF0000", "#0000FF"] + self.mode = "spatial_wave" + self.speed = 150 + + # Internal State + self.led_count = 30 + + def update_config(self, payload): + if "colors" in payload: + self.colors = payload["colors"] + if "mode" in payload: + self.mode = payload["mode"] + if "speed" in payload: + self.speed = payload["speed"] + self._logger.info(f"LedWorker config updated: {payload}") + + def run(self): + self._logger.info("LedWorker started") + while self.running: + if self.paused: + time.sleep(1) + continue + + try: + is_printing = self._printer.is_printing() + + # Adaptive Frequency + if is_printing: + delay = 0.6 # 600ms Safe Mode + else: + delay = 0.05 # 50ms Idle Mode + + self.process_frame(is_printing) + time.sleep(delay) + + except Exception as e: + self._logger.error(f"LedWorker error: {e}") + time.sleep(1) + + def process_frame(self, is_printing): + # Bandwidth Safety / Fallback logic + current_mode = self.mode + if is_printing and self.mode in ["spatial_wave"]: # Add other spatial modes here + current_mode = "solid" # Downgrade to global fade/solid + + commands = [] + + if current_mode == "solid": + # Global Fade (Single M150) + # Assuming first color is primary + color = self.colors[0] if self.colors else "#FFFFFF" + r, g, b = self.hex_to_rgb(color) + commands.append(f"M150 R{r} U{g} B{b}") + + elif current_mode == "spatial_wave": + # Multiple M150 commands + # Example wave effect + t = time.time() + for i in range(self.led_count): + phase = (t / (20000.0 / (self.speed or 1))) + (i / 5.0) + r = int(math.sin(phase) * 127 + 128) + b = int(math.cos(phase) * 127 + 128) + commands.append(f"M150 I{i} R{r} U0 B{b}") + + # Inject G-code + if commands: + # In a real scenario, you might batch these or send individually + # OctoPrint's send_cmd doesn't support lists for single command, but self._printer.commands does + self._printer.commands(commands, tags=set(["suppress_log"])) + + def hex_to_rgb(self, hex_val): + hex_val = hex_val.lstrip('#') + lv = len(hex_val) + return tuple(int(hex_val[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) + + def stop(self): + self.running = False + class LiveGCodeControlPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, - octoprint.plugin.TemplatePlugin): + octoprint.plugin.TemplatePlugin, + octoprint.plugin.SimpleApiPlugin): def __init__(self): # Initialize the logger @@ -15,6 +108,24 @@ def __init__(self): self._logger.info("LiveGCodeControlPlugin: Initializing...") self.active_rules = [] # Initialize active_rules self.last_matched_rule_pattern = None # Initialize last matched rule pattern + self.led_worker = None + + def on_after_startup(self): + self._logger.info("LiveGCodeControlPlugin: Starting LedWorker...") + self.led_worker = LedWorker(self._printer, self._logger) + self.led_worker.start() + + ##~~ SimpleApiPlugin mixin + + def on_api_command(self, command, data): + if command == "update_led_config": + if self.led_worker: + self.led_worker.update_config(data.get('payload', {})) + + def get_api_commands(self): + return dict( + update_led_config=["payload"] + ) ##~~ SettingsPlugin mixin @@ -44,8 +155,8 @@ def get_assets(self): # Define your plugin's asset files to automatically include in the # core UI here. return dict( - js=["js/livegcodecontrol.js"], - css=["css/livegcodecontrol.css"], + js=["js/livegcodecontrol.js", "js/neoflux_ui.js"], + css=["css/livegcodecontrol.css", "css/neoflux.css"], less=["less/livegcodecontrol.less"] ) diff --git a/octoprint_livegcodecontrol/static/css/neoflux.css b/octoprint_livegcodecontrol/static/css/neoflux.css new file mode 100644 index 0000000..7696a07 --- /dev/null +++ b/octoprint_livegcodecontrol/static/css/neoflux.css @@ -0,0 +1,78 @@ +/* Neoflux Cyberpunk Styling */ +#neoflux-container { + background-color: #0d0d0d; + color: #00ffea; + font-family: 'Courier New', Courier, monospace; + padding: 20px; + border: 2px solid #00ffea; + box-shadow: 0 0 15px #00ffea; +} + +#neoflux-container h2 { + text-shadow: 0 0 10px #00ffea; + border-bottom: 1px solid #00ffea; + padding-bottom: 10px; +} + +.neoflux-control-panel { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-top: 20px; +} + +.neoflux-preview { + flex: 1; + min-width: 300px; + border: 1px dashed #ff00ff; + padding: 10px; + background-color: #1a1a1a; +} + +.neoflux-controls { + flex: 1; + min-width: 300px; +} + +canvas#neoflux-canvas { + width: 100%; + height: auto; + background-color: #000; + box-shadow: 0 0 10px #ff00ff; +} + +.neoflux-btn { + background-color: #000; + color: #ff00ff; + border: 1px solid #ff00ff; + padding: 10px 20px; + cursor: pointer; + font-size: 1.2em; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 2px; +} + +.neoflux-btn:hover { + background-color: #ff00ff; + color: #000; + box-shadow: 0 0 15px #ff00ff; +} + +.neoflux-input-group { + margin-bottom: 15px; +} + +.neoflux-input-group label { + display: block; + margin-bottom: 5px; + color: #00ffea; +} + +.neoflux-input { + background-color: #222; + border: 1px solid #00ffea; + color: #fff; + padding: 8px; + width: 100%; +} diff --git a/octoprint_livegcodecontrol/static/js/livegcodecontrol.js b/octoprint_livegcodecontrol/static/js/livegcodecontrol.js index 018b3b9..5ab1673 100644 --- a/octoprint_livegcodecontrol/static/js/livegcodecontrol.js +++ b/octoprint_livegcodecontrol/static/js/livegcodecontrol.js @@ -18,6 +18,36 @@ $(function() { return self.editingRule() ? "Update Rule" : "Add Rule"; }); + // --- NEOFLUX Implementation --- + self.neofluxController = null; + self.neofluxMode = ko.observable("spatial_wave"); + self.neofluxSpeed = ko.observable(150); + + self.applyNeoFluxConfig = function() { + if (!self.neofluxController) return; + + // Update local controller state + self.neofluxController.updateConfig({ + mode: self.neofluxMode(), + speed: parseInt(self.neofluxSpeed()) + }); + + // Send configuration to backend + var payload = { + command: "update_led_config", + payload: self.neofluxController.getConfigPayload() + }; + + OctoPrint.simpleApiCommand("livegcodecontrol", "update_led_config", payload) + .done(function(response) { + console.log("NEOFLUX config updated:", response); + }) + .fail(function(response) { + console.error("Failed to update NEOFLUX config:", response); + }); + }; + // ------------------------------ + // --- Helper function to create a new rule object --- function createRule(enabled, pattern, actionType, actionGcode) { return { @@ -85,6 +115,15 @@ $(function() { }; // --- OctoPrint Settings Plugin Hooks --- + self.onAfterBinding = function() { + // Initialize NeoFlux Controller (Moved from onStartup for DOM safety) + if (window.NeoFluxController && document.getElementById("neoflux-canvas")) { + this.neofluxController = new window.NeoFluxController("neoflux-canvas"); + } else { + console.warn("NeoFluxController or Canvas element missing."); + } + }; + self.onBeforeBinding = function() { // Load existing rules from settings var savedRulesData = self.settingsViewModel.settings.plugins.livegcodecontrol.rules(); @@ -127,6 +166,12 @@ $(function() { OCTOPRINT_VIEWMODELS.push({ construct: LiveGCodeControlViewModel, dependencies: ["settingsViewModel"], - elements: ["#settings_plugin_livegcodecontrol"] + elements: ["#settings_plugin_livegcodecontrol", "#neoflux-container"], + onStartup: function() { + // Initialize NeoFlux Controller + if (window.NeoFluxController) { + this.neofluxController = new window.NeoFluxController("neoflux-canvas"); + } + } }); }); diff --git a/octoprint_livegcodecontrol/static/js/neoflux_ui.js b/octoprint_livegcodecontrol/static/js/neoflux_ui.js new file mode 100644 index 0000000..78feff0 --- /dev/null +++ b/octoprint_livegcodecontrol/static/js/neoflux_ui.js @@ -0,0 +1,85 @@ +// neoflux_ui.js + +(function(global) { + class NeoFluxController { + constructor(canvasId) { + this.canvas = document.getElementById(canvasId); + this.ctx = this.canvas ? this.canvas.getContext('2d') : null; + this.width = this.canvas ? this.canvas.width : 300; + this.height = this.canvas ? this.canvas.height : 50; + this.ledCount = 30; // Default + this.colors = ["#FF0000", "#0000FF"]; + this.mode = "spatial_wave"; + this.speed = 150; + this.animationId = null; + this.lastFrameTime = 0; + + if (this.canvas) { + this.startPreview(); + } + } + + updateConfig(config) { + if (config.colors) this.colors = config.colors; + if (config.mode) this.mode = config.mode; + if (config.speed) this.speed = config.speed; + } + + startPreview() { + if (!this.ctx) return; + const animate = (time) => { + const delta = time - this.lastFrameTime; + if (delta > (1000 / 60)) { // Cap at ~60fps + this.render(time); + this.lastFrameTime = time; + } + this.animationId = requestAnimationFrame(animate); + }; + this.animationId = requestAnimationFrame(animate); + } + + stopPreview() { + if (this.animationId) { + cancelAnimationFrame(this.animationId); + } + } + + render(time) { + // Clear canvas + this.ctx.fillStyle = '#000'; + this.ctx.fillRect(0, 0, this.width, this.height); + + const ledWidth = this.width / this.ledCount; + + for (let i = 0; i < this.ledCount; i++) { + let color = this.calculateLedColor(i, time); + this.ctx.fillStyle = color; + this.ctx.fillRect(i * ledWidth, 5, ledWidth - 2, this.height - 10); + } + } + + calculateLedColor(index, time) { + // Simple visualization simulation based on mode + if (this.mode === 'spatial_wave') { + const phase = (time / (20000 / this.speed)) + (index / 5); + const r = Math.sin(phase) * 127 + 128; + const b = Math.cos(phase) * 127 + 128; + return `rgb(${Math.floor(r)}, 0, ${Math.floor(b)})`; + } else if (this.mode === 'solid') { + return this.colors[0] || '#ffffff'; + } + return '#333'; + } + + getConfigPayload() { + return { + colors: this.colors, + mode: this.mode, + speed: this.speed + }; + } + } + + global.NeoFluxController = NeoFluxController; + +})(window); diff --git a/octoprint_livegcodecontrol/templates/livegcodecontrol_tab.jinja2 b/octoprint_livegcodecontrol/templates/livegcodecontrol_tab.jinja2 new file mode 100644 index 0000000..098d265 --- /dev/null +++ b/octoprint_livegcodecontrol/templates/livegcodecontrol_tab.jinja2 @@ -0,0 +1,25 @@ +
+

NEOFLUX LED CONTROLLER

+
+
+

Preview

+ +
+
+

Configuration

+
+ + +
+
+ + +
+ + +
+
+