Browse Source

Fix camera port diagnostic for A1/P1 printers (#1799)

Stefano Maffeis 2 months ago
parent
commit
271560f7cb

+ 26 - 4
backend/app/services/printer_diagnostic.py

@@ -16,6 +16,7 @@ import socket
 
 from backend.app.models.printer import Printer
 from backend.app.schemas.printer import DiagnosticCheck, PrinterDiagnosticResult
+from backend.app.services.camera import get_camera_port
 from backend.app.services.discovery import is_running_in_docker
 from backend.app.services.printer_manager import printer_manager
 from backend.app.utils.printer_models import has_external_storage
@@ -26,6 +27,7 @@ logger = logging.getLogger(__name__)
 PORT_MQTT = 8883  # MQTT over TLS — control + status. Connection-critical.
 PORT_FTPS = 990  # FTPS — file upload; required to send prints.
 PORT_RTSPS = 322  # RTSPS — camera stream; optional.
+PORT_CHAMBER_IMAGE = 6000  # Chamber image protocol — A1/P1 camera stream; optional.
 
 _PORT_PROBE_TIMEOUT = 3.0
 
@@ -54,6 +56,19 @@ async def _check_port(ip: str, port: int, timeout: float = _PORT_PROBE_TIMEOUT)
         return False
 
 
+def _camera_port_for_printer(printer: Printer | None) -> tuple[int, str]:
+    """Return the model-specific camera diagnostic port and display protocol."""
+    if not printer:
+        return PORT_RTSPS, "RTSPS"
+    model = getattr(printer, "model", None)
+    if not model:
+        return PORT_RTSPS, "RTSPS"
+    camera_port = get_camera_port(model)
+    if camera_port == PORT_CHAMBER_IMAGE:
+        return camera_port, "Chamber Image"
+    return camera_port, "RTSPS"
+
+
 def _detect_docker_network_mode() -> str:
     """Detect Docker network mode.
 
@@ -118,15 +133,22 @@ async def run_connection_diagnostic(
     checks: list[DiagnosticCheck] = []
 
     # --- Port reachability (probed in parallel) ---
-    mqtt_ok, ftps_ok, rtsps_ok = await asyncio.gather(
+    camera_port, camera_protocol = _camera_port_for_printer(printer)
+    mqtt_ok, ftps_ok, camera_ok = await asyncio.gather(
         _check_port(ip_address, PORT_MQTT),
         _check_port(ip_address, PORT_FTPS),
-        _check_port(ip_address, PORT_RTSPS),
+        _check_port(ip_address, camera_port),
     )
-    # MQTT is connection-critical; FTPS/RTSPS only degrade printing/camera.
+    # MQTT is connection-critical; FTPS/camera only degrade printing/camera.
     checks.append(DiagnosticCheck(id="port_mqtt", status="pass" if mqtt_ok else "fail"))
     checks.append(DiagnosticCheck(id="port_ftps", status="pass" if ftps_ok else "warn"))
-    checks.append(DiagnosticCheck(id="port_rtsps", status="pass" if rtsps_ok else "warn"))
+    checks.append(
+        DiagnosticCheck(
+            id="port_rtsps",
+            status="pass" if camera_ok else "warn",
+            params={"port": camera_port, "protocol": camera_protocol},
+        )
+    )
 
     # --- Docker network mode ---
     network_mode: str | None = None

+ 23 - 1
backend/tests/unit/services/test_printer_diagnostic.py

@@ -21,7 +21,7 @@ def _statuses(result):
 
 def _port_probe(overrides=None):
     """Sync side_effect for _check_port. Defaults: every port reachable."""
-    reachable = {8883: True, 990: True, 322: True}
+    reachable = {8883: True, 990: True, 322: True, 6000: True}
     reachable.update(overrides or {})
 
     def _probe(ip, port, timeout=3.0):
@@ -142,6 +142,28 @@ class TestExistingPrinter:
         assert s["port_ftps"] == "warn"
         assert s["port_rtsps"] == "warn"
 
+    async def test_a1_mini_uses_chamber_image_camera_port(self):
+        # A1/P1-family printers use the chamber-image camera protocol on 6000,
+        # not RTSPS on 322. A closed 322 must not create a false camera warning.
+        with _Env(ports=_port_probe({322: False, 6000: True}), state=_state()):
+            result = await run_connection_diagnostic(
+                "192.168.1.50",
+                printer=_printer(model="A1 Mini"),
+            )
+        assert _statuses(result)["port_rtsps"] == "pass"
+        camera_check = next(c for c in result.checks if c.id == "port_rtsps")
+        assert camera_check.params == {"port": 6000, "protocol": "Chamber Image"}
+
+    async def test_rtsp_models_still_probe_rtsps_port(self):
+        with _Env(ports=_port_probe({322: False, 6000: True}), state=_state()):
+            result = await run_connection_diagnostic(
+                "192.168.1.50",
+                printer=_printer(model="X1C"),
+            )
+        assert _statuses(result)["port_rtsps"] == "warn"
+        camera_check = next(c for c in result.checks if c.id == "port_rtsps")
+        assert camera_check.params == {"port": 322, "protocol": "RTSPS"}
+
     async def test_developer_mode_off_is_a_problem(self):
         with _Env(state=_state(connected=True, developer_mode=False)):
             result = await run_connection_diagnostic("192.168.1.50", printer=_printer())

+ 24 - 0
frontend/src/__tests__/components/ConnectionDiagnosticModal.test.tsx

@@ -98,4 +98,28 @@ describe('ConnectionDiagnosticModal', () => {
 
     spy.mockRestore();
   });
+
+  it('renders model-specific camera port diagnostics', async () => {
+    const spy = vi.spyOn(api, 'diagnosePrinter').mockResolvedValue({
+      ...PROBLEM_RESULT,
+      overall: 'warnings',
+      checks: [
+        { id: 'port_mqtt', status: 'pass', params: {} },
+        { id: 'port_ftps', status: 'pass', params: {} },
+        {
+          id: 'port_rtsps',
+          status: 'warn',
+          params: { protocol: 'Chamber Image', port: 6000 },
+        },
+      ],
+    });
+
+    renderModal({ printerId: 1, printerName: 'Test A1 Mini', onClose: vi.fn() });
+
+    await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
+    expect(await screen.findByText(/Camera port \(Chamber Image 6000\)/i)).toBeInTheDocument();
+    expect(screen.getByText(/Port 6000 is unreachable/i)).toBeInTheDocument();
+
+    spy.mockRestore();
+  });
 });

+ 8 - 2
frontend/src/components/ConnectionDiagnostic.tsx

@@ -40,8 +40,12 @@ export function DiagnosticChecklist({ result }: { result: PrinterDiagnosticResul
         : 'bg-red-500/10 border-red-500/30 text-red-300';
 
   const renderCheck = (check: DiagnosticCheck) => {
+    const params =
+      check.id === 'port_rtsps'
+        ? { protocol: 'RTSPS', port: 322, ...check.params }
+        : check.params;
     const detail = t(`diagnostic.check.${check.id}.${check.status}`, {
-      ...check.params,
+      ...params,
       defaultValue: '',
     });
     return (
@@ -55,7 +59,9 @@ export function DiagnosticChecklist({ result }: { result: PrinterDiagnosticResul
           <StatusIcon status={check.status} />
         </div>
         <div className="flex-1 min-w-0">
-          <div className="text-sm text-white">{t(`diagnostic.check.${check.id}.title`)}</div>
+          <div className="text-sm text-white">
+            {t(`diagnostic.check.${check.id}.title`, params)}
+          </div>
           {detail && <div className="text-xs text-bambu-gray mt-0.5">{detail}</div>}
         </div>
       </li>

+ 2 - 2
frontend/src/i18n/locales/de.ts

@@ -5885,9 +5885,9 @@ export default {
         skip: 'Nicht geprüft — eine aktive MQTT-Verbindung ist erforderlich. Bei älteren Slicern, in denen diese Einstellung nur im Slicer existiert, meldet sie der Drucker nicht — diese Prüfung besteht auch dann, wenn die Option deaktiviert ist. Prüfen Sie Installationsschritt 4 in diesem Fall manuell.',
       },
       port_rtsps: {
-        title: 'Kameraport (RTSPS 322)',
+        title: 'Kameraport ({{protocol}} {{port}})',
         pass: 'Erreichbar — der Kamerastream funktioniert.',
-        warn: 'Port 322 ist nicht erreichbar. Die Live-Kameraansicht funktioniert nicht. Dies betrifft das Drucken nicht.',
+        warn: 'Port {{port}} ist nicht erreichbar. Die Live-Kameraansicht funktioniert nicht. Dies betrifft das Drucken nicht.',
       },
       network_mode: {
         title: 'Docker-Netzwerkmodus',

+ 2 - 2
frontend/src/i18n/locales/en.ts

@@ -5910,9 +5910,9 @@ export default {
         skip: 'Not checked — needs a live MQTT connection. On older slicers where this setting lives only in the slicer the printer never reports it, so this check will pass even when the option is off — verify install step 4 manually.',
       },
       port_rtsps: {
-        title: 'Camera port (RTSPS 322)',
+        title: 'Camera port ({{protocol}} {{port}})',
         pass: 'Reachable — the camera stream will work.',
-        warn: 'Port 322 is unreachable. The live camera view will not work. This does not affect printing.',
+        warn: 'Port {{port}} is unreachable. The live camera view will not work. This does not affect printing.',
       },
       network_mode: {
         title: 'Docker network mode',

+ 2 - 2
frontend/src/i18n/locales/es.ts

@@ -5894,9 +5894,9 @@ export default {
         skip: 'No comprobado — se necesita una conexión MQTT activa. En slicers más antiguos donde este ajuste solo existe en el slicer, la impresora no lo reporta, así que esta comprobación pasa aunque la opción esté desactivada — verifique el paso 4 de la instalación manualmente.',
       },
       port_rtsps: {
-        title: 'Puerto de la cámara (RTSPS 322)',
+        title: 'Puerto de la cámara ({{protocol}} {{port}})',
         pass: 'Accesible — la transmisión de la cámara funcionará.',
-        warn: 'El puerto 322 no es accesible. La vista de la cámara en directo no funcionará. Esto no afecta a la impresión.',
+        warn: 'El puerto {{port}} no es accesible. La vista de la cámara en directo no funcionará. Esto no afecta a la impresión.',
       },
       network_mode: {
         title: 'Modo de red de Docker',

+ 2 - 2
frontend/src/i18n/locales/fr.ts

@@ -5875,9 +5875,9 @@ export default {
         skip: 'Non vérifié — une connexion MQTT active est requise. Sur les slicers plus anciens où ce paramètre n\'existe que dans le slicer, l\'imprimante ne le signale pas, donc cette vérification passe même si l\'option est désactivée — vérifiez l\'étape 4 de l\'installation manuellement.',
       },
       port_rtsps: {
-        title: 'Port caméra (RTSPS 322)',
+        title: 'Port caméra ({{protocol}} {{port}})',
         pass: 'Accessible — le flux de la caméra fonctionnera.',
-        warn: 'Le port 322 est inaccessible. La vue caméra en direct ne fonctionnera pas. Cela n\'affecte pas l\'impression.',
+        warn: 'Le port {{port}} est inaccessible. La vue caméra en direct ne fonctionnera pas. Cela n\'affecte pas l\'impression.',
       },
       network_mode: {
         title: 'Mode réseau Docker',

+ 2 - 2
frontend/src/i18n/locales/it.ts

@@ -5874,9 +5874,9 @@ export default {
         skip: 'Non verificato — è necessaria una connessione MQTT attiva. Negli slicer più vecchi dove questa impostazione esiste solo nello slicer, la stampante non la segnala, quindi questo controllo passa anche se l\'opzione è disattivata — verifica manualmente il passo 4 dell\'installazione.',
       },
       port_rtsps: {
-        title: 'Porta fotocamera (RTSPS 322)',
+        title: 'Porta fotocamera ({{protocol}} {{port}})',
         pass: 'Raggiungibile — lo streaming della fotocamera funzionerà.',
-        warn: 'La porta 322 non è raggiungibile. La visualizzazione live della fotocamera non funzionerà. Questo non influisce sulla stampa.',
+        warn: 'La porta {{port}} non è raggiungibile. La visualizzazione live della fotocamera non funzionerà. Questo non influisce sulla stampa.',
       },
       network_mode: {
         title: 'Modalità di rete Docker',

+ 2 - 2
frontend/src/i18n/locales/ja.ts

@@ -5886,9 +5886,9 @@ export default {
         skip: '未確認 — アクティブなMQTT接続が必要です。古いスライサーでこの設定がスライサー側のみに存在する場合、プリンターはそれを報告しないため、オプションが無効でもこのチェックは通過します — インストール手順4を手動で確認してください。',
       },
       port_rtsps: {
-        title: 'カメラポート (RTSPS 322)',
+        title: 'カメラポート ({{protocol}} {{port}})',
         pass: '到達可能 — カメラストリームは機能します。',
-        warn: 'ポート322に到達できません。ライブカメラ表示は機能しません。これは印刷には影響しません。',
+        warn: 'ポート{{port}}に到達できません。ライブカメラ表示は機能しません。これは印刷には影響しません。',
       },
       network_mode: {
         title: 'Dockerネットワークモード',

+ 2 - 2
frontend/src/i18n/locales/ko.ts

@@ -5934,9 +5934,9 @@ export default {
         skip: '확인되지 않음 — 활성 MQTT 연결이 필요합니다. 이 설정이 슬라이서에만 존재하는 이전 슬라이서에서는 프린터가 보고하지 않으므로, 옵션이 꺼져 있어도 이 검사는 통과합니다 — 설치 단계 4를 수동으로 확인하세요.'
       },
       port_rtsps: {
-        title: '카메라 포트 (RTSPS 322)',
+        title: '카메라 포트 ({{protocol}} {{port}})',
         pass: '연결 가능 — 카메라 스트림이 작동합니다.',
-        warn: '포트 322에 연결할 수 없습니다. 라이브 카메라 보기가 작동하지 않습니다. 인쇄에는 영향을 주지 않습니다.'
+        warn: '포트 {{port}}에 연결할 수 없습니다. 라이브 카메라 보기가 작동하지 않습니다. 인쇄에는 영향을 주지 않습니다.'
       },
       network_mode: {
         title: 'Docker 네트워크 모드',

+ 2 - 2
frontend/src/i18n/locales/pt-BR.ts

@@ -5874,9 +5874,9 @@ export default {
         skip: 'Não verificado — é necessária uma conexão MQTT ativa. Em fatiadores mais antigos onde essa configuração existe apenas no fatiador, a impressora não a reporta, então esta verificação passa mesmo com a opção desligada — verifique o passo 4 da instalação manualmente.',
       },
       port_rtsps: {
-        title: 'Porta da câmera (RTSPS 322)',
+        title: 'Porta da câmera ({{protocol}} {{port}})',
         pass: 'Acessível — o streaming da câmera funcionará.',
-        warn: 'A porta 322 está inacessível. A visualização ao vivo da câmera não funcionará. Isso não afeta a impressão.',
+        warn: 'A porta {{port}} está inacessível. A visualização ao vivo da câmera não funcionará. Isso não afeta a impressão.',
       },
       network_mode: {
         title: 'Modo de rede Docker',

+ 2 - 2
frontend/src/i18n/locales/tr.ts

@@ -5824,9 +5824,9 @@ export default {
         skip: 'Kontrol edilmedi — etkin bir MQTT bağlantısı gerekli. Bu ayarın yalnızca dilimleyicide bulunduğu eski dilimleyicilerde yazıcı bunu bildirmez, bu nedenle seçenek kapalı olsa bile bu kontrol geçer — kurulum adımı 4\'ü manuel olarak doğrulayın.',
       },
       port_rtsps: {
-        title: 'Kamera portu (RTSPS 322)',
+        title: 'Kamera portu ({{protocol}} {{port}})',
         pass: 'Erişilebilir — kamera akışı çalışacak.',
-        warn: 'Port 322 erişilemez. Canlı kamera görünümü çalışmayacak. Bu, baskıyı etkilemez.',
+        warn: 'Port {{port}} erişilemez. Canlı kamera görünümü çalışmayacak. Bu, baskıyı etkilemez.',
       },
       network_mode: {
         title: 'Docker ağ modu',

+ 2 - 2
frontend/src/i18n/locales/zh-CN.ts

@@ -5873,9 +5873,9 @@ export default {
         skip: '未检查 — 需要有效的 MQTT 连接。在该设置仅存在于切片机中的较旧切片机上,打印机不会报告此设置,因此即使选项已关闭,此检查也会通过 — 请手动验证安装步骤 4。',
       },
       port_rtsps: {
-        title: '摄像头端口(RTSPS 322)',
+        title: '摄像头端口({{protocol}} {{port}})',
         pass: '可达 — 摄像头视频流将正常工作。',
-        warn: '端口 322 不可达。实时摄像头视图将无法工作。这不影响打印。',
+        warn: '端口 {{port}} 不可达。实时摄像头视图将无法工作。这不影响打印。',
       },
       network_mode: {
         title: 'Docker 网络模式',

+ 2 - 2
frontend/src/i18n/locales/zh-TW.ts

@@ -5873,9 +5873,9 @@ export default {
         skip: '未檢查 — 需要有效的 MQTT 連線。在該設定僅存在於切片機中的較舊切片機上,印表機不會回報此設定,因此即使選項已關閉,此檢查也會通過 — 請手動驗證安裝步驟 4。',
       },
       port_rtsps: {
-        title: '攝影機連接埠(RTSPS 322)',
+        title: '攝影機連接埠({{protocol}} {{port}})',
         pass: '可達 — 攝影機串流將正常運作。',
-        warn: '連接埠 322 無法連線。即時攝影機檢視將無法運作。這不影響列印。',
+        warn: '連接埠 {{port}} 無法連線。即時攝影機檢視將無法運作。這不影響列印。',
       },
       network_mode: {
         title: 'Docker 網路模式',