From a17fcd72d36c9e74da8e6f67e774fe8758e4adbc Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 29 Nov 2025 06:44:06 +0000
Subject: [PATCH 1/2] Implement NEOFLUX LED controller feature
- Added `neoflux.css` and `neoflux_ui.js` for the new Cyberpunk UI and Canvas preview.
- Created `livegcodecontrol_tab.jinja2` to host the new NEOFLUX tab.
- Refactored `__init__.py` to include `LedWorker` thread for safe G-code injection.
- Implemented `SimpleApiPlugin` to handle `update_led_config` commands.
- Updated `livegcodecontrol.js` to bridge the frontend and backend.
- Ensured adaptive frequency and bandwidth safety for print jobs.
---
octoprint_livegcodecontrol/__init__.py | 117 +++++++++++++++++-
.../static/css/neoflux.css | 78 ++++++++++++
.../static/js/livegcodecontrol.js | 38 +++++-
.../static/js/neoflux_ui.js | 85 +++++++++++++
.../templates/livegcodecontrol_tab.jinja2 | 25 ++++
5 files changed, 339 insertions(+), 4 deletions(-)
create mode 100644 octoprint_livegcodecontrol/static/css/neoflux.css
create mode 100644 octoprint_livegcodecontrol/static/js/neoflux_ui.js
create mode 100644 octoprint_livegcodecontrol/templates/livegcodecontrol_tab.jinja2
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..a21a0c7 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 {
@@ -127,6 +157,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
+
+
+
+
+
+
+
+
+
+
+
+
+
From af04b67e05994a22d2406a8c232e4abbdfa9907f Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 29 Nov 2025 07:04:06 +0000
Subject: [PATCH 2/2] Refactor NEOFLUX implementation for thread safety and
race condition fix
- Fixed DOM race condition by moving frontend initialization to `onAfterBinding`.
- Added explicit DOM availability checks for `NeoFluxController`.
- Implemented `threading.Lock` in `LedWorker` for thread-safe configuration updates.
- Added G-code command deduplication to reduce serial traffic.
- Removed redundant command key from API payload.
---
octoprint_livegcodecontrol/__init__.py | 132 +++++++++++++++++-
.../static/css/neoflux.css | 78 +++++++++++
.../static/js/livegcodecontrol.js | 40 +++++-
.../static/js/neoflux_ui.js | 85 +++++++++++
.../templates/livegcodecontrol_tab.jinja2 | 25 ++++
5 files changed, 356 insertions(+), 4 deletions(-)
create mode 100644 octoprint_livegcodecontrol/static/css/neoflux.css
create mode 100644 octoprint_livegcodecontrol/static/js/neoflux_ui.js
create mode 100644 octoprint_livegcodecontrol/templates/livegcodecontrol_tab.jinja2
diff --git a/octoprint_livegcodecontrol/__init__.py b/octoprint_livegcodecontrol/__init__.py
index e1a400e..0f5972c 100644
--- a/octoprint_livegcodecontrol/__init__.py
+++ b/octoprint_livegcodecontrol/__init__.py
@@ -4,10 +4,118 @@
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
+
+ self._lock = threading.Lock()
+
+ # Configuration
+ self.colors = ["#FF0000", "#0000FF"]
+ self.mode = "spatial_wave"
+ self.speed = 150
+
+ # Internal State
+ self.led_count = 30
+ self.last_sent_commands = None
+
+ def update_config(self, payload):
+ with self._lock:
+ 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):
+ # Snapshot state with lock
+ with self._lock:
+ mode = self.mode
+ colors = list(self.colors)
+ speed = self.speed
+ led_count = self.led_count
+
+ # Bandwidth Safety / Fallback logic
+ current_mode = mode
+ if is_printing and 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 = colors[0] if 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(led_count):
+ phase = (t / (20000.0 / (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 with deduplication
+ if commands:
+ # Check if commands are identical to last sent
+ if commands != self.last_sent_commands:
+ self._printer.commands(commands, tags=set(["suppress_log"]))
+ self.last_sent_commands = commands
+ else:
+ # Optional: Log debug or do nothing
+ pass
+
+ 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 +123,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 +170,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..fe86095 100644
--- a/octoprint_livegcodecontrol/static/js/livegcodecontrol.js
+++ b/octoprint_livegcodecontrol/static/js/livegcodecontrol.js
@@ -18,6 +18,35 @@ $(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 = {
+ 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 +114,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 +165,6 @@ $(function() {
OCTOPRINT_VIEWMODELS.push({
construct: LiveGCodeControlViewModel,
dependencies: ["settingsViewModel"],
- elements: ["#settings_plugin_livegcodecontrol"]
+ elements: ["#settings_plugin_livegcodecontrol", "#neoflux-container"]
});
});
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
+
+
+
+
+
+
+
+
+
+
+
+
+