Skip to content
13 changes: 7 additions & 6 deletions src/labelle/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# this notice are preserved.
# === END LICENSE STATEMENT ===
import logging
import math
from pathlib import Path
from typing import List, NoReturn, Optional

Expand Down Expand Up @@ -53,12 +54,12 @@
LOG = logging.getLogger(__name__)


def mm_to_payload_px(labeler: DymoLabeler, mm: float, margin: float) -> float:
def mm_to_payload_px(labeler: DymoLabeler, mm: float, margin: int) -> int:
"""Convert a length in mm to a number of pixels of payload.

Margin is subtracted from each side.
"""
return max(0, (mm * labeler.pixels_per_mm()) - margin * 2)
return max(0, math.ceil(mm * labeler.pixels_per_mm()) - margin * 2)


def version_callback(value: bool) -> None:
Expand Down Expand Up @@ -96,7 +97,7 @@ def list_devices() -> NoReturn:
console = Console()
headers = ["Manufacturer", "Product", "Serial Number", "USB"]
table = Table(*headers, show_header=True)
for device in device_manager.devices:
for device in device_manager.get_devices_from_last_scan():
table.add_row(
device.manufacturer, device.product, device.serial_number, device.usb_id
)
Expand Down Expand Up @@ -220,7 +221,7 @@ def default(
Optional[Path], typer.Option(help="Picture", rich_help_panel="Elements")
] = None,
margin_px: Annotated[
float,
int,
typer.Option(
help="Horizontal margins [px]", rich_help_panel="Label Dimensions"
),
Expand Down Expand Up @@ -535,7 +536,7 @@ def default(
render_engine=render_engine,
justify=justify,
visible_horizontal_margin_px=margin_px,
labeler_margin_px=dymo_labeler.labeler_margin_px,
labeler_margin_px=dymo_labeler.get_labeler_margin_px(),
max_width_px=max_payload_len_px,
min_width_px=min_payload_len_px,
)
Expand All @@ -547,7 +548,7 @@ def default(
dymo_labeler=dymo_labeler,
justify=justify,
visible_horizontal_margin_px=margin_px,
labeler_margin_px=dymo_labeler.labeler_margin_px,
labeler_margin_px=dymo_labeler.get_labeler_margin_px(),
max_width_px=max_payload_len_px,
min_width_px=min_payload_len_px,
)
Expand Down
7 changes: 3 additions & 4 deletions src/labelle/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,9 @@ def _on_settings_changed(self, settings: Settings) -> None:
justify=settings.justify,
)

is_ready = self._dymo_labeler.is_ready
self._settings_toolbar.setEnabled(is_ready)
self._label_list.setEnabled(is_ready)
self._render_widget.setEnabled(is_ready)
self._settings_toolbar.setEnabled(True)
self._label_list.setEnabled(True)
self._render_widget.setEnabled(self._dymo_labeler.device is not None)

def _update_preview_render(self, preview_bitmap: Image.Image) -> None:
self._render.update_preview_render(preview_bitmap)
Expand Down
1 change: 1 addition & 0 deletions src/labelle/gui/q_device_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def _init_connections(self) -> None:
self._devices.currentIndexChanged.connect(self._index_changed)

def repopulate(self) -> None:
"""Update the device selector."""
old_hashes = {device.hash for device in self.device_manager.devices}
self._devices.clear()
for idx, device in enumerate(self.device_manager.devices):
Expand Down
8 changes: 6 additions & 2 deletions src/labelle/gui/q_labels_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,14 @@ def _payload_render_engine(self):
def render_preview(self) -> None:
assert self.dymo_labeler is not None
assert self.render_context is not None
assert self.h_margin_mm is not None
assert self.min_label_width_mm is not None
render_engine = PrintPreviewRenderEngine(
render_engine=self._payload_render_engine,
dymo_labeler=self.dymo_labeler,
justify=self.justify,
visible_horizontal_margin_px=self.dymo_labeler.mm_to_px(self.h_margin_mm),
labeler_margin_px=self.dymo_labeler.labeler_margin_px,
labeler_margin_px=self.dymo_labeler.get_labeler_margin_px(),
max_width_px=None,
min_width_px=self.dymo_labeler.mm_to_px(self.min_label_width_mm),
)
Expand All @@ -176,11 +178,13 @@ def render_preview(self) -> None:
def render_print(self) -> None:
assert self.dymo_labeler is not None
assert self.render_context is not None
assert self.h_margin_mm is not None
assert self.min_label_width_mm is not None
render_engine = PrintPayloadRenderEngine(
render_engine=self._payload_render_engine,
justify=self.justify,
visible_horizontal_margin_px=self.dymo_labeler.mm_to_px(self.h_margin_mm),
labeler_margin_px=self.dymo_labeler.labeler_margin_px,
labeler_margin_px=self.dymo_labeler.get_labeler_margin_px(),
max_width_px=None,
min_width_px=self.dymo_labeler.mm_to_px(self.min_label_width_mm),
)
Expand Down
20 changes: 16 additions & 4 deletions src/labelle/lib/devices/device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,27 @@ class DeviceManagerNoDevices(DeviceManagerError):


