Improvement: Add AUTO SPEND feature that tracks your filament usage

Bump version to 0.0.2
This commit is contained in:
Filip Bednárik 2024-12-14 01:20:23 +01:00
parent 1530e9de86
commit ac1a391b18
7 changed files with 132 additions and 10 deletions

View File

@ -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

View File

@ -1 +1 @@
__version__ = '0.0.1'
__version__ = '0.0.2'

View File

@ -3,3 +3,4 @@ PRINTER_ACCESS_CODE=
PRINTER_ID=
PRINTER_IP=
SPOOLMAN_BASE_URL=
AUTO_SPEND=False

View File

@ -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)

View File

@ -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"]:

View File

@ -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)

70
tools_3mf.py Normal file
View File

@ -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}")