From 180934e4a24d5b7af46881daa83c9eb347303e12 Mon Sep 17 00:00:00 2001 From: Mister Teknis <54577164+MisterTeknis@users.noreply.github.com> Date: Sun, 4 Jan 2026 12:08:46 +1100 Subject: [PATCH 1/2] Updated LAN Only model download Corrected LAN Only gcode file download to fix filament tracking. Added path filtering to filename only. Added retry attempts to prevent race condition where files were not fully saved on the printer before trying to grab them. Required printer option: Store sent files on external storage Tested on P2S with AMS 2 Pro --- filament_usage_tracker.py | 2 +- tools_3mf.py | 76 +++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/filament_usage_tracker.py b/filament_usage_tracker.py index e7eeb532..821a3d9b 100644 --- a/filament_usage_tracker.py +++ b/filament_usage_tracker.py @@ -435,7 +435,7 @@ def _retrieve_model(self, model_url: str | None) -> str | None: download3mfFromLocalFilesystem(uri.path, model_file) else: log(f"[filament-tracker] Downloading model via FTP: {model_url}") - download3mfFromFTP(model_url.replace("ftp://", "").replace(".gcode", ""), model_file) + 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}") diff --git a/tools_3mf.py b/tools_3mf.py index 39bb566d..170398f3 100644 --- a/tools_3mf.py +++ b/tools_3mf.py @@ -7,6 +7,7 @@ import os import re import time +import io from datetime import datetime from config import PRINTER_CODE, PRINTER_IP from urllib.parse import urlparse @@ -72,34 +73,71 @@ def download3mfFromFTP(filename, destFile): remote_path = "/cache/" + filename local_path = destFile.name # 🔹 Download into the current directory encoded_remote_path = urllib.parse.quote(remote_path) - with open(local_path, "wb") as f: - c = pycurl.Curl() - url = f"ftps://{ftp_host}{encoded_remote_path}" - - # 🔹 Setup explicit FTPS connection (like FileZilla) - c.setopt(c.URL, url) - c.setopt(c.USERPWD, f"{ftp_user}:{ftp_pass}") - c.setopt(c.WRITEDATA, f) + + c = pycurl.Curl() + url = f"ftps://{ftp_host}{encoded_remote_path}" + + # 🔹 Setup explicit FTPS connection (like FileZilla) + c.setopt(c.URL, url) + c.setopt(c.USERPWD, f"{ftp_user}:{ftp_pass}") + #c.setopt(c.WRITEDATA, f) - # 🔹 Enable SSL/TLS - c.setopt(c.SSL_VERIFYPEER, 0) # Disable SSL verification - c.setopt(c.SSL_VERIFYHOST, 0) + # 🔹 Enable SSL/TLS + c.setopt(c.SSL_VERIFYPEER, 0) # Disable SSL verification + c.setopt(c.SSL_VERIFYHOST, 0) - # 🔹 Enable passive mode (like FileZilla) - c.setopt(c.FTP_SSL, c.FTPSSL_ALL) + # 🔹 Enable passive mode (like FileZilla) + c.setopt(c.FTP_SSL, c.FTPSSL_ALL) - # 🔹 Enable proper TLS authentication - c.setopt(c.FTPSSLAUTH, c.FTPAUTH_TLS) + # 🔹 Enable proper TLS authentication + c.setopt(c.FTPSSLAUTH, c.FTPAUTH_TLS) - log("[DEBUG] Starting file download...") + log(f"[DEBUG] Attempting file download of: {remote_path}") #Log attempted path + # Setup a retry loop + # Try to prevent race condition where trying to access file before it is fully in cache, causing File not found errors + max_retries = 3 + for attempt in range(1, max_retries + 1): try: + with open(local_path, "wb") as f: + c.setopt(c.WRITEDATA, f) + log(f"[DEBUG] Attempt {attempt}: Starting download of {filename}...") + + # Perform the transfer c.perform() + log("[DEBUG] File successfully downloaded!") + return True # Exit function on success + + # Error, check its just a file not found error before retry except pycurl.error as e: - log(f"[ERROR] cURL error: {e}") + err_code = e.args[0] + if err_code == 78: # File Not Found + if attempt < max_retries: + log(f"[WARNING] File not found. Printer might still be writing. Retrying in 1s...") + time.sleep(1) + continue + else: + log("[ERROR] File not found after max retries.") + log("[DEBUG] Listing found printer files in /cache directory") + buffer = io.BytesIO() + c.setopt(c.URL, f"ftps://{ftp_host}/cache/") + c.setopt(c.WRITEDATA, buffer) + c.setopt(c.DIRLISTONLY, True) + try: + c.perform() + log(f"[DEBUG] Directory Listing: {buffer.getvalue().decode('utf-8').splitlines()}") + except: + log("[ERROR] Could not retrieve directory listing.") + # Check if external storage not setup or connected. /cache is denied access + if err_code == 9: # Server denied you to change to the given directory + log("[DEBUG] Printer denied access to /cache path. Ensure external storage is setup to store print files in printer settings.") + break + else: + log(f"[ERROR] Fatal cURL error {err_code}: {e}") + break # Don't retry for non-78 File Not Found errors - c.close() + c.close() def download3mfFromLocalFilesystem(path, destFile): with open(path, "rb") as src_file: @@ -127,7 +165,7 @@ def getMetaDataFrom3mf(url): elif url.startswith("local:"): download3mfFromLocalFilesystem(url.replace("local:", ""), temp_file) else: - download3mfFromFTP(url.replace("ftp://", "").replace(".gcode",""), temp_file) + download3mfFromFTP(url.rpartition('/')[-1], temp_file) # Pull just filename to clear out any unexpected paths temp_file.close() metadata["model_path"] = url From 9ee0676339ab3ff1c638480342694e43090399b0 Mon Sep 17 00:00:00 2001 From: Mister Teknis <54577164+MisterTeknis@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:03:12 +1100 Subject: [PATCH 2/2] Fix retry connection reset Fix the pycurl connection not being reset properly between retries / directory listings causing dropped connection errors. --- tools_3mf.py | 97 +++++++++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/tools_3mf.py b/tools_3mf.py index 170398f3..bcbc8c3c 100644 --- a/tools_3mf.py +++ b/tools_3mf.py @@ -74,23 +74,9 @@ def download3mfFromFTP(filename, destFile): local_path = destFile.name # 🔹 Download into the current directory encoded_remote_path = urllib.parse.quote(remote_path) - c = pycurl.Curl() url = f"ftps://{ftp_host}{encoded_remote_path}" - # 🔹 Setup explicit FTPS connection (like FileZilla) - c.setopt(c.URL, url) - c.setopt(c.USERPWD, f"{ftp_user}:{ftp_pass}") - #c.setopt(c.WRITEDATA, f) - - # 🔹 Enable SSL/TLS - c.setopt(c.SSL_VERIFYPEER, 0) # Disable SSL verification - c.setopt(c.SSL_VERIFYHOST, 0) - - # 🔹 Enable passive mode (like FileZilla) - c.setopt(c.FTP_SSL, c.FTPSSL_ALL) - - # 🔹 Enable proper TLS authentication - c.setopt(c.FTPSSLAUTH, c.FTPAUTH_TLS) + log(f"[DEBUG] Attempting file download of: {remote_path}") #Log attempted path @@ -98,8 +84,11 @@ def download3mfFromFTP(filename, destFile): # Try to prevent race condition where trying to access file before it is fully in cache, causing File not found errors max_retries = 3 for attempt in range(1, max_retries + 1): - try: - with open(local_path, "wb") as f: + with open(local_path, "wb") as f: + c = setupPycurlConnection(ftp_user, ftp_pass) + try: + c.setopt(c.URL, url) + # Set output to file c.setopt(c.WRITEDATA, f) log(f"[DEBUG] Attempt {attempt}: Starting download of {filename}...") @@ -107,37 +96,59 @@ def download3mfFromFTP(filename, destFile): c.perform() log("[DEBUG] File successfully downloaded!") + c.close() return True # Exit function on success # Error, check its just a file not found error before retry - except pycurl.error as e: - err_code = e.args[0] - if err_code == 78: # File Not Found - if attempt < max_retries: - log(f"[WARNING] File not found. Printer might still be writing. Retrying in 1s...") - time.sleep(1) - continue + except pycurl.error as e: + err_code = e.args[0] + c.close() + if err_code == 78: # File Not Found + if attempt < max_retries: + log(f"[WARNING] File not found. Printer might still be writing. Retrying in 1s...") + time.sleep(2) + continue + else: + log("[ERROR] File not found after max retries.") + log("[DEBUG] Listing found printer files in /cache directory") + buffer = io.BytesIO() + c = setupPycurlConnection(ftp_user, ftp_pass) + c.setopt(c.URL, f"ftps://{ftp_host}/cache/") + c.setopt(c.WRITEDATA, buffer) + c.setopt(c.DIRLISTONLY, True) + try: + c.perform() + log(f"[DEBUG] Directory Listing: {buffer.getvalue().decode('utf-8').splitlines()}") + except: + log("[ERROR] Could not retrieve directory listing.") + # Check if external storage not setup or connected. /cache is denied access + if err_code == 9: # Server denied you to change to the given directory + log("[DEBUG] Printer denied access to /cache path. Ensure external storage is setup to store print files in printer settings.") + break else: - log("[ERROR] File not found after max retries.") - log("[DEBUG] Listing found printer files in /cache directory") - buffer = io.BytesIO() - c.setopt(c.URL, f"ftps://{ftp_host}/cache/") - c.setopt(c.WRITEDATA, buffer) - c.setopt(c.DIRLISTONLY, True) - try: - c.perform() - log(f"[DEBUG] Directory Listing: {buffer.getvalue().decode('utf-8').splitlines()}") - except: - log("[ERROR] Could not retrieve directory listing.") - # Check if external storage not setup or connected. /cache is denied access - if err_code == 9: # Server denied you to change to the given directory - log("[DEBUG] Printer denied access to /cache path. Ensure external storage is setup to store print files in printer settings.") - break - else: - log(f"[ERROR] Fatal cURL error {err_code}: {e}") - break # Don't retry for non-78 File Not Found errors + log(f"[ERROR] Fatal cURL error {err_code}: {e}") + break # Don't retry for non-78 File Not Found errors + +def setupPycurlConnection(ftp_user, ftp_pass): + # Setup shared options for curl connections + c = pycurl.Curl() + + # 🔹 Setup explicit FTPS connection (like FileZilla) + + c.setopt(c.USERPWD, f"{ftp_user}:{ftp_pass}") + + + # 🔹 Enable SSL/TLS + c.setopt(c.SSL_VERIFYPEER, 0) # Disable SSL verification + c.setopt(c.SSL_VERIFYHOST, 0) + + # 🔹 Enable passive mode (like FileZilla) + c.setopt(c.FTP_SSL, c.FTPSSL_ALL) + + # 🔹 Enable proper TLS authentication + c.setopt(c.FTPSSLAUTH, c.FTPAUTH_TLS) - c.close() + return c def download3mfFromLocalFilesystem(path, destFile): with open(path, "rb") as src_file: