Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions backend/app/models/tape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Series-neutral tape specification record.

Used by all printer model modules and the tape registry dispatch layer.
"""

from __future__ import annotations

from dataclasses import dataclass

from app.services.status_block import MediaType


@dataclass(frozen=True, slots=True)
class TapeSpec:
width_mm: int
media_type: MediaType
print_area_pins: int
print_area_dots: int
bytes_per_raster: int
min_length_mm: float
max_length_mm: int
cutter_min_length_mm: float
132 changes: 132 additions & 0 deletions backend/app/printer_models/pt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Tape data for Brother PT-Series printers (180 DPI, 128-pin head).

Source: Brother Raster Command Reference (PT-E550W / PT-P710BT / PT-P750W) v1.02.
The numbers here come straight from the manufacturer's printable-area tables.
"""

from __future__ import annotations

from app.models.tape import TapeSpec
from app.services.status_block import MediaType

# TZe laminated tapes. The non-laminated TZe-N* variants share the same
# print-area geometry — only the material differs — so we reuse this list
# for MediaType.NON_LAMINATED in the registry.
PT_TZE_TAPES: tuple[TapeSpec, ...] = (
TapeSpec(
width_mm=4,
media_type=MediaType.LAMINATED,
print_area_pins=24,
print_area_dots=24,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=1000,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=6,
media_type=MediaType.LAMINATED,
print_area_pins=32,
print_area_dots=32,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=1000,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=9,
media_type=MediaType.LAMINATED,
print_area_pins=50,
print_area_dots=50,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=1000,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=12,
media_type=MediaType.LAMINATED,
print_area_pins=70,
print_area_dots=70,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=1000,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=18,
media_type=MediaType.LAMINATED,
print_area_pins=112,
print_area_dots=112,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=1000,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=24,
media_type=MediaType.LAMINATED,
print_area_pins=128,
print_area_dots=128,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=1000,
cutter_min_length_mm=24.5,
),
)

# Heat-shrink tubing 2:1 (status-block media-type byte = 0x11).
# Widths are nominal — actual tape is slightly narrower (e.g. 12mm HS = ~11.7mm).
# Pin counts are the Brother-published values.
PT_HS_2_1_TAPES: tuple[TapeSpec, ...] = (
TapeSpec(
width_mm=6,
media_type=MediaType.HEAT_SHRINK_2_1,
print_area_pins=28,
print_area_dots=28,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=500,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=9,
media_type=MediaType.HEAT_SHRINK_2_1,
print_area_pins=48,
print_area_dots=48,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=500,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=12,
media_type=MediaType.HEAT_SHRINK_2_1,
print_area_pins=66,
print_area_dots=66,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=500,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=18,
media_type=MediaType.HEAT_SHRINK_2_1,
print_area_pins=106,
print_area_dots=106,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=500,
cutter_min_length_mm=24.5,
),
TapeSpec(
width_mm=24,
media_type=MediaType.HEAT_SHRINK_2_1,
print_area_pins=128,
print_area_dots=128,
bytes_per_raster=16,
min_length_mm=4.4,
max_length_mm=500,
cutter_min_length_mm=24.5,
),
)
47 changes: 47 additions & 0 deletions backend/app/services/tape_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Lookup of Brother tape specifications by physical width + media type.

QL-Series die-cut labels will be added once their byte-sequence reproducible
fixtures are in place — see docs/decisions/0004-plugin-architecture-for-printer-models.md.
"""

from __future__ import annotations

import dataclasses

from app.models.tape import TapeSpec
from app.printer_models.pt import PT_HS_2_1_TAPES, PT_TZE_TAPES
from app.services.status_block import MediaType


class UnknownTapeError(Exception):
"""Raised when a (width_mm, media_type) combination has no registered spec."""


class TapeRegistry:
@staticmethod
def lookup_pt(width_mm: int, media_type: MediaType) -> TapeSpec:
"""Return the PT-Series tape spec for the given width + media type.

