diff --git a/octoprint_livegcodecontrol/__init__.py b/octoprint_livegcodecontrol/__init__.py index e1a400e..c9af908 100644 --- a/octoprint_livegcodecontrol/__init__.py +++ b/octoprint_livegcodecontrol/__init__.py @@ -4,10 +4,105 @@ import octoprint.plugin import logging # Import the logging module import re # Import the regular expression module +import threading +import time +import math +import flask + +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, + octoprint.plugin.EventHandlerPlugin): def __init__(self): # Initialize the logger @@ -15,12 +110,68 @@ 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_get(self, request): + return flask.jsonify(presets=self._settings.get(["neoflux_presets"])) + + def on_api_command(self, command, data): + if command == "update_led_config": + if self.led_worker: + self.led_worker.update_config(data.get('payload', {})) + + elif command == "save_preset": + name = data.get("name") + config = data.get("config") + if name and config: + presets = self._settings.get(["neoflux_presets"]) + presets[name] = config + self._settings.set(["neoflux_presets"], presets) + self._settings.save() + self._logger.info(f"Saved preset: {name}") + + elif command == "delete_preset": + name = data.get("name") + if name: + presets = self._settings.get(["neoflux_presets"]) + if name in presets: + del presets[name] + self._settings.set(["neoflux_presets"], presets) + self._settings.save() + self._logger.info(f"Deleted preset: {name}") + + def get_api_commands(self): + return dict( + update_led_config=["payload"], + save_preset=["name", "config"], + delete_preset=["name"] + ) + + def on_event(self, event, payload): + events_mapping = self._settings.get(["neoflux_events"]) + if event in events_mapping: + preset_name = events_mapping[event] + presets = self._settings.get(["neoflux_presets"]) + if preset_name in presets: + config = presets[preset_name] + if self.led_worker: + self._logger.info(f"Applying preset '{preset_name}' for event '{event}'") + self.led_worker.update_config(config) ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict( - rules=[] # Default empty list for rules + rules=[], # Default empty list for rules + neoflux_presets={}, + neoflux_events={} ) def on_settings_initialized(self): @@ -44,8 +195,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..3616e0f 100644 --- a/octoprint_livegcodecontrol/static/js/livegcodecontrol.js +++ b/octoprint_livegcodecontrol/static/js/livegcodecontrol.js @@ -18,6 +18,132 @@ $(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.neofluxPresets = ko.observableArray([]); + self.selectedPreset = ko.observable(); + + self.neofluxEvents = ko.observableArray([ + "Startup", "PrintStarted", "PrintDone", "PrintFailed", "PrintPaused", "PrintResumed" + ]); + self.neofluxEventMappings = ko.observableArray([]); // {event: "PrintStarted", preset: ko.observable("CyberPunk")} + + self.refreshPresets = function() { + OctoPrint.simpleApiGet("livegcodecontrol") + .done(function(response) { + var presets = []; + if (response.presets) { + for (var name in response.presets) { + presets.push({name: name, config: response.presets[name]}); + } + } + self.neofluxPresets(presets); + }); + }; + + self.selectedPreset.subscribe(function(newPresetName) { + if (newPresetName) { + var preset = ko.utils.arrayFirst(self.neofluxPresets(), function(item) { + return item.name === newPresetName; + }); + if (preset) { + self.neofluxMode(preset.config.mode); + self.neofluxSpeed(preset.config.speed); + if (self.neofluxController && preset.config.colors) { + self.neofluxController.colors = preset.config.colors; // Update colors + } + // Update preview immediately + if (self.neofluxController) { + self.neofluxController.updateConfig({ + mode: self.neofluxMode(), + speed: parseInt(self.neofluxSpeed()) + }); + } + } + } + }); + + self.savePreset = function() { + var name = prompt("Enter preset name:"); + if (name) { + if (self.neofluxController) { + // Ensure local controller state is up to date before grabbing config + self.neofluxController.updateConfig({ + mode: self.neofluxMode(), + speed: parseInt(self.neofluxSpeed()) + }); + } + + var config = self.neofluxController ? self.neofluxController.getConfigPayload() : { + mode: self.neofluxMode(), + speed: parseInt(self.neofluxSpeed()), + colors: ["#FF0000", "#0000FF"] // Default fallback + }; + + OctoPrint.simpleApiCommand("livegcodecontrol", "save_preset", { + name: name, + config: config + }).done(function() { + self.refreshPresets(); + }); + } + }; + + self.deletePreset = function() { + var name = self.selectedPreset(); + if (name && confirm("Are you sure you want to delete preset '" + name + "'?")) { + OctoPrint.simpleApiCommand("livegcodecontrol", "delete_preset", { + name: name + }).done(function() { + self.refreshPresets(); + self.selectedPreset(undefined); + }); + } + }; + + 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); + }); + }; + + self.testEventMapping = function(mapping) { + var presetName = mapping.preset(); + if (presetName) { + var preset = ko.utils.arrayFirst(self.neofluxPresets(), function(item) { + return item.name === presetName; + }); + if (preset) { + // Just apply it live + OctoPrint.simpleApiCommand("livegcodecontrol", "update_led_config", { + payload: preset.config + }); + } + } + }; + // ------------------------------ + // --- Helper function to create a new rule object --- function createRule(enabled, pattern, actionType, actionGcode) { return { @@ -85,6 +211,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(); @@ -96,12 +231,26 @@ $(function() { }); self.rules(mappedRules); } + + // NeoFlux Events + self.refreshPresets(); + var savedEvents = self.settingsViewModel.settings.plugins.livegcodecontrol.neoflux_events(); + var mappings = []; + ko.utils.arrayForEach(self.neofluxEvents(), function(evt) { + var preset = savedEvents ? savedEvents[evt] : undefined; + mappings.push({ + event: evt, + preset: ko.observable(preset) + }); + }); + self.neofluxEventMappings(mappings); }; self.onSettingsShown = function() { // Could refresh data from server if necessary, but usually onBeforeBinding is enough for settings // Ensure editing state is clear when settings are reshown self.cancelEdit(); + self.refreshPresets(); // Ensure presets are up to date in settings }; self.onSettingsHidden = function() { @@ -121,12 +270,27 @@ $(function() { }; }); self.settingsViewModel.settings.plugins.livegcodecontrol.rules(rulesToSave); + + // Save NeoFlux Events + var eventsToSave = {}; + ko.utils.arrayForEach(self.neofluxEventMappings(), function(mapping) { + if (mapping.preset()) { + eventsToSave[mapping.event] = mapping.preset(); + } + }); + self.settingsViewModel.settings.plugins.livegcodecontrol.neoflux_events(eventsToSave); }; } 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_settings.jinja2 b/octoprint_livegcodecontrol/templates/livegcodecontrol_settings.jinja2 index b9ebdef..61b2421 100644 --- a/octoprint_livegcodecontrol/templates/livegcodecontrol_settings.jinja2 +++ b/octoprint_livegcodecontrol/templates/livegcodecontrol_settings.jinja2 @@ -66,4 +66,28 @@ + +
| Event | +Preset | +Actions | +
|---|---|---|
| + | + + | ++ + | +