diff --git a/Makefile b/Makefile index 92bf406..b4301cd 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PYTHON ?= python3 NPM ?= npm PHP ?= php -PY_FILES := kassa.py $(shell find plugins tests -type f -name '*.py' | sort) +PY_FILES := kassa.py load_printer_logo.py $(shell find plugins tests -type f -name '*.py' | sort) PHP_FILES := $(shell find www -type f -name '*.php' | sort) .PHONY: test python-test js-test php-test venv pip-compile pip-sync pip-sync-dev lint python-lint js-lint php-lint check-types fix check @@ -42,7 +42,7 @@ pip-sync-dev: ## synchronizes the .venv with the state of requirements.txt lint: python-lint js-lint php-lint ## Do basic linting python-lint: venv - @. .venv/bin/activate && ${env} ${PYTHON} -m pylint --persistent=no kassa.py plugins + @. .venv/bin/activate && ${env} ${PYTHON} -m pylint --persistent=no kassa.py load_printer_logo.py plugins @. .venv/bin/activate; for file in ${PY_FILES}; do ${env} ${PYTHON} -m black --check --quiet --workers 1 "$$file" || exit $$?; done node_modules/.package-lock.json: package.json package-lock.json diff --git a/README.md b/README.md index fcb17aa..8636feb 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,35 @@ remain supported. The adapter has no unique serial number, so this rule assumes only one `067b:2303` adapter is connected. With several identical adapters, narrow the rule to the appropriate USB port before installing it. +### Store the receipt logo in the Epson + +Receipts use NV logo 1 already stored in the printer (`FS p 1 3`, the existing +`LOGO` constant). Upload a replacement once with: + +```sh +# Stop the kassa backend before uploading. +.venv/bin/python load_printer_logo.py images/hack42.png --write --test-print +``` + +The script reads `pos.serial` from `config.yaml` (default `/dev/ttyKASSA`, +19200 baud). Use `--config`, `--port`, or `--baudrate` to override it. Without +`--write` it only prepares the image; `--output logo.bin` exports the `FS q` +command for offline inspection. `--test-print` prints the stored logo after +uploading. Restart the backend afterwards. + +The default stored width is 192 dots; `LOGO` prints at twice the stored width +and height, so the printed width is at most 384 dots. `--width` changes the +stored width. Images retain their aspect ratio; transparent pixels and padding +become white. Stored logos persist after power-off; receipt generation does not +upload the image again. + +Epson's [FS q command](https://download4.epson.biz/sec_pubs/pos/reference_en/escpos/fs_lq.html) +replaces **all** existing NV logos/graphics and resets the printer. Run the +script only when changing the logo, with the backend stopped, and keep the +printer powered on until it finishes. A successful serial write cannot confirm +that the printer accepted the logo; verify the test print. The printer must +support the same legacy NV commands already used for the receipt header. + ### data Data is stored in 'data'. In production it is also committed to git, but for diff --git a/load_printer_logo.py b/load_printer_logo.py new file mode 100644 index 0000000..7c4154d --- /dev/null +++ b/load_printer_logo.py @@ -0,0 +1,103 @@ +"""Upload NV logo 1 for the existing Epson FS p receipt header.""" + +import argparse +from pathlib import Path +import struct +import time + +from PIL import Image, ImageOps +import serial + +from config import load_config +from plugins.POS import CENTER, CUT, FEED, LOGO, PRINTER + + +def encode_logo(path, width=192): + """Encode FS q column data; LOGO prints this at twice the stored size.""" + if not 8 <= width <= 512: + raise ValueError("Stored width must be between 8 and 512 dots") + with Image.open(path) as source: + image = source.convert("RGBA") + image.thumbnail((width, 512), Image.Resampling.LANCZOS) + white = Image.new("RGBA", image.size, "white") + gray = Image.alpha_composite(white, image).convert("L") + ink = ImageOps.invert(gray.point(lambda value: 255 if value >= 128 else 0)) + x_bytes, y_bytes = (image.width + 7) // 8, (image.height + 7) // 8 + padded = Image.new("1", (x_bytes * 8, y_bytes * 8), 0) + padded.paste(ink.convert("1"), (0, 0)) + # FS q packs each column from top to bottom, most significant bit first. + columns = padded.transpose(Image.Transpose.TRANSPOSE).tobytes() + return b"\x1cq\x01" + struct.pack("