ftp_profiles.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """Per-printer-model FTP tuning knobs.
  2. Mirrors the shape of :mod:`backend.app.services.camera_profiles` — a
  3. small registry of per-model overrides so quirky firmwares can be
  4. tuned without sprinkling ``if model == "X":`` branches through
  5. ``bambu_ftp.py``. Adding a new model's quirk is a config edit (an
  6. entry in ``_PROFILES`` plus the alias for its internal SSDP code if
  7. needed), not another hard-coded branch.
  8. The default profile matches the historical pre-fix behaviour, so
  9. every model that doesn't have an entry here keeps its existing FTP
  10. behaviour byte-for-byte.
  11. Currently only the TLS-version cap lives here (P2S firmware
  12. 01.02.00.00 needs it — see ``cap_tls_v1_2`` below). The A1
  13. data-channel-plaintext quirk still lives in :class:`BambuFTPClient`
  14. via ``A1_MODELS`` / ``skip_session_reuse``; folding that into a
  15. profile field is a future cleanup, not load-bearing for this fix.
  16. """
  17. from __future__ import annotations
  18. from dataclasses import dataclass
  19. @dataclass(frozen=True)
  20. class FTPProfile:
  21. """Tuning knobs for one printer model's FTP path.
  22. All defaults reflect the historical behaviour. Models with quirky
  23. firmware override individual fields rather than re-defining the
  24. whole profile.
  25. """
  26. # Pin the SSL context's ``maximum_version`` to TLS 1.2.
  27. #
  28. # ``ssl.create_default_context()`` negotiates TLS 1.3 when both peers
  29. # support it. Some Bambu printer firmwares (P2S 01.02.00.00 confirmed
  30. # by @iitazz, #1401) implement session reuse on the FTPS data
  31. # channel against an old vsFTPd build that doesn't tolerate TLS
  32. # 1.3's asynchronous session-ticket model: the data channel gets
  33. # torn down mid-stream and the upload aborts with 426 "Failure
  34. # reading network stream" — visible as a clean truncation at a
  35. # chunk boundary (one reporter saw exactly 7 × 64 KB landed on
  36. # the printer). Capping to TLS 1.2 makes session resumption
  37. # synchronous and the upload completes normally.
  38. #
  39. # Note this cap only bites on models that *offer* 1.3 in the first
  40. # place. Probed directly on :990, an X1C and an H2D both refuse
  41. # TLS 1.0, 1.1 and 1.3 with a handshake_failure alert and complete
  42. # only on 1.2 — so for those models the cap is a no-op and the
  43. # negotiated version was never 1.3. The P2S evidently does offer
  44. # 1.3, which is why it alone surfaced the session-reuse bug.
  45. # (P1S untested; no claim made either way.)
  46. #
  47. # **Defaults to False** — only applied to printer models where a
  48. # reporter has confirmed the symptom. This is deliberately
  49. # conservative; flipping a printer to the capped path is a config
  50. # edit when a new model surfaces the same bug.
  51. cap_tls_v1_2: bool = False
  52. # ---------------------------------------------------------------------------
  53. # Profile registry
  54. # ---------------------------------------------------------------------------
  55. # Default profile = historical behaviour. Used for every model that
  56. # doesn't have an entry in ``_PROFILES``.
  57. DEFAULT_PROFILE = FTPProfile()
  58. # Per-model overrides. Keys are uppercase display names (e.g. "P2S")
  59. # AFTER alias normalisation, so internal SSDP codes ("N7") resolve via
  60. # ``_MODEL_ALIASES`` below.
  61. _PROFILES: dict[str, FTPProfile] = {
  62. # P2S firmware 01.02.00.00 trips the vsFTPd + TLS 1.3 session-reuse
  63. # bug on the FTPS data channel (#1401, reporter @iitazz). Cap to
  64. # TLS 1.2 so session resumption is synchronous and the upload
  65. # completes.
  66. "P2S": FTPProfile(
  67. cap_tls_v1_2=True,
  68. ),
  69. # X2D firmware 01.01.00.00 fails the implicit-FTPS handshake on
  70. # port 990 with ``[SSL: WRONG_VERSION_NUMBER]`` against Python
  71. # 3.13's default TLS-1.3 ClientHello (#1638, reporter @vasmarfas).
  72. # Without the 3MF download the print falls through to the no-3MF
  73. # fallback archive path and the card lands almost empty (no
  74. # filament total, no layers, no MakerWorld link). Cap to TLS 1.2
  75. # by analogy with P2S; if the symptom turns out to be a different
  76. # FTPS variant on the X2D (explicit AUTH TLS, different port) the
  77. # entry stays useful as a per-model tuning slot for the follow-up.
  78. "X2D": FTPProfile(
  79. cap_tls_v1_2=True,
  80. ),
  81. # H2C firmware 01.02.00.00 (#2582, reporter @gyrene2083) — same H2
  82. # generation and same firmware line as P2S, and with no profile it
  83. # ran on the Python-default TLS 1.3. Reported symptom is exactly the
  84. # one the X2D comment describes: the sliced 3MF intermittently fails
  85. # to come off the printer over FTPS, so the print drops to the no-3MF
  86. # fallback archive with no slice data — which is why the Print Log
  87. # shows no filament and nothing is deducted. Cap to TLS 1.2 by analogy
  88. # with P2S (intermittent "sometimes works" points at the session-reuse
  89. # variant, not X2D's deterministic handshake failure); if a debug
  90. # capture shows a different FTPS variant the entry stays the tuning slot.
  91. "H2C": FTPProfile(
  92. cap_tls_v1_2=True,
  93. ),
  94. }
  95. # SSDP internal codes that should resolve to a display-name profile.
  96. # Mirrors the same map in :mod:`camera_profiles`.
  97. _MODEL_ALIASES: dict[str, str] = {
  98. "N7": "P2S", # P2S internal SSDP code
  99. "N6": "X2D", # X2D internal SSDP code
  100. "O1C": "H2C", # H2C internal SSDP code
  101. "O1C2": "H2C", # H2C dual-nozzle variant SSDP code
  102. }
  103. def get_ftp_profile(model: str | None) -> FTPProfile:
  104. """Return the :class:`FTPProfile` for *model*, or the default.
  105. ``model`` can be either a display name (e.g. ``"P2S"``) or an
  106. internal SSDP code (e.g. ``"N7"``). Unknown / missing models fall
  107. back to :data:`DEFAULT_PROFILE` so the FTP path is never blocked
  108. on a missing entry.
  109. """
  110. if not model:
  111. return DEFAULT_PROFILE
  112. key = model.upper().strip()
  113. key = _MODEL_ALIASES.get(key, key)
  114. return _PROFILES.get(key, DEFAULT_PROFILE)