Non-laminated TZe-N tapes share TZe laminated dimensions, so both
MediaType.LAMINATED and MediaType.NON_LAMINATED resolve to the same
PT_TZE_TAPES table. The returned spec always carries the queried
media_type so callers see the media_type they asked for.
"""
if media_type in (MediaType.LAMINATED, MediaType.NON_LAMINATED):
table = PT_TZE_TAPES
elif media_type == MediaType.HEAT_SHRINK_2_1:
table = PT_HS_2_1_TAPES
Comment on lines +23 to +33
else:
# MediaType.HEAT_SHRINK_3_1 and QL-Series types are intentionally
# unregistered — add a new table and branch here when supported.
raise UnknownTapeError(f"No PT-Series tape table for media_type={media_type.name}")
Comment on lines +22 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The lookup_pt method contains hardcoded logic mapping MediaType to PT-specific tables. Per Rule 17 of the Repository Style Guide, this model-specific logic should reside in the PT plugin. Consider refactoring TapeRegistry to be a generic dispatcher that delegates to series-specific plugins, keeping the core registry decoupled from specific hardware details.

References
  1. Model-specific code must be located within the appropriate plugin file in app/printer_models/. (link)


for spec in table:
if spec.width_mm == width_mm:
if spec.media_type != media_type:
return dataclasses.replace(spec, media_type=media_type)
return spec
Comment on lines +39 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When media_type is MediaType.NON_LAMINATED, the registry currently returns a TapeSpec where media_type is LAMINATED. This identity mismatch can be confusing for downstream consumers. Use dataclasses.replace to ensure the returned object reflects the requested media_type.

Suggested change
for spec in table:
if spec.width_mm == width_mm:
return spec
for spec in table:
if spec.width_mm == width_mm:
return dataclasses.replace(spec, media_type=media_type) if spec.media_type != media_type else spec


raise UnknownTapeError(
f"No PT-Series tape spec for width={width_mm}mm, media={media_type.name}"
)
37 changes: 37 additions & 0 deletions backend/tests/unit/services/test_tape_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest
from app.services.status_block import MediaType
from app.services.tape_registry import TapeRegistry, UnknownTapeError


def test_lookup_pt_series_12mm_laminated() -> None:
spec = TapeRegistry.lookup_pt(width_mm=12, media_type=MediaType.LAMINATED)
assert spec.width_mm == 12
assert spec.print_area_pins == 70
assert spec.print_area_dots == 70
assert spec.bytes_per_raster == 16
assert spec.min_length_mm == pytest.approx(4.4)
assert spec.max_length_mm == 1000
assert spec.cutter_min_length_mm == pytest.approx(24.5)


def test_lookup_pt_series_24mm() -> None:
spec = TapeRegistry.lookup_pt(width_mm=24, media_type=MediaType.LAMINATED)
assert spec.print_area_pins == 128


def test_lookup_pt_unknown_width_raises() -> None:
with pytest.raises(UnknownTapeError):
TapeRegistry.lookup_pt(width_mm=15, media_type=MediaType.LAMINATED)


def test_lookup_pt_heat_shrink_2_1() -> None:
spec = TapeRegistry.lookup_pt(width_mm=12, media_type=MediaType.HEAT_SHRINK_2_1)
# 12mm HS 2:1 (~11.7mm tape) — 66 print pins per Brother spec
assert spec.print_area_pins == 66


def test_lookup_pt_series_non_laminated_uses_tze_table() -> None:
"""NON_LAMINATED resolves via the TZe table; returned spec carries the queried media_type."""
spec = TapeRegistry.lookup_pt(width_mm=12, media_type=MediaType.NON_LAMINATED)
assert spec.print_area_pins == 70 # same geometry as 12mm laminated TZe
assert spec.media_type == MediaType.NON_LAMINATED # spec reflects what was requested