diff --git a/config.py b/config.py
index ca30dad..15317e7 100644
--- a/config.py
+++ b/config.py
@@ -45,3 +45,5 @@ def _env_to_int(name: str, default: int) -> int:
DISABLE_MISMATCH_WARNING = _env_to_bool("DISABLE_MISMATCH_WARNING", False)
CLEAR_ASSIGNMENT_WHEN_EMPTY = _env_to_bool("CLEAR_ASSIGNMENT_WHEN_EMPTY", False)
COLOR_DISTANCE_TOLERANCE = _env_to_int("COLOR_DISTANCE_TOLERANCE", 40)
+
+DOWNLOADED_FILES = {} # Store locations of currently downloaded temp files
diff --git a/filament_usage_tracker.py b/filament_usage_tracker.py
index 821a3d9..cd9c31f 100644
--- a/filament_usage_tracker.py
+++ b/filament_usage_tracker.py
@@ -8,10 +8,10 @@
from pathlib import Path
from urllib.parse import urlparse
-from config import EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID, TRACK_LAYER_USAGE
+from config import EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID, TRACK_LAYER_USAGE, PRINTER_IP, PRINTER_NAME
from spoolman_client import consumeSpool
from spoolman_service import fetchSpools, getAMSFromTray, trayUid
-from tools_3mf import download3mfFromCloud, download3mfFromFTP, download3mfFromLocalFilesystem
+from tools_3mf import download3mfFromCloud, download3mfFromFTP, download3mfFromLocalFilesystem, clearTempFile, retrieveModel
from print_history import update_filament_spool, update_filament_grams_used, get_all_filament_usage_for_print, update_layer_tracking
from logger import log
@@ -299,7 +299,10 @@ def _handle_print_start(self, print_obj: dict) -> None:
log("[filament-tracker] Print start")
model_url = print_obj.get("url")
- model_path = self._retrieve_model(model_url)
+ if not model_url:
+ log("[filament-tracker] No model URL provided")
+ return None
+ model_path = retrieveModel(model_url)
if model_path is None:
log("Failed to retrieve model. Print will not be tracked.")
@@ -419,28 +422,6 @@ def apply_ams_mapping(self, ams_mapping: list[int] | None) -> None:
self._maybe_update_predicted_total()
self._update_layer_tracking_progress()
- def _retrieve_model(self, model_url: str | None) -> str | None:
- if not model_url:
- log("[filament-tracker] No model URL provided")
- return None
-
- uri = urlparse(model_url)
- try:
- with tempfile.NamedTemporaryFile(suffix=".3mf", delete=False) as model_file:
- if uri.scheme in ("https", "http"):
- log(f"[filament-tracker] Downloading model via HTTP(S): {model_url}")
- download3mfFromCloud(model_url, model_file)
- elif uri.scheme == "local":
- log(f"[filament-tracker] Loading model from local path: {uri.path}")
- download3mfFromLocalFilesystem(uri.path, model_file)
- else:
- log(f"[filament-tracker] Downloading model via FTP: {model_url}")
- download3mfFromFTP(model_url.rpartition('/')[-1], model_file) # Pull just filename to clear out any unexpected paths
- return model_file.name
- except Exception as exc:
- log(f"Failed to fetch model: {exc}")
- return None
-
def _handle_layer_change(self, layer: int) -> None:
if self.active_model is None:
return
@@ -487,6 +468,9 @@ def _handle_print_end(self) -> None:
self._reset_layer_tracking_state()
clear_checkpoint()
+ log("[DEBUG] Clearing temp print file")
+ clearTempFile(PRINTER_NAME, PRINTER_IP)
+
def _handle_print_abort(self, status: str = LAYER_TRACKING_STATUS_ABORTED) -> None:
if self.active_model is None:
return
@@ -512,6 +496,9 @@ def _handle_print_abort(self, status: str = LAYER_TRACKING_STATUS_ABORTED) -> No
self._reset_layer_tracking_state()
clear_checkpoint()
+ log("[DEBUG] Clearing temp print file")
+ clearTempFile(PRINTER_NAME, PRINTER_IP)
+
def _mm_to_grams(self, length_mm: float, diameter_mm: float, density_g_per_cm3: float) -> float:
"""
Convert filament length in mm to grams.
diff --git a/mqtt_bambulab.py b/mqtt_bambulab.py
index 2d70488..9fe746d 100644
--- a/mqtt_bambulab.py
+++ b/mqtt_bambulab.py
@@ -16,10 +16,13 @@
EXTERNAL_SPOOL_ID,
TRACK_LAYER_USAGE,
CLEAR_ASSIGNMENT_WHEN_EMPTY,
+ DOWNLOADED_FILES,
+ PRINTER_NAME,
+
)
from messages import GET_VERSION, PUSH_ALL, AMS_FILAMENT_SETTING
from spoolman_service import spendFilaments, setActiveTray, fetchSpools, clear_active_spool_for_tray
-from tools_3mf import getMetaDataFrom3mf
+from tools_3mf import getMetaDataFrom3mf, clearTempFile
import time
import copy
from collections.abc import Mapping
@@ -158,44 +161,44 @@ def update_dict(original: dict, updates: dict) -> dict:
return original
-def _parse_grams(value):
- try:
- return float(value)
- except (TypeError, ValueError):
- return None
-
-def _mask_serial(serial: str | None, keep_chars: int = 3) -> str:
- if not serial:
- return ""
- visible = serial[:keep_chars]
- if len(serial) <= keep_chars:
- return visible
- return f"{visible}..."
-
-def _mask_sn_values(value):
- if isinstance(value, dict):
- for key, item in value.items():
- if key.lower() == "sn" and isinstance(item, str):
- value[key] = _mask_serial(item)
- else:
- _mask_sn_values(item)
- elif isinstance(value, list):
- for elem in value:
- _mask_sn_values(elem)
-
-def _mask_mqtt_payload(payload: str) -> str:
- try:
- data = json.loads(payload)
- _mask_sn_values(data)
- masked = json.dumps(data, separators=(",", ":"))
- except ValueError:
- masked = payload
-
- masked_serial = _mask_serial(PRINTER_ID)
- if masked_serial:
- masked = masked.replace(PRINTER_ID, masked_serial)
-
- return masked
+def _parse_grams(value):
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ return None
+
+def _mask_serial(serial: str | None, keep_chars: int = 3) -> str:
+ if not serial:
+ return ""
+ visible = serial[:keep_chars]
+ if len(serial) <= keep_chars:
+ return visible
+ return f"{visible}..."
+
+def _mask_sn_values(value):
+ if isinstance(value, dict):
+ for key, item in value.items():
+ if key.lower() == "sn" and isinstance(item, str):
+ value[key] = _mask_serial(item)
+ else:
+ _mask_sn_values(item)
+ elif isinstance(value, list):
+ for elem in value:
+ _mask_sn_values(elem)
+
+def _mask_mqtt_payload(payload: str) -> str:
+ try:
+ data = json.loads(payload)
+ _mask_sn_values(data)
+ masked = json.dumps(data, separators=(",", ":"))
+ except ValueError:
+ masked = payload
+
+ masked_serial = _mask_serial(PRINTER_ID)
+ if masked_serial:
+ masked = masked.replace(PRINTER_ID, masked_serial)
+
+ return masked
def map_filament(tray_tar):
global PENDING_PRINT_METADATA
@@ -292,7 +295,14 @@ def processMessage(data):
length_used=length_used,
estimated_length=estimated_length_mm,
)
-
+
+ # # Clear out temp files if they exist on FAILED or FINISHED prints
+ # if(DOWNLOADED_FILES.get(f"{PRINTER_NAME}_{PRINTER_IP}") and PRINTER_STATE_LAST["print"].get("gecode_state") == "RUNNING" and (PRINTER_STATE["print"].get("gcode_state") == "FAILED" or PRINTER_STATE["print"].get("gcode_state") == "FINISHED")):
+ # log("would of cleared here")
+ # #clearTempFile(PRINTER_NAME, PRINTER_IP)
+
+
+
#if ("gcode_state" in data["print"] and data["print"]["gcode_state"] == "RUNNING") and ("print_type" in data["print"] and data["print"]["print_type"] != "local") \
# and ("tray_tar" in data["print"] and data["print"]["tray_tar"] != "255") and ("stg_cur" in data["print"] and data["print"]["stg_cur"] == 0 and PRINT_CURRENT_STAGE != 0):
@@ -306,7 +316,11 @@ def processMessage(data):
):
if not PENDING_PRINT_METADATA:
- PENDING_PRINT_METADATA = getMetaDataFrom3mf(PRINTER_STATE["print"]["gcode_file"])
+ # TODO Seems to be trying to find an internal gcode file inside the 3mf file?
+ # And only firing sometimes, do we need this?
+ #PENDING_PRINT_METADATA = getMetaDataFrom3mf(PRINTER_STATE["print"]["gcode_file"])
+ log(f"[DEBUG] Would have tried to grab file {PRINTER_STATE["print"]["gcode_file"]}")
+
if PENDING_PRINT_METADATA:
PENDING_PRINT_METADATA["print_type"] = PRINTER_STATE["print"].get("print_type")
PENDING_PRINT_METADATA["task_id"] = PRINTER_STATE["print"].get("task_id")
@@ -449,8 +463,8 @@ def on_message(client, userdata, msg):
"models_by_id": models_by_id,
}
- if "print" in data:
- append_to_rotating_file("/home/app/logs/mqtt.log", _mask_mqtt_payload(msg.payload.decode()))
+ if "print" in data:
+ append_to_rotating_file("/home/app/logs/mqtt.log", _mask_mqtt_payload(msg.payload.decode()))
#print(data)
diff --git a/tools_3mf.py b/tools_3mf.py
index bcbc8c3..724ae24 100644
--- a/tools_3mf.py
+++ b/tools_3mf.py
@@ -9,7 +9,7 @@
import time
import io
from datetime import datetime
-from config import PRINTER_CODE, PRINTER_IP
+from config import PRINTER_CODE, PRINTER_IP, PRINTER_NAME, DOWNLOADED_FILES
from urllib.parse import urlparse
from logger import log
@@ -167,119 +167,111 @@ def getMetaDataFrom3mf(url):
try:
metadata = {}
- # Create a temporary file
- with tempfile.NamedTemporaryFile(delete_on_close=False,delete=True, suffix=".3mf") as temp_file:
- temp_file_name = temp_file.name
+ # Retrieve the model
+ temp_file_name = retrieveModel(url)
- if url.startswith("http"):
- download3mfFromCloud(url, temp_file)
- elif url.startswith("local:"):
- download3mfFromLocalFilesystem(url.replace("local:", ""), temp_file)
+
+ metadata["model_path"] = url
+
+ parsed_url = urlparse(url)
+ metadata["file"] = os.path.basename(parsed_url.path)
+
+
+
+ # Unzip the 3MF file
+ with zipfile.ZipFile(temp_file_name, 'r') as z:
+ # Check for the Metadata/slice_info.config file
+ slice_info_path = "Metadata/slice_info.config"
+ if slice_info_path in z.namelist():
+ with z.open(slice_info_path) as slice_info_file:
+ # Parse the XML content of the file
+ tree = ET.parse(slice_info_file)
+ root = tree.getroot()
+
+ # Extract id and used_g from each filament
+ """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+
+ for meta in root.findall(".//plate/metadata"):
+ if meta.attrib.get("key") == "index":
+ metadata["plateID"] = meta.attrib.get("value", "")
+
+ usage = {}
+ filaments= {}
+ filamentId = 1
+ for plate in root.findall(".//plate"):
+ for filament in plate.findall(".//filament"):
+ used_g = filament.attrib.get("used_g")
+ #filamentId = int(filament.attrib.get("id"))
+
+ usage[filamentId] = used_g
+ filaments[filamentId] = {"id": filamentId,
+ "tray_info_idx": filament.attrib.get("tray_info_idx"),
+ "type":filament.attrib.get("type"),
+ "color": filament.attrib.get("color"),
+ "used_g": used_g,
+ "used_m":filament.attrib.get("used_m")}
+ filamentId += 1
+
+ metadata["filaments"] = filaments
+ metadata["usage"] = usage
else:
- download3mfFromFTP(url.rpartition('/')[-1], temp_file) # Pull just filename to clear out any unexpected paths
-
- temp_file.close()
- metadata["model_path"] = url
-
- parsed_url = urlparse(url)
- metadata["file"] = os.path.basename(parsed_url.path)
-
- log(f"3MF file downloaded and saved as {temp_file_name}.")
-
- # Unzip the 3MF file
- with zipfile.ZipFile(temp_file_name, 'r') as z:
- # Check for the Metadata/slice_info.config file
- slice_info_path = "Metadata/slice_info.config"
- if slice_info_path in z.namelist():
- with z.open(slice_info_path) as slice_info_file:
- # Parse the XML content of the file
- tree = ET.parse(slice_info_file)
- root = tree.getroot()
-
- # Extract id and used_g from each filament
- """
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- """
-
- for meta in root.findall(".//plate/metadata"):
- if meta.attrib.get("key") == "index":
- metadata["plateID"] = meta.attrib.get("value", "")
-
- usage = {}
- filaments= {}
- filamentId = 1
- for plate in root.findall(".//plate"):
- for filament in plate.findall(".//filament"):
- used_g = filament.attrib.get("used_g")
- #filamentId = int(filament.attrib.get("id"))
-
- usage[filamentId] = used_g
- filaments[filamentId] = {"id": filamentId,
- "tray_info_idx": filament.attrib.get("tray_info_idx"),
- "type":filament.attrib.get("type"),
- "color": filament.attrib.get("color"),
- "used_g": used_g,
- "used_m":filament.attrib.get("used_m")}
- filamentId += 1
-
- metadata["filaments"] = filaments
- metadata["usage"] = usage
- else:
- log(f"File '{slice_info_path}' not found in the archive.")
- return {}
+ log(f"File '{slice_info_path}' not found in the archive.")
+ return {}
- metadata["image"] = time.strftime('%Y%m%d%H%M%S') + ".png"
+ metadata["image"] = time.strftime('%Y%m%d%H%M%S') + ".png"
- with z.open("Metadata/plate_"+metadata["plateID"]+".png") as source_file:
- with open(os.path.join(os.getcwd(), 'static', 'prints', metadata["image"]), 'wb') as target_file:
- target_file.write(source_file.read())
+ with z.open("Metadata/plate_"+metadata["plateID"]+".png") as source_file:
+ with open(os.path.join(os.getcwd(), 'static', 'prints', metadata["image"]), 'wb') as target_file:
+ target_file.write(source_file.read())
- # Check for the Metadata/slice_info.config file
- gcode_path = "Metadata/plate_"+metadata["plateID"]+".gcode"
- metadata["gcode_path"] = gcode_path
- if gcode_path in z.namelist():
- with z.open(gcode_path) as gcode_file:
- metadata["filamentOrder"] = get_filament_order(gcode_file)
+ # Check for the Metadata/slice_info.config file
+ gcode_path = "Metadata/plate_"+metadata["plateID"]+".gcode"
+ metadata["gcode_path"] = gcode_path
+ if gcode_path in z.namelist():
+ with z.open(gcode_path) as gcode_file:
+ metadata["filamentOrder"] = get_filament_order(gcode_file)
- log(metadata)
+ log(metadata)
- return metadata
+ return metadata
except requests.exceptions.RequestException as e:
log(f"Error downloading file: {e}")
@@ -293,3 +285,61 @@ def getMetaDataFrom3mf(url):
except Exception as e:
log(f"An unexpected error occurred: {e}")
return {}
+
+# Reteive the model from temp file cache if we already have it, otherwise grab it from the printer.
+def retrieveModel(url):
+ if not url:
+ log("[DEBUG] No URL supplied to retireve printer model")
+ return None
+
+ try:
+ temp_file_name = ""
+
+ # Check if we have already downloaded the file previously
+ if not DOWNLOADED_FILES.get(f"{PRINTER_NAME}_{PRINTER_IP}"):
+ log("[DEBUG] Don't already have the model in temp file cache")
+
+ # Create a temporary file, DELETE is FALSE, make sure to cleanup when printing ends
+ #TODO handle cleanup on crashes
+
+ # Create OpenSpoolMan temp directory for easier/safer file cleanup
+ temp_dir = os.path.join(tempfile.gettempdir(), "OpenSpoolMan")
+ os.makedirs(temp_dir, exist_ok=True)
+
+ with tempfile.NamedTemporaryFile(dir=temp_dir, delete_on_close=False,delete=False,prefix=f"{PRINTER_NAME}_{PRINTER_IP}_", suffix=".3mf") as temp_file:
+ temp_file_name = temp_file.name
+
+ if url.startswith("http"):
+ download3mfFromCloud(url, temp_file)
+ elif url.startswith("local:"):
+ download3mfFromLocalFilesystem(url.replace("local:", ""), temp_file)
+ else:
+ download3mfFromFTP(url.rpartition('/')[-1], temp_file) # Pull just filename to clear out any unexpected paths
+
+ temp_file.close()
+ DOWNLOADED_FILES[f"{PRINTER_NAME}_{PRINTER_IP}"] = temp_file_name # Use Printer name + IP for future multiple printer support
+ log(f"3MF file downloaded and saved as {temp_file_name}.")
+
+ # Set to previously downloaded temp file
+ else:
+ temp_file_name = DOWNLOADED_FILES[f"{PRINTER_NAME}_{PRINTER_IP}"]
+ log(f"[DEBUG] Using already cached temp file, path: {temp_file_name}")
+
+ return temp_file_name
+
+ except requests.exceptions.RequestException as e:
+ log(f"Error downloading file: {e}")
+ return None
+ except Exception as e:
+ log(f"An unexpected error occurred: {e}")
+ return None
+
+def clearTempFile(PRINTER_NAME, PRINTER_IP):
+
+ temp_file_path = DOWNLOADED_FILES.pop(f"{PRINTER_NAME}_{PRINTER_IP}", None)
+
+ if temp_file_path:
+ if os.path.exists(temp_file_path):
+ os.remove(temp_file_path)
+ print(f"Temp file: {temp_file_path} cleared for printer: {PRINTER_NAME} {PRINTER_IP}.")
+