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>
215 lines
7.4 KiB
Python
Executable File
215 lines
7.4 KiB
Python
Executable File
#!/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()
|