mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
Fix A1/A1 Mini FTP compatibility and add configurable timeout
Resolves FTP transfer failures on A1/A1 Mini printers caused by SSL session reuse incompatibility. Also adds configurable FTP timeout setting for slow WiFi connections. Changes: - Add SSL session reuse bypass for A1/A1 Mini printers (auto-detected) - Add printer_model parameter to FTP functions for model detection - Add configurable FTP timeout (10-120s, default 30s) in Settings - Update all FTP callers: archiving, reprint, timelapse, firmware upload Technical details: - A1/A1 Mini printers don't support vsFTPd's SSL session reuse - X1C/P1S continue to use session reuse as required by their FTP server - ImplicitFTP_TLS now accepts skip_session_reuse flag - BambuFTPClient auto-detects A1/A1 Mini and sets flag accordingly
This commit is contained in:
@@ -26,8 +26,14 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **FTP retry for unreliable WiFi** - Configurable retry logic for all FTP operations:
|
||||
- Enable/disable retry in Settings > General > FTP Retry
|
||||
- Configure retry count (1-10 attempts) and delay (1-30 seconds)
|
||||
- Configurable connection timeout (10-120 seconds, default 30s)
|
||||
- Applies to: 3MF archiving, print uploads, timelapse downloads, firmware updates
|
||||
- Helps P1S, X1C, and other printers with weak WiFi connections
|
||||
- **A1/A1 Mini FTP fix** - Resolved FTP upload failures on A1 series printers:
|
||||
- A1 printers don't support SSL session reuse on data connections
|
||||
- Automatic detection and workaround for A1 and A1 Mini models
|
||||
- Maintains full encryption while skipping problematic session reuse
|
||||
- Fixes "read operation timed out" errors during file uploads
|
||||
|
||||
### Fixed
|
||||
- **QR code endpoint** - Fixed 500 error on archive QR code generation:
|
||||
|
||||
@@ -1088,7 +1088,7 @@ async def scan_timelapse(
|
||||
remote_path = matching_file.get("path") or f"/timelapse/{matching_file['name']}"
|
||||
|
||||
# Get FTP retry settings
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay = await get_ftp_retry_settings()
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
|
||||
|
||||
if ftp_retry_enabled:
|
||||
timelapse_data = await with_ftp_retry(
|
||||
@@ -1096,12 +1096,20 @@ async def scan_timelapse(
|
||||
printer.ip_address,
|
||||
printer.access_code,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
max_retries=ftp_retry_count,
|
||||
retry_delay=ftp_retry_delay,
|
||||
operation_name=f"Download timelapse {matching_file['name']}",
|
||||
)
|
||||
else:
|
||||
timelapse_data = await download_file_bytes_async(printer.ip_address, printer.access_code, remote_path)
|
||||
timelapse_data = await download_file_bytes_async(
|
||||
printer.ip_address,
|
||||
printer.access_code,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
)
|
||||
|
||||
if not timelapse_data:
|
||||
raise HTTPException(500, "Failed to download timelapse")
|
||||
@@ -1166,7 +1174,7 @@ async def select_timelapse(
|
||||
raise HTTPException(404, f"Timelapse '{filename}' not found on printer")
|
||||
|
||||
# Download and attach
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay = await get_ftp_retry_settings()
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
|
||||
|
||||
if ftp_retry_enabled:
|
||||
timelapse_data = await with_ftp_retry(
|
||||
@@ -1174,12 +1182,20 @@ async def select_timelapse(
|
||||
printer.ip_address,
|
||||
printer.access_code,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
max_retries=ftp_retry_count,
|
||||
retry_delay=ftp_retry_delay,
|
||||
operation_name=f"Download timelapse {filename}",
|
||||
)
|
||||
else:
|
||||
timelapse_data = await download_file_bytes_async(printer.ip_address, printer.access_code, remote_path)
|
||||
timelapse_data = await download_file_bytes_async(
|
||||
printer.ip_address,
|
||||
printer.access_code,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
)
|
||||
|
||||
if not timelapse_data:
|
||||
raise HTTPException(500, "Failed to download timelapse")
|
||||
@@ -2071,16 +2087,18 @@ async def reprint_archive(
|
||||
remote_filename = f"{base_name}.3mf"
|
||||
remote_path = f"/{remote_filename}"
|
||||
|
||||
# Get FTP retry settings
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
|
||||
|
||||
# Delete existing file if present (avoids 553 error)
|
||||
await delete_file_async(
|
||||
printer.ip_address,
|
||||
printer.access_code,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
)
|
||||
|
||||
# Get FTP retry settings
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay = await get_ftp_retry_settings()
|
||||
|
||||
if ftp_retry_enabled:
|
||||
uploaded = await with_ftp_retry(
|
||||
upload_file_async,
|
||||
@@ -2088,6 +2106,8 @@ async def reprint_archive(
|
||||
printer.access_code,
|
||||
file_path,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
max_retries=ftp_retry_count,
|
||||
retry_delay=ftp_retry_delay,
|
||||
operation_name=f"Upload for reprint to {printer.name}",
|
||||
@@ -2098,6 +2118,8 @@ async def reprint_archive(
|
||||
printer.access_code,
|
||||
file_path,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
)
|
||||
|
||||
if not uploaded:
|
||||
|
||||
+5
-1
@@ -590,7 +590,7 @@ async def on_print_start(printer_id: int, data: dict):
|
||||
downloaded_filename = None
|
||||
|
||||
# Get FTP retry settings
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay = await get_ftp_retry_settings()
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
|
||||
|
||||
for try_filename in possible_names:
|
||||
if not try_filename.endswith(".3mf"):
|
||||
@@ -615,6 +615,8 @@ async def on_print_start(printer_id: int, data: dict):
|
||||
printer.access_code,
|
||||
remote_path,
|
||||
temp_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
max_retries=ftp_retry_count,
|
||||
retry_delay=ftp_retry_delay,
|
||||
operation_name=f"Download 3MF from {remote_path}",
|
||||
@@ -625,6 +627,8 @@ async def on_print_start(printer_id: int, data: dict):
|
||||
printer.access_code,
|
||||
remote_path,
|
||||
temp_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
)
|
||||
if downloaded:
|
||||
downloaded_filename = try_filename
|
||||
|
||||
@@ -15,11 +15,16 @@ T = TypeVar("T")
|
||||
|
||||
|
||||
class ImplicitFTP_TLS(FTP_TLS):
|
||||
"""FTP_TLS subclass for implicit FTPS (port 990) with session reuse."""
|
||||
"""FTP_TLS subclass for implicit FTPS (port 990) with optional session reuse.
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
SSL session reuse is required by X1C/P1S printers (vsFTPd), but causes issues
|
||||
with A1/A1 Mini printers. Set skip_session_reuse=True for A1 printers.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, skip_session_reuse: bool = False, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._sock = None
|
||||
self.skip_session_reuse = skip_session_reuse
|
||||
self.ssl_context = ssl.create_default_context()
|
||||
self.ssl_context.check_hostname = False
|
||||
self.ssl_context.verify_mode = ssl.CERT_NONE
|
||||
@@ -44,15 +49,27 @@ class ImplicitFTP_TLS(FTP_TLS):
|
||||
return self.welcome
|
||||
|
||||
def ntransfercmd(self, cmd, rest=None):
|
||||
"""Override to reuse SSL session for data connection (required by vsFTPd)."""
|
||||
"""Override to wrap data connection in SSL.
|
||||
|
||||
Session reuse is required by X1C/P1S (vsFTPd) but breaks A1/A1 Mini printers.
|
||||
When skip_session_reuse is True, we still encrypt the data channel but
|
||||
don't reuse the control connection's session.
|
||||
"""
|
||||
conn, size = FTP.ntransfercmd(self, cmd, rest)
|
||||
if self._prot_p:
|
||||
# Reuse the SSL session from the control connection
|
||||
conn = self.ssl_context.wrap_socket(
|
||||
conn,
|
||||
server_hostname=self.host,
|
||||
session=self.sock.session, # Reuse session!
|
||||
)
|
||||
if self.skip_session_reuse:
|
||||
# A1/A1 Mini: Don't reuse session (causes timeouts/hangs)
|
||||
conn = self.ssl_context.wrap_socket(
|
||||
conn,
|
||||
server_hostname=self.host,
|
||||
)
|
||||
else:
|
||||
# X1C/P1S: Reuse SSL session (required by vsFTPd)
|
||||
conn = self.ssl_context.wrap_socket(
|
||||
conn,
|
||||
server_hostname=self.host,
|
||||
session=self.sock.session,
|
||||
)
|
||||
return conn, size
|
||||
|
||||
|
||||
@@ -60,18 +77,39 @@ class BambuFTPClient:
|
||||
"""FTP client for retrieving files from Bambu Lab printers."""
|
||||
|
||||
FTP_PORT = 990
|
||||
DEFAULT_TIMEOUT = 30 # Default timeout in seconds (increased for A1 printers)
|
||||
# Models that need SSL session reuse disabled (A1 series has FTP issues with session reuse)
|
||||
SKIP_SESSION_REUSE_MODELS = ("A1", "A1 Mini")
|
||||
|
||||
def __init__(self, ip_address: str, access_code: str):
|
||||
def __init__(
|
||||
self,
|
||||
ip_address: str,
|
||||
access_code: str,
|
||||
timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
):
|
||||
self.ip_address = ip_address
|
||||
self.access_code = access_code
|
||||
self.timeout = timeout if timeout is not None else self.DEFAULT_TIMEOUT
|
||||
self.printer_model = printer_model
|
||||
self._ftp: ImplicitFTP_TLS | None = None
|
||||
|
||||
def _should_skip_session_reuse(self) -> bool:
|
||||
"""Check if this printer model needs SSL session reuse disabled."""
|
||||
if not self.printer_model:
|
||||
return False
|
||||
return self.printer_model in self.SKIP_SESSION_REUSE_MODELS
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Connect to the printer FTP server (implicit FTPS on port 990)."""
|
||||
try:
|
||||
logger.debug(f"FTP connecting to {self.ip_address}:{self.FTP_PORT}")
|
||||
self._ftp = ImplicitFTP_TLS()
|
||||
self._ftp.connect(self.ip_address, self.FTP_PORT, timeout=10)
|
||||
skip_reuse = self._should_skip_session_reuse()
|
||||
logger.debug(
|
||||
f"FTP connecting to {self.ip_address}:{self.FTP_PORT} "
|
||||
f"(timeout={self.timeout}s, model={self.printer_model}, skip_session_reuse={skip_reuse})"
|
||||
)
|
||||
self._ftp = ImplicitFTP_TLS(skip_session_reuse=skip_reuse)
|
||||
self._ftp.connect(self.ip_address, self.FTP_PORT, timeout=self.timeout)
|
||||
logger.debug("FTP connected, logging in as bblp")
|
||||
self._ftp.login("bblp", self.access_code)
|
||||
logger.debug("FTP logged in, setting prot_p and passive mode")
|
||||
@@ -314,12 +352,24 @@ async def download_file_async(
|
||||
remote_path: str,
|
||||
local_path: Path,
|
||||
timeout: float = 60.0,
|
||||
socket_timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
) -> bool:
|
||||
"""Async wrapper for downloading a file with timeout."""
|
||||
"""Async wrapper for downloading a file with timeout.
|
||||
|
||||
Args:
|
||||
ip_address: Printer IP address
|
||||
access_code: Printer access code
|
||||
remote_path: Remote file path on printer
|
||||
local_path: Local path to save file
|
||||
timeout: Overall operation timeout (asyncio)
|
||||
socket_timeout: FTP socket timeout for slow connections (e.g., A1 printers)
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _download():
|
||||
client = BambuFTPClient(ip_address, access_code)
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
if client.connect():
|
||||
try:
|
||||
return client.download_to_file(remote_path, local_path)
|
||||
@@ -339,12 +389,19 @@ async def download_file_try_paths_async(
|
||||
access_code: str,
|
||||
remote_paths: list[str],
|
||||
local_path: Path,
|
||||
socket_timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
) -> bool:
|
||||
"""Try downloading a file from multiple paths using a single connection."""
|
||||
"""Try downloading a file from multiple paths using a single connection.
|
||||
|
||||
Args:
|
||||
socket_timeout: FTP socket timeout for slow connections (e.g., A1 printers)
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _download():
|
||||
client = BambuFTPClient(ip_address, access_code)
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
if not client.connect():
|
||||
return False
|
||||
|
||||
@@ -363,13 +420,28 @@ async def upload_file_async(
|
||||
remote_path: str,
|
||||
timeout: float = 600.0,
|
||||
progress_callback: Callable[[int, int], None] | None = None,
|
||||
socket_timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
) -> bool:
|
||||
"""Async wrapper for uploading a file with timeout and progress callback."""
|
||||
"""Async wrapper for uploading a file with timeout and progress callback.
|
||||
|
||||
Args:
|
||||
ip_address: Printer IP address
|
||||
access_code: Printer access code
|
||||
local_path: Local file path to upload
|
||||
remote_path: Remote path on printer
|
||||
timeout: Overall operation timeout (asyncio)
|
||||
progress_callback: Optional callback for progress updates
|
||||
socket_timeout: FTP socket timeout for slow connections (e.g., A1 printers)
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _upload():
|
||||
logger.info(f"FTP connecting to {ip_address} for upload...")
|
||||
client = BambuFTPClient(ip_address, access_code)
|
||||
logger.info(
|
||||
f"FTP connecting to {ip_address} for upload (model={printer_model}, socket_timeout={socket_timeout}s)..."
|
||||
)
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
if client.connect():
|
||||
logger.info(f"FTP connected to {ip_address}")
|
||||
try:
|
||||
@@ -391,12 +463,19 @@ async def list_files_async(
|
||||
access_code: str,
|
||||
path: str = "/",
|
||||
timeout: float = 30.0,
|
||||
socket_timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
) -> list[dict]:
|
||||
"""Async wrapper for listing files with timeout."""
|
||||
"""Async wrapper for listing files with timeout.
|
||||
|
||||
Args:
|
||||
socket_timeout: FTP socket timeout for slow connections (e.g., A1 printers)
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _list():
|
||||
client = BambuFTPClient(ip_address, access_code)
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
if client.connect():
|
||||
try:
|
||||
return client.list_files(path)
|
||||
@@ -415,12 +494,19 @@ async def delete_file_async(
|
||||
ip_address: str,
|
||||
access_code: str,
|
||||
remote_path: str,
|
||||
socket_timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
) -> bool:
|
||||
"""Async wrapper for deleting a file."""
|
||||
"""Async wrapper for deleting a file.
|
||||
|
||||
Args:
|
||||
socket_timeout: FTP socket timeout for slow connections (e.g., A1 printers)
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _delete():
|
||||
client = BambuFTPClient(ip_address, access_code)
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
if client.connect():
|
||||
try:
|
||||
return client.delete_file(remote_path)
|
||||
@@ -435,12 +521,19 @@ async def download_file_bytes_async(
|
||||
ip_address: str,
|
||||
access_code: str,
|
||||
remote_path: str,
|
||||
socket_timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
) -> bytes | None:
|
||||
"""Async wrapper for downloading file as bytes."""
|
||||
"""Async wrapper for downloading file as bytes.
|
||||
|
||||
Args:
|
||||
socket_timeout: FTP socket timeout for slow connections (e.g., A1 printers)
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _download():
|
||||
client = BambuFTPClient(ip_address, access_code)
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
if client.connect():
|
||||
try:
|
||||
return client.download_file(remote_path)
|
||||
@@ -454,12 +547,19 @@ async def download_file_bytes_async(
|
||||
async def get_storage_info_async(
|
||||
ip_address: str,
|
||||
access_code: str,
|
||||
socket_timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
) -> dict | None:
|
||||
"""Async wrapper for getting storage info."""
|
||||
"""Async wrapper for getting storage info.
|
||||
|
||||
Args:
|
||||
socket_timeout: FTP socket timeout for slow connections (e.g., A1 printers)
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _get_storage():
|
||||
client = BambuFTPClient(ip_address, access_code)
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
if client.connect():
|
||||
try:
|
||||
return client.get_storage_info()
|
||||
@@ -470,8 +570,12 @@ async def get_storage_info_async(
|
||||
return await loop.run_in_executor(None, _get_storage)
|
||||
|
||||
|
||||
async def get_ftp_retry_settings() -> tuple[bool, int, float]:
|
||||
"""Get FTP retry settings from database."""
|
||||
async def get_ftp_retry_settings() -> tuple[bool, int, float, float]:
|
||||
"""Get FTP retry settings from database.
|
||||
|
||||
Returns:
|
||||
Tuple of (retry_enabled, retry_count, retry_delay, timeout)
|
||||
"""
|
||||
from backend.app.api.routes.settings import get_setting
|
||||
from backend.app.core.database import async_session
|
||||
|
||||
@@ -479,7 +583,8 @@ async def get_ftp_retry_settings() -> tuple[bool, int, float]:
|
||||
enabled = (await get_setting(db, "ftp_retry_enabled") or "true") == "true"
|
||||
count = int(await get_setting(db, "ftp_retry_count") or "3")
|
||||
delay = float(await get_setting(db, "ftp_retry_delay") or "2")
|
||||
return enabled, count, delay
|
||||
timeout = float(await get_setting(db, "ftp_timeout") or "30")
|
||||
return enabled, count, delay, timeout
|
||||
|
||||
|
||||
async def with_ftp_retry(
|
||||
|
||||
@@ -302,7 +302,7 @@ class FirmwareUpdateService:
|
||||
asyncio.run_coroutine_threadsafe(self._broadcast_progress(printer_id, state), loop)
|
||||
|
||||
# Get FTP retry settings
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay = await get_ftp_retry_settings()
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
|
||||
|
||||
if ftp_retry_enabled:
|
||||
success = await with_ftp_retry(
|
||||
@@ -312,6 +312,8 @@ class FirmwareUpdateService:
|
||||
firmware_path,
|
||||
remote_path,
|
||||
progress_callback=on_upload_progress,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=model,
|
||||
max_retries=ftp_retry_count,
|
||||
retry_delay=ftp_retry_delay,
|
||||
operation_name=f"Upload firmware to printer {printer_id}",
|
||||
@@ -323,6 +325,8 @@ class FirmwareUpdateService:
|
||||
firmware_path,
|
||||
remote_path,
|
||||
progress_callback=on_upload_progress,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=model,
|
||||
)
|
||||
|
||||
if not success:
|
||||
|
||||
@@ -272,7 +272,7 @@ class PrintScheduler:
|
||||
remote_path = f"/cache/{remote_filename}"
|
||||
|
||||
# Get FTP retry settings
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay = await get_ftp_retry_settings()
|
||||
ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
|
||||
|
||||
try:
|
||||
if ftp_retry_enabled:
|
||||
@@ -282,6 +282,8 @@ class PrintScheduler:
|
||||
printer.access_code,
|
||||
file_path,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
max_retries=ftp_retry_count,
|
||||
retry_delay=ftp_retry_delay,
|
||||
operation_name=f"Upload print to {printer.name}",
|
||||
@@ -292,6 +294,8 @@ class PrintScheduler:
|
||||
printer.access_code,
|
||||
file_path,
|
||||
remote_path,
|
||||
socket_timeout=ftp_timeout,
|
||||
printer_model=printer.model,
|
||||
)
|
||||
except Exception as e:
|
||||
uploaded = False
|
||||
|
||||
@@ -568,6 +568,7 @@ export interface AppSettings {
|
||||
ftp_retry_enabled: boolean;
|
||||
ftp_retry_count: number;
|
||||
ftp_retry_delay: number;
|
||||
ftp_timeout: number;
|
||||
}
|
||||
|
||||
export type AppSettingsUpdate = Partial<AppSettings>;
|
||||
|
||||
@@ -347,7 +347,8 @@ export function SettingsPage() {
|
||||
settings.default_printer_id !== localSettings.default_printer_id ||
|
||||
settings.ftp_retry_enabled !== localSettings.ftp_retry_enabled ||
|
||||
settings.ftp_retry_count !== localSettings.ftp_retry_count ||
|
||||
settings.ftp_retry_delay !== localSettings.ftp_retry_delay;
|
||||
settings.ftp_retry_delay !== localSettings.ftp_retry_delay ||
|
||||
settings.ftp_timeout !== localSettings.ftp_timeout;
|
||||
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
@@ -383,6 +384,7 @@ export function SettingsPage() {
|
||||
ftp_retry_enabled: localSettings.ftp_retry_enabled,
|
||||
ftp_retry_count: localSettings.ftp_retry_count,
|
||||
ftp_retry_delay: localSettings.ftp_retry_delay,
|
||||
ftp_timeout: localSettings.ftp_timeout,
|
||||
};
|
||||
updateMutation.mutate(settingsToSave);
|
||||
}, 500);
|
||||
@@ -1086,6 +1088,26 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 border-t border-bambu-dark-tertiary">
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
Connection timeout
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min="10"
|
||||
max="120"
|
||||
value={localSettings.ftp_timeout ?? 30}
|
||||
onChange={(e) => updateSetting('ftp_timeout', Math.min(120, Math.max(10, parseInt(e.target.value) || 30)))}
|
||||
className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
<span className="text-bambu-gray">seconds</span>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray mt-1">
|
||||
Socket timeout for slow connections. Increase for A1/A1 Mini printers with weak WiFi (10-120)
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -23,7 +23,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-_3zW21zb.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-7FRtmIRx.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BEtulymk.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user