class DeviceManager:
"""Incrementally maintain a list of connected USB devices.

The list begins empty. It is updated whenever scan() is called. The list is
accessible via get_devices_from_last_scan().
"""

_devices: dict[str, UsbDevice]

def __init__(self) -> None:
self._devices = {}

def scan(self) -> bool:
"""Check for devices being connected or disconnected.

Returns true if the list of devices has changed.
"""
prev = self._devices
try:
cur = {dev.hash: dev for dev in UsbDevice.supported_devices() if dev.hash}
cur = {
dev.hash: dev for dev in UsbDevice.find_supported_devices() if dev.hash
}
except POSSIBLE_USB_ERRORS as e:
self._devices.clear()
raise DeviceManagerError(f"Failed scanning devices: {e}") from e
Expand All @@ -44,15 +56,16 @@ def scan(self) -> bool:
cur_set = set(cur)

for dev in prev_set - cur_set:
# Pop removed devices
self._devices.pop(dev)
for dev in cur_set - prev_set:
# Add new devices
self._devices[dev] = cur[dev]

changed = prev_set != cur_set
return changed

@property
def devices(self) -> list[UsbDevice]:
def get_devices_from_last_scan(self) -> list[UsbDevice]:
try:
return sorted(self._devices.values(), key=lambda dev: dev.hash)
except POSSIBLE_USB_ERRORS:
Expand Down Expand Up @@ -94,7 +107,6 @@ def get_device_config_by_id(product_id: int) -> DeviceConfig:
:param idValue: USB ID value
:return: Device config, None if not found
"""
#
for device in SUPPORTED_PRODUCTS:
if device.matches_device_id(product_id):
return device
Expand Down
51 changes: 24 additions & 27 deletions src/labelle/lib/devices/dymo_labeler.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from labelle.lib.devices.device_config import DeviceConfig
from labelle.lib.devices.device_manager import get_device_config_by_id
from labelle.lib.devices.usb_device import UsbDevice, UsbDeviceError
from labelle.lib.margins import LabelMarginsPx

LOG = logging.getLogger(__name__)
POSSIBLE_USB_ERRORS = (UsbDeviceError, NoBackendError, USBError)
Expand Down Expand Up @@ -258,8 +259,12 @@ def __init__(
raise ValueError("No device config")

if tape_size_mm is None:
# Select highest supported tape size as default, if not set
tape_size_mm = max(self.device_config.supported_tape_sizes_mm)
if device is None:
# If there's no device, then use the most common tape size
tape_size_mm = 12
else:
# Select highest supported tape size as default, if not set
tape_size_mm = max(self.device_config.supported_tape_sizes_mm)

# Check if selected tape size is supported
if tape_size_mm not in self.device_config.supported_tape_sizes_mm:
Expand All @@ -271,16 +276,7 @@ def __init__(

def get_label_height_px(self):
"""Get the (usable) tape height in pixels."""
return self.tape_print_properties.usable_tape_height_px

@property
def _functions(self) -> DymoLabelerFunctions:
assert self._device is not None
return DymoLabelerFunctions(
devout=self._device.devout,
devin=self._device.devin,
synwait=64,
)
return self.compute_tape_print_properties().usable_tape_height_px

@property
def minimum_horizontal_margin_mm(self):
Expand All @@ -290,15 +286,14 @@ def minimum_horizontal_margin_mm(self):
self.device_config.distance_between_print_head_and_cutter_px
)

@property
def labeler_margin_px(self) -> tuple[float, float]:
return (
self.device_config.distance_between_print_head_and_cutter_px,
self.tape_print_properties.top_margin_px,
def get_labeler_margin_px(self) -> LabelMarginsPx:
tape_print_properties = self.compute_tape_print_properties()
return LabelMarginsPx(
horizontal=self.device_config.distance_between_print_head_and_cutter_px,
vertical=tape_print_properties.top_margin_px,
)

@property
def tape_print_properties(self) -> TapePrintProperties:
def compute_tape_print_properties(self) -> TapePrintProperties:
# Check if selected tape size supported
if self.tape_size_mm not in self.device_config.supported_tape_sizes_mm:
raise ValueError(
Expand Down Expand Up @@ -374,25 +369,21 @@ def set_device(self, device: UsbDevice | None):
LOG.error(e)
self._device = device

@property
def is_ready(self) -> bool:
return self.device is not None

def pixels_per_mm(self) -> float:
# Calculate the pixels per mm for this printer
# Example: printhead of 128 Pixels, distributed over 18 mm of active area.
# Makes 7.11 pixels/mm
return self.device_config.print_head_px / self.device_config.print_head_mm

def px_to_mm(self, px) -> float:
def px_to_mm(self, px: int) -> float:
"""Convert pixels to millimeters for the current printer."""
mm = px / self.pixels_per_mm()
# Round up to nearest 0.1mm
return math.ceil(mm * 10) / 10

def mm_to_px(self, mm) -> float:
def mm_to_px(self, mm: float) -> int:
"""Convert millimeters to pixels for the current printer."""
return mm * self.pixels_per_mm()
return math.ceil(mm * self.pixels_per_mm())

def print(
self,
Expand Down Expand Up @@ -431,7 +422,13 @@ def print(

try:
LOG.debug("Printing label..")
self._functions.print_label(label_matrix)
assert self._device is not None
functions = DymoLabelerFunctions(
devout=self._device.devout,
devin=self._device.devin,
synwait=64,
)
functions.print_label(label_matrix)
LOG.debug("Done printing.")
if self._device is not None:
self._device.dispose()
Expand Down
18 changes: 7 additions & 11 deletions src/labelle/lib/devices/online_device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@


class OnlineDeviceManager(QWidget):
_last_scan_error: DeviceManagerError | None
last_scan_error: DeviceManagerError | None
_status_time: QTimer
_device_manager: DeviceManager
last_scan_error_changed_signal = QtCore.pyqtSignal(
Expand All @@ -26,20 +26,20 @@ class OnlineDeviceManager(QWidget):
def __init__(self) -> None:
super().__init__()
self._device_manager = DeviceManager()
self._last_scan_error = None
self.last_scan_error = None
self._init_timers()

def _refresh_devices(self) -> None:
prev = self._last_scan_error
prev = self.last_scan_error
try:
changed = self._device_manager.scan()
self._last_scan_error = None
self.last_scan_error = None
if changed:
self.devices_changed_signal.emit()
except DeviceManagerError as e:
self._last_scan_error = e
self.last_scan_error = e

if str(prev) != str(self._last_scan_error):
if str(prev) != str(self.last_scan_error):
self.devices_changed_signal.emit()
self.last_scan_error_changed_signal.emit()

Expand All @@ -49,10 +49,6 @@ def _init_timers(self) -> None:
self._status_time.start(2000)
self._refresh_devices()

@property
def last_scan_error(self) -> DeviceManagerError | None:
return self._last_scan_error

@property
def devices(self) -> list[UsbDevice]:
return self._device_manager.devices
return self._device_manager.get_devices_from_last_scan()
2 changes: 1 addition & 1 deletion src/labelle/lib/devices/usb_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def is_supported(self) -> bool:
return False

@staticmethod
def supported_devices() -> set[UsbDevice]:
def find_supported_devices() -> set[UsbDevice]:
return {
UsbDevice(dev)
for dev in usb.core.find(
Expand Down
6 changes: 6 additions & 0 deletions src/labelle/lib/margins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from typing import NamedTuple


class LabelMarginsPx(NamedTuple):
horizontal: int
vertical: int
Loading
Loading