Improvement: Add AUTO SPEND feature that tracks your filament usage
Bump version to 0.0.2
This commit is contained in:
parent
1530e9de86
commit
ac1a391b18
20
README.md
20
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
|
||||
|
@ -1 +1 @@
|
||||
__version__ = '0.0.1'
|
||||
__version__ = '0.0.2'
|
||||
|
@ -3,3 +3,4 @@ PRINTER_ACCESS_CODE=
|
||||
PRINTER_ID=
|
||||
PRINTER_IP=
|
||||
SPOOLMAN_BASE_URL=
|
||||
AUTO_SPEND=False
|
||||
|
@ -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)
|
||||
|
@ -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"]:
|
||||
|
@ -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
70
tools_3mf.py
Normal 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}")
|
Loading…
x
Reference in New Issue
Block a user