Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 114 additions & 3 deletions octoprint_livegcodecontrol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,128 @@
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
self._logger = logging.getLogger("octoprint.plugins.livegcodecontrol")
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

Expand Down Expand Up @@ -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"]
)

Expand Down
78 changes: 78 additions & 0 deletions octoprint_livegcodecontrol/static/css/neoflux.css
Original file line number Diff line number Diff line change
@@ -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%;
}
47 changes: 46 additions & 1 deletion octoprint_livegcodecontrol/static/js/livegcodecontrol.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");
}
}
});
});
85 changes: 85 additions & 0 deletions octoprint_livegcodecontrol/static/js/neoflux_ui.js
Original file line number Diff line number Diff line change
@@ -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);
25 changes: 25 additions & 0 deletions octoprint_livegcodecontrol/templates/livegcodecontrol_tab.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div id="neoflux-container">
<h2>NEOFLUX LED CONTROLLER</h2>
<div class="neoflux-control-panel">
<div class="neoflux-preview">
<h3>Preview</h3>
<canvas id="neoflux-canvas" width="600" height="100"></canvas>
</div>
<div class="neoflux-controls">
<h3>Configuration</h3>
<div class="neoflux-input-group">
<label>Mode</label>
<select id="neoflux-mode" class="neoflux-input" data-bind="value: neofluxMode">
<option value="spatial_wave">Spatial Wave</option>
<option value="solid">Solid Color</option>
</select>
</div>
<div class="neoflux-input-group">
<label>Speed</label>
<input type="number" class="neoflux-input" data-bind="value: neofluxSpeed">
</div>
<!-- More controls can be added here -->
<button class="neoflux-btn" data-bind="click: applyNeoFluxConfig">Apply Configuration</button>
</div>
</div>
</div>