camwall.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. """Read-only Cam Wall feed for token-authenticated kiosk displays (#2531).
  2. The Cam Wall inside the SPA runs on the ordinary printers API, behind a JWT. A
  3. wall pinned to a TV has no login, so it authenticates with a long-lived
  4. ``camwall``-scoped token carried in the URL — and a URL on a lobby screen is
  5. about as private as a sticky note.
  6. That is why this endpoint exists instead of letting a token through to
  7. ``GET /printers``: the printer list carries ``serial_number`` and
  8. ``ip_address`` (see ``schemas/printer.py``), and neither belongs on a screen in
  9. a shared room. What a wall tile actually draws is the whole payload here — a
  10. name, a connection flag, a state, a progress bar.
  11. Notably absent is the print filename. A token wall renders the compact status
  12. overlay, so the part being printed is never named to the room; the field simply
  13. isn't served rather than being served and then hidden client-side.
  14. """
  15. import logging
  16. from fastapi import APIRouter, Depends
  17. from sqlalchemy import select
  18. from sqlalchemy.ext.asyncio import AsyncSession
  19. from backend.app.core.auth import RequireCamWallTokenIfAuthEnabled
  20. from backend.app.core.database import get_db
  21. from backend.app.core.printer_scope import PrinterScope
  22. from backend.app.models.printer import Printer
  23. from backend.app.services.printer_manager import printer_manager
  24. _logger = logging.getLogger(__name__)
  25. router = APIRouter(prefix="/camwall", tags=["camwall"])
  26. @router.get("/printers")
  27. async def list_camwall_printers(
  28. printer_scope: PrinterScope = RequireCamWallTokenIfAuthEnabled,
  29. db: AsyncSession = Depends(get_db),
  30. ) -> list[dict]:
  31. """Every printer the token's owner may see (#1727), plus the handful of
  32. status fields a Cam Wall tile draws.
  33. One call for the whole wall rather than one per printer: a kiosk polls this
  34. on a fixed interval with no WebSocket to invalidate it, and N+1 requests
  35. every few seconds is a poor trade for a screen nobody is interacting with.
  36. Ordered by name so tile positions stay put across polls — a wall that
  37. reshuffles itself is unusable to watch.
  38. """
  39. query = select(Printer).order_by(Printer.name)
  40. if (clause := printer_scope.where_strict(Printer.id)) is not None:
  41. query = query.where(clause)
  42. result = await db.execute(query)
  43. printers = list(result.scalars().all())
  44. payload: list[dict] = []
  45. for printer in printers:
  46. state = printer_manager.get_status(printer.id)
  47. entry: dict = {
  48. "id": printer.id,
  49. "name": printer.name,
  50. "camera_rotation": printer.camera_rotation or 0,
  51. # Mirrors get_printer_status(): no state object at all means the
  52. # printer was never connected this run; a state object still has
  53. # to be asked whether its link is currently up.
  54. "connected": bool(state and state.connected),
  55. "state": None,
  56. "progress": None,
  57. "remaining_time": None,
  58. "layer_num": None,
  59. "total_layers": None,
  60. # Codes only — enough for the client to run the same
  61. # filterKnownHMSErrors() it uses on the authenticated wall, so the
  62. # error chip means the same thing in both modes.
  63. "hms_errors": [],
  64. }
  65. if state is not None:
  66. entry.update(
  67. {
  68. "state": state.state,
  69. "progress": state.progress,
  70. "remaining_time": state.remaining_time,
  71. "layer_num": state.layer_num,
  72. "total_layers": state.total_layers,
  73. "hms_errors": [
  74. {
  75. "code": e.code,
  76. "attr": e.attr,
  77. "module": e.module,
  78. "severity": e.severity,
  79. "actions": e.actions or [],
  80. # The wall counts a fault only when Bambu publishes
  81. # text for it or it offers actions, the same rule as
  82. # the printer card, so it needs the text (#2728).
  83. "full_code": e.full_code,
  84. "description": e.description,
  85. }
  86. for e in (state.hms_errors or [])
  87. ],
  88. }
  89. )
  90. payload.append(entry)
  91. return payload