From ac1a391b1857394c51c099f5d140058826168da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Bedn=C3=A1rik?= Date: Sat, 14 Dec 2024 01:20:23 +0100 Subject: [PATCH] Improvement: Add AUTO SPEND feature that tracks your filament usage Bump version to 0.0.2 --- README.md | 20 +++++++++++++ __version__.py | 2 +- config.env.template | 1 + config.py | 1 + mqtt_bambulab.py | 41 ++++++++++++++++++++------ spoolman_client.py | 7 +++++ tools_3mf.py | 70 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 tools_3mf.py diff --git a/README.md b/README.md index d13b961..428a583 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Everything works locally without cloud access, you can use scripts/init_bambulab - set PRINTER_ACCESS_CODE - On your printer clicking on Setting -> Lan Only Mode -> Access Code (you _don't_ need to enable the LAN Only Mode) - set PRINTER_IP - On your printer clicking on Setting -> Lan Only Mode -> IP Address (you _don't_ need to enable the LAN Only Mode) - set SPOOLMAN_BASE_URL - according to your SpoolMan installation without trailing slash + - set AUTO_SPEND - to True if you want for system to track your filament spending (check AUTO_SPEND issues below) default to False - Run the server (wsgi.py) - Run Spool Man - Add following extra Fields to your SpoolMan: @@ -47,14 +48,33 @@ Run in docker by configuring config.env and running compose.yaml, you will need Run in kubernetes using helm chart, where you can configure the ingress with SSL. https://github.com/truecharts/public/blob/master/charts/library/common/values.yaml +### AUTO SPEND +You can turn this feature on to automatically update the spool usage in SpoolMan. +This feature is using slicer information about predicted filament weight usage (and in future correlating it with the progress of the printer to compute the estimate of filament used). + +This feature has currently following issues/drawbacks: + - Spending on the start of the print + - Not spending according to print process and spending full filament weight even if print fails + - Don't know if it works with LAN mode, since it downloads the 3MF file from cloud + - Doesn't work if you print from SD card + - Not tested with multiple AMS systems + - External spool spending not yet implemented + - Not handling the mismatch between the SpoolMan and AMS (if you don't have the Active Tray information correct in spoolman it won't work properly) + ### Notes: - If you change the BASE_URL of this app, you will need to reconfigure all NFC TAGS ### TBD: - Filament remaining in AMS (I have only AMS lite, if you have AMS we can test together) - Filament spending based on printing + - TODO: handle situation when the print doesn't finish + - TODO: what about locally run file? + - TODO: test with multiple AMS + - TODO: filament usage in external spool - Evidently needed GUI improvements - Code cleanup - Video showcase - Docker compose SSL + - Logs - TODOs + - Bambu Spools have apparently two RFID chips with two distinct UIDs so if you swap the filament to different tray it may fail, needs rework/testing diff --git a/__version__.py b/__version__.py index b5cb49c..699a98c 100644 --- a/__version__.py +++ b/__version__.py @@ -1 +1 @@ -__version__ = '0.0.1' +__version__ = '0.0.2' diff --git a/config.env.template b/config.env.template index e776d5a..fa4175b 100644 --- a/config.env.template +++ b/config.env.template @@ -3,3 +3,4 @@ PRINTER_ACCESS_CODE= PRINTER_ID= PRINTER_IP= SPOOLMAN_BASE_URL= +AUTO_SPEND=False diff --git a/config.py b/config.py index beec9b9..4a7b935 100644 --- a/config.py +++ b/config.py @@ -6,3 +6,4 @@ PRINTER_CODE = os.getenv('PRINTER_ACCESS_CODE') # Printer access code - Ru PRINTER_IP = os.getenv('PRINTER_IP') # Printer local IP address - Check wireless on printer SPOOLMAN_BASE_URL = os.getenv('SPOOLMAN_BASE_URL') SPOOLMAN_API_URL = f"{SPOOLMAN_BASE_URL}/api/v1" +AUTO_SPEND = os.getenv('AUTO_SPEND', False) diff --git a/mqtt_bambulab.py b/mqtt_bambulab.py index c57d19e..72bcb4e 100644 --- a/mqtt_bambulab.py +++ b/mqtt_bambulab.py @@ -5,14 +5,14 @@ from threading import Thread import paho.mqtt.client as mqtt -from config import PRINTER_ID, PRINTER_CODE, PRINTER_IP +from config import PRINTER_ID, PRINTER_CODE, PRINTER_IP, AUTO_SPEND from messages import GET_VERSION, PUSH_ALL -from spoolman_client import fetchSpoolList, patchExtraTags +from spoolman_client import fetchSpoolList, patchExtraTags, consumeSpool +from tools_3mf import getFilamentsUsageFrom3mf MQTT_CLIENT = {} # Global variable storing MQTT Client LAST_AMS_CONFIG = {} # Global variable storing last AMS configuration - def num2letter(num): return chr(ord("A") + int(num)) @@ -28,22 +28,45 @@ def publish(client, msg): return False + +def spendFilaments(filaments_usage): + print(filaments_usage) + ams_usage = {} + for tray_id, usage in filaments_usage: + if tray_id != -1: + #TODO: hardcoded ams_id + if ams_usage.get(f"{PRINTER_ID}_0_{tray_id}"): + ams_usage[f"{PRINTER_ID}_0_{tray_id}"] += float(usage) + else: + ams_usage[f"{PRINTER_ID}_0_{tray_id}"] = float(usage) + + for spool in fetchSpools(): + #TODO: What if there is a mismatch between AMS and SpoolMan? + if spool.get("extra") and spool.get("extra").get("active_tray") and ams_usage.get(json.loads(spool.get("extra").get("active_tray"))): + consumeSpool(spool["id"], ams_usage.get(json.loads(spool.get("extra").get("active_tray")))) + # Inspired by https://github.com/Donkie/Spoolman/issues/217#issuecomment-2303022970 def on_message(client, userdata, msg): global LAST_AMS_CONFIG - # TODO: Consume spool try: data = json.loads(msg.payload.decode()) - # print(data) + #print(data) + if AUTO_SPEND: + #Prepare AMS spending estimation + if "print" in data and "command" in data["print"] and data["print"]["command"] == "project_file" and "url" in data["print"]: + expected_filaments_usage = getFilamentsUsageFrom3mf(data["print"]["url"]) + ams_used = data["print"]["use_ams"] + ams_mapping = data["print"]["ams_mapping"] + if ams_used: + spendFilaments(zip(ams_mapping, expected_filaments_usage)) + + #Save external spool tray data if "print" in data and "vt_tray" in data["print"]: - print(data) LAST_AMS_CONFIG["vt_tray"] = data["print"]["vt_tray"] + #Save ams spool data if "print" in data and "ams" in data["print"] and "ams" in data["print"]["ams"]: - print(data) LAST_AMS_CONFIG["ams"] = data["print"]["ams"]["ams"] - - print(LAST_AMS_CONFIG) for ams in data["print"]["ams"]["ams"]: print(f"AMS [{num2letter(ams['id'])}] (hum: {ams['humidity']}, temp: {ams['temp']}ÂșC)") for tray in ams["tray"]: diff --git a/spoolman_client.py b/spoolman_client.py index 930a39b..b322f3f 100644 --- a/spoolman_client.py +++ b/spoolman_client.py @@ -25,3 +25,10 @@ def fetchSpoolList(): print(response.status_code) print(response.text) return response.json() + +def consumeSpool(spool_id, use_weight): + response = requests.put(f"{SPOOLMAN_API_URL}/spool/{spool_id}/use", json={ + "use_weight": use_weight + }) + print(response.status_code) + print(response.text) diff --git a/tools_3mf.py b/tools_3mf.py new file mode 100644 index 0000000..5664954 --- /dev/null +++ b/tools_3mf.py @@ -0,0 +1,70 @@ +import requests +import zipfile +import tempfile +import xml.etree.ElementTree as ET + +def getFilamentsUsageFrom3mf(url): + """ + Download a 3MF file from a URL, unzip it, and parse filament usage. + + Args: + url (str): URL to the 3MF file. + + Returns: + list[dict]: List of dictionaries with `tray_info_idx` and `used_g`. + """ + try: + # Create a temporary file + with tempfile.NamedTemporaryFile(delete=False, suffix=".3mf") as temp_file: + temp_file_name = temp_file.name + print("Downloading 3MF file...") + + # Download the file and save it to the temporary file + response = requests.get(url) + response.raise_for_status() + temp_file.write(response.content) + print(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 + result = [] + for plate in root.findall(".//plate"): + for filament in plate.findall(".//filament"): + used_g = filament.attrib.get("used_g") + if used_g: + result.append(used_g) + else: + result.append('0.0') + return result + else: + print(f"File '{slice_info_path}' not found in the archive.") + return [] + except requests.exceptions.RequestException as e: + print(f"Error downloading file: {e}") + return [] + except zipfile.BadZipFile: + print("The downloaded file is not a valid 3MF archive.") + return [] + except ET.ParseError: + print("Error parsing the XML file.") + return [] + except Exception as e: + print(f"An unexpected error occurred: {e}") + return [] + finally: + # Cleanup: Delete the temporary file + try: + import os + if os.path.exists(temp_file_name): + os.remove(temp_file_name) + except Exception as cleanup_error: + print(f"Error during cleanup: {cleanup_error}")