Add Print Queue UI and slicer post-processing script
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
- Create Print Queue page showing pending/completed/cancelled jobs - Add needs_weighing filter to spool API for flagged spools - Add IPrintJob interface and needs_weighing to ISpool model - Add slicer_post_process.py for Elegoo/Orca Slicer integration - Add translations for print queue feature - Bump version to 0.23C.3 The workflow: 1. Slicer post-processing script creates pending print job 2. After print, user confirms (deducts filament) or cancels (flags for weighing) 3. Spools needing weigh-in are shown in Print Queue UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
214
scripts/slicer_post_process.py
Executable file
214
scripts/slicer_post_process.py
Executable file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Elegoo Slicer / Orca Slicer Post-Processing Script for Spoolman Integration.
|
||||
|
||||
This script parses G-code files to extract filament usage information and creates
|
||||
a pending print job in Spoolman. After the print completes, use the Spoolman
|
||||
Print Queue UI to confirm the print (deducts filament) or cancel it (flags for weighing).
|
||||
|
||||
Usage:
|
||||
1. In Elegoo Slicer, go to Printer Settings > Post-processing Scripts
|
||||
2. Add the path to this script: /path/to/slicer_post_process.py
|
||||
3. Configure the environment variables below or create a config file
|
||||
|
||||
Configuration:
|
||||
Set these environment variables:
|
||||
- SPOOLMAN_URL: Your Spoolman instance URL (e.g., http://192.168.0.11:8000)
|
||||
- SPOOLMAN_SPOOL_ID: Default spool ID to use (or set per-filament in slicer)
|
||||
|
||||
Or create a file at ~/.config/spoolman/slicer.conf:
|
||||
[spoolman]
|
||||
url = http://192.168.0.11:8000
|
||||
spool_id = 1
|
||||
|
||||
The script will:
|
||||
1. Read the G-code file passed as argument
|
||||
2. Parse Elegoo/Orca slicer comments for filament usage
|
||||
3. Create a pending print job in Spoolman via API
|
||||
4. The job appears in the Print Queue for confirmation after printing
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_config() -> tuple[str, int]:
|
||||
"""Get Spoolman URL and default spool ID from environment or config file."""
|
||||
url = os.environ.get("SPOOLMAN_URL")
|
||||
spool_id = os.environ.get("SPOOLMAN_SPOOL_ID")
|
||||
|
||||
if not url:
|
||||
config_path = Path.home() / ".config" / "spoolman" / "slicer.conf"
|
||||
if config_path.exists():
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
url = config.get("spoolman", "url", fallback=None)
|
||||
spool_id = config.get("spoolman", "spool_id", fallback=None)
|
||||
|
||||
if not url:
|
||||
print("ERROR: SPOOLMAN_URL not set. Set environment variable or create config file.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not spool_id:
|
||||
print("ERROR: SPOOLMAN_SPOOL_ID not set. Set environment variable or create config file.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return url.rstrip("/"), int(spool_id)
|
||||
|
||||
|
||||
def parse_gcode(filepath: str) -> dict:
|
||||
"""
|
||||
Parse G-code file for filament usage information.
|
||||
|
||||
Looks for Elegoo/Orca Slicer comments like:
|
||||
; total filament used [g] = 12.34
|
||||
; filament used [mm] = 4567.89
|
||||
; filament used [g] = 12.34
|
||||
|
||||
Also checks for PrusaSlicer format:
|
||||
; filament used [mm] = 4567.89
|
||||
; filament used [g] = 12.34
|
||||
"""
|
||||
result = {
|
||||
"filename": Path(filepath).name,
|
||||
"filament_used_g": None,
|
||||
"filament_used_mm": None,
|
||||
}
|
||||
|
||||
# Patterns for different slicers
|
||||
patterns = {
|
||||
# Elegoo/Orca format
|
||||
"total_filament_g": re.compile(r";\s*total filament used \[g\]\s*=\s*([\d.]+)", re.IGNORECASE),
|
||||
"filament_g": re.compile(r";\s*filament used \[g\]\s*=\s*([\d.]+)", re.IGNORECASE),
|
||||
"filament_mm": re.compile(r";\s*filament used \[mm\]\s*=\s*([\d.]+)", re.IGNORECASE),
|
||||
# Alternative formats
|
||||
"filament_weight": re.compile(r";\s*(?:total )?filament (?:weight|cost)\s*=\s*([\d.]+)\s*g", re.IGNORECASE),
|
||||
"filament_length": re.compile(r";\s*(?:total )?filament\s*=\s*([\d.]+)\s*mm", re.IGNORECASE),
|
||||
}
|
||||
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
||||
# Only read first 500 lines and last 500 lines for efficiency
|
||||
lines = f.readlines()
|
||||
header = lines[:500]
|
||||
footer = lines[-500:] if len(lines) > 500 else []
|
||||
check_lines = header + footer
|
||||
|
||||
for line in check_lines:
|
||||
if not line.startswith(";"):
|
||||
continue
|
||||
|
||||
# Check each pattern
|
||||
for key, pattern in patterns.items():
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
value = float(match.group(1))
|
||||
if "g" in key or "weight" in key:
|
||||
if result["filament_used_g"] is None:
|
||||
result["filament_used_g"] = value
|
||||
elif "mm" in key or "length" in key:
|
||||
if result["filament_used_mm"] is None:
|
||||
result["filament_used_mm"] = value
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Error parsing G-code: {e}", file=sys.stderr)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def create_print_job(url: str, spool_id: int, gcode_info: dict) -> bool:
|
||||
"""Create a pending print job in Spoolman."""
|
||||
if gcode_info["filament_used_g"] is None:
|
||||
print("Warning: Could not find filament weight in G-code. Skipping job creation.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
api_url = f"{url}/api/v1/print-job"
|
||||
|
||||
payload = {
|
||||
"spool_id": spool_id,
|
||||
"filename": gcode_info["filename"],
|
||||
"filament_used_g": gcode_info["filament_used_g"],
|
||||
}
|
||||
|
||||
if gcode_info["filament_used_mm"] is not None:
|
||||
payload["filament_used_mm"] = gcode_info["filament_used_mm"]
|
||||
|
||||
try:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
api_url,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
if response.status in (200, 201):
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
print(f"Created print job #{result['id']}: {gcode_info['filename']} ({gcode_info['filament_used_g']:.1f}g)")
|
||||
return True
|
||||
else:
|
||||
print(f"Error: API returned status {response.status}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode("utf-8") if e.fp else ""
|
||||
print(f"Error: API request failed: {e.code} {e.reason} - {error_body}", file=sys.stderr)
|
||||
return False
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Error: Could not connect to Spoolman: {e.reason}", file=sys.stderr)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: slicer_post_process.py <gcode_file>", file=sys.stderr)
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
gcode_path = sys.argv[1]
|
||||
|
||||
if not os.path.exists(gcode_path):
|
||||
print(f"Error: G-code file not found: {gcode_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get configuration
|
||||
url, spool_id = get_config()
|
||||
|
||||
# Parse G-code
|
||||
print(f"Parsing G-code: {gcode_path}")
|
||||
gcode_info = parse_gcode(gcode_path)
|
||||
|
||||
if gcode_info["filament_used_g"]:
|
||||
print(f" Filament: {gcode_info['filament_used_g']:.2f}g", end="")
|
||||
if gcode_info["filament_used_mm"]:
|
||||
print(f" ({gcode_info['filament_used_mm']:.1f}mm)")
|
||||
else:
|
||||
print()
|
||||
else:
|
||||
print(" Warning: Could not parse filament usage from G-code")
|
||||
|
||||
# Create print job
|
||||
success = create_print_job(url, spool_id, gcode_info)
|
||||
|
||||
if success:
|
||||
print(f"Print job created in Spoolman ({url})")
|
||||
print("After printing, use the Print Queue to confirm or cancel the job.")
|
||||
else:
|
||||
print("Failed to create print job. The print will proceed without tracking.", file=sys.stderr)
|
||||
|
||||
# Always exit 0 so the slicer continues with the print
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user