spool_filament_preset.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. from datetime import datetime
  2. from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint, func
  3. from sqlalchemy.orm import Mapped, mapped_column, relationship
  4. from backend.app.core.database import Base
  5. class SpoolFilamentPreset(Base):
  6. """Per-printer-model override of a spool's slicer filament preset.
  7. ``Spool.slicer_filament`` holds ONE preset, and that is deliberate -- the
  8. spool form is printer-agnostic and the user picks the variant they want
  9. (see ``spool-form/utils.ts``). It stops being enough as soon as the same
  10. spool is used on two different printer models: a cloud or Orca preset is
  11. bound to a model (``@BBL X1C``), so the spool that carries an X1C variant
  12. configures an AMS slot on an H2C with a preset that machine has no profile
  13. for.
  14. Keyed on the printer MODEL, not the printer: ``@BBL X1C`` is the same
  15. preset on every X1C the user owns, and keying per machine would make them
  16. pick the identical value once per printer. (K profiles are the opposite --
  17. a K value is measured on one individual hotend -- which is why
  18. ``spool_k_profile`` keys on ``printer_id`` and this does not.)
  19. ``nozzle_diameter`` is part of the key because the preset lands on an AMS
  20. slot, and a slot feeds exactly one nozzle: on a dual-nozzle machine with
  21. two different diameters fitted, one preset per model cannot be right for
  22. both hotends, and diameter-specific presets genuinely exist
  23. (``Bambu PLA Basic @BBL A1M 0.2 nozzle``). Empty string means "any nozzle
  24. of this model". The spool form does not write that row -- it offers one row
  25. per nozzle size and nothing above them, because a preset lands on an AMS
  26. slot and a slot feeds exactly one nozzle -- but the level is kept in the
  27. cascade for API clients that want one value to cover a whole model.
  28. Resolution order is
  29. exact (model, diameter) -> (model, "") -> ``Spool.slicer_filament``
  30. which is what ``services.spool_filament_preset.resolve_spool_preset``
  31. implements. Empty string rather than NULL because NULLs compare distinct
  32. in a UNIQUE constraint on both SQLite and PostgreSQL, so a nullable
  33. column would happily store the same "any nozzle" row twice.
  34. """
  35. __tablename__ = "spool_filament_preset"
  36. __table_args__ = (UniqueConstraint("spool_id", "printer_model", "nozzle_diameter"),)
  37. id: Mapped[int] = mapped_column(primary_key=True)
  38. spool_id: Mapped[int] = mapped_column(ForeignKey("spool.id", ondelete="CASCADE"), index=True)
  39. # Matches ``printers.model`` ("X1C", "H2D", "A1 mini"), not a display name.
  40. printer_model: Mapped[str] = mapped_column(String(50))
  41. # "" = any nozzle of this model; otherwise the bare decimal the printer
  42. # reports ("0.4", "0.2"), the same form ``spool_k_profile`` stores.
  43. nozzle_diameter: Mapped[str] = mapped_column(String(10), default="")
  44. # Wider than ``Spool.slicer_filament`` (String(50)) on purpose: the same
  45. # values reach the Spoolman path, whose write schema already allows 128 /
  46. # 255, and a preset id that fits there must not truncate here.
  47. slicer_filament: Mapped[str | None] = mapped_column(String(128))
  48. slicer_filament_name: Mapped[str | None] = mapped_column(String(255))
  49. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  50. spool: Mapped["Spool"] = relationship(back_populates="filament_presets")
  51. class SpoolmanFilamentPreset(Base):
  52. """``SpoolFilamentPreset`` for a Spoolman-managed spool.
  53. Mirrors ``SpoolmanKProfile``: Spoolman owns the spool, Bambuddy owns this
  54. override, so the row is local and keyed by the remote spool id with no
  55. foreign key to enforce it. Kept in a Bambuddy table rather than in the
  56. spool's Spoolman ``extra`` dict for the same reason the K profiles are --
  57. it is Bambu-specific data that no other Spoolman client can use, and the
  58. extra dict cannot express a per-model list without hand-rolled JSON.
  59. """
  60. __tablename__ = "spoolman_filament_preset"
  61. __table_args__ = (UniqueConstraint("spoolman_spool_id", "printer_model", "nozzle_diameter"),)
  62. id: Mapped[int] = mapped_column(primary_key=True)
  63. spoolman_spool_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
  64. printer_model: Mapped[str] = mapped_column(String(50))
  65. nozzle_diameter: Mapped[str] = mapped_column(String(10), default="")
  66. slicer_filament: Mapped[str | None] = mapped_column(String(128))
  67. slicer_filament_name: Mapped[str | None] = mapped_column(String(255))
  68. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  69. from backend.app.models.spool import Spool # noqa: E402, F401