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_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 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 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 the server (wsgi.py)
|
||||||
- Run Spool Man
|
- Run Spool Man
|
||||||
- Add following extra Fields to your SpoolMan:
|
- 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
|
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:
|
### Notes:
|
||||||
- If you change the BASE_URL of this app, you will need to reconfigure all NFC TAGS
|
- If you change the BASE_URL of this app, you will need to reconfigure all NFC TAGS
|
||||||
|
|
||||||
### TBD:
|
### TBD:
|
||||||
- Filament remaining in AMS (I have only AMS lite, if you have AMS we can test together)
|
- Filament remaining in AMS (I have only AMS lite, if you have AMS we can test together)
|
||||||
- Filament spending based on printing
|
- 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
|
- Evidently needed GUI improvements
|
||||||
- Code cleanup
|
- Code cleanup
|
||||||
- Video showcase
|
- Video showcase
|
||||||
- Docker compose SSL
|
- Docker compose SSL
|
||||||
|
- Logs
|
||||||
- TODOs
|
- 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_ID=
|
||||||
PRINTER_IP=
|
PRINTER_IP=
|
||||||
SPOOLMAN_BASE_URL=
|
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
|
PRINTER_IP = os.getenv('PRINTER_IP') # Printer local IP address - Check wireless on printer
|
||||||
SPOOLMAN_BASE_URL = os.getenv('SPOOLMAN_BASE_URL')
|
SPOOLMAN_BASE_URL = os.getenv('SPOOLMAN_BASE_URL')
|
||||||
SPOOLMAN_API_URL = f"{SPOOLMAN_BASE_URL}/api/v1"
|
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
|
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 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
|
MQTT_CLIENT = {} # Global variable storing MQTT Client
|
||||||
LAST_AMS_CONFIG = {} # Global variable storing last AMS configuration
|
LAST_AMS_CONFIG = {} # Global variable storing last AMS configuration
|
||||||
|
|
||||||
|
|
||||||
def num2letter(num):
|
def num2letter(num):
|
||||||
return chr(ord("A") + int(num))
|
return chr(ord("A") + int(num))
|
||||||
|
|
||||||
@ -28,22 +28,45 @@ def publish(client, msg):
|
|||||||
return False
|
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
|
# Inspired by https://github.com/Donkie/Spoolman/issues/217#issuecomment-2303022970
|
||||||
def on_message(client, userdata, msg):
|
def on_message(client, userdata, msg):
|
||||||
global LAST_AMS_CONFIG
|
global LAST_AMS_CONFIG
|
||||||
# TODO: Consume spool
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(msg.payload.decode())
|
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"]:
|
if "print" in data and "vt_tray" in data["print"]:
|
||||||
print(data)
|
|
||||||
LAST_AMS_CONFIG["vt_tray"] = data["print"]["vt_tray"]
|
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"]:
|
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"]
|
LAST_AMS_CONFIG["ams"] = data["print"]["ams"]["ams"]
|
||||||
|
|
||||||
print(LAST_AMS_CONFIG)
|
|
||||||
for ams in data["print"]["ams"]["ams"]:
|
for ams in data["print"]["ams"]["ams"]:
|
||||||
print(f"AMS [{num2letter(ams['id'])}] (hum: {ams['humidity']}, temp: {ams['temp']}ºC)")
|
print(f"AMS [{num2letter(ams['id'])}] (hum: {ams['humidity']}, temp: {ams['temp']}ºC)")
|
||||||
for tray in ams["tray"]:
|
for tray in ams["tray"]:
|
||||||
|
@ -25,3 +25,10 @@ def fetchSpoolList():
|
|||||||
print(response.status_code)
|
print(response.status_code)
|
||||||
print(response.text)
|
print(response.text)
|
||||||
return response.json()
|
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