python-escpos/src/escpos/cli.py

644 lines
19 KiB
Python

#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
"""CLI.
This module acts as a command line interface for python-escpos. It mirrors
closely the available ESCPOS commands while adding a couple extra ones for convenience.
It requires you to have a configuration file. See documentation for details.
"""
import argparse
import platform
from typing import Any, Dict, List
try:
import argcomplete
except ImportError:
# this CLI works nevertheless without argcomplete
pass # noqa
import sys
from . import config, escpos
from . import printer as escpos_printer_module
from . import version
# Must be defined before it's used in DEMO_FUNCTIONS
def str_to_bool(string: str) -> bool:
"""Convert string to bool.
Used as a type in argparse so that we get back a proper
bool instead of always True.
"""
return string.lower() in ("y", "yes", "1", "true")
# A list of functions that work better with a newline to be sent after them.
REQUIRES_NEWLINE = ("qr", "barcode", "text", "block_text")
# Used in demo method
# Key: The name of escpos function and the argument passed on the CLI. Some
# manual translation is done in the case of barcodes_a -> barcode.
# Value: A list of dictionaries to pass to the escpos function as arguments.
DEMO_FUNCTIONS = {
"text": [
{
"txt": "Hello, World!\n",
}
],
"qr": [
{"content": "This tests a QR code"},
{"content": "https://en.wikipedia.org/"},
],
"barcodes_a": [
{"bc": "UPC-A", "code": "13243546576"},
{"bc": "UPC-E", "code": "132435"},
{"bc": "EAN13", "code": "4006381333931"},
{"bc": "EAN8", "code": "1324354"},
{"bc": "CODE39", "code": "TEST"},
{"bc": "ITF", "code": "55867492279103"},
{"bc": "NW7", "code": "A00000000A"},
],
"barcodes_b": [
{"bc": "UPC-A", "code": "13243546576", "function_type": "B"},
{"bc": "UPC-E", "code": "132435", "function_type": "B"},
{"bc": "EAN13", "code": "4006381333931", "function_type": "B"},
{"bc": "EAN8", "code": "1324354", "function_type": "B"},
{"bc": "CODE39", "code": "TEST", "function_type": "B"},
{"bc": "ITF", "code": "55867492279103", "function_type": "B"},
{"bc": "NW7", "code": "A00000000A", "function_type": "B"},
{"bc": "CODE93", "code": "A00000000A", "function_type": "B"},
{"bc": "CODE93", "code": "1324354657687", "function_type": "B"},
{"bc": "CODE128A", "code": "TEST", "function_type": "B"},
{"bc": "CODE128B", "code": "TEST", "function_type": "B"},
{"bc": "CODE128C", "code": "TEST", "function_type": "B"},
{"bc": "GS1-128", "code": "00123456780000000001", "function_type": "B"},
{
"bc": "GS1 DataBar Omnidirectional",
"code": "0000000000000",
"function_type": "B",
},
{"bc": "GS1 DataBar Truncated", "code": "0000000000000", "function_type": "B"},
{"bc": "GS1 DataBar Limited", "code": "0000000000000", "function_type": "B"},
{"bc": "GS1 DataBar Expanded", "code": "00AAAAAAA", "function_type": "B"},
],
}
# Used to build the CLI
# A list of dictionaries. Each dict is a CLI argument.
# Keys:
# parser: A dict of args for command_parsers.add_parser
# defaults: A dict of args for subparser.set_defaults
# arguments: A list of dicts of args for subparser.add_argument
ESCPOS_COMMANDS: List[Dict[str, Any]] = [
{
"parser": {
"name": "qr",
"help": "Print a QR code",
},
"defaults": {
"func": "qr",
},
"arguments": [
{
"option_strings": ("--content",),
"help": "Text to print as a qr code",
"required": True,
},
{
"option_strings": ("--size",),
"help": "QR code size (1-16) [default:3]",
"required": False,
"type": int,
},
],
},
{
"parser": {
"name": "barcode",
"help": "Print a barcode",
},
"defaults": {
"func": "barcode",
},
"arguments": [
{
"option_strings": ("--code",),
"help": "Barcode data to print",
"required": True,
},
{
"option_strings": ("--bc",),
"help": "Barcode format",
"required": True,
},
{
"option_strings": ("--height",),
"help": "Barcode height in px",
"type": int,
},
{
"option_strings": ("--width",),
"help": "Barcode width",
"type": int,
},
{
"option_strings": ("--pos",),
"help": "Label position",
"choices": ["BELOW", "ABOVE", "BOTH", "OFF"],
},
{
"option_strings": ("--font",),
"help": "Label font",
"choices": ["A", "B"],
},
{
"option_strings": ("--align_ct",),
"help": "Align barcode center",
"type": str_to_bool,
},
{
"option_strings": ("--function_type",),
"help": "ESCPOS function type",
"choices": ["A", "B"],
},
{
"option_strings": ("--force_software",),
"help": "Force render and print barcode as an image",
"choices": ["graphics", "bitImageColumn", "bitImageRaster"],
},
],
},
{
"parser": {
"name": "text",
"help": "Print plain text",
},
"defaults": {
"func": "text",
},
"arguments": [
{
"option_strings": ("--txt",),
"help": "Plain text to print",
"required": True,
}
],
},
{
"parser": {
"name": "block_text",
"help": "Print wrapped text",
},
"defaults": {
"func": "block_text",
},
"arguments": [
{
"option_strings": ("--txt",),
"help": "block_text to print",
"required": True,
},
{
"option_strings": ("--columns",),
"help": "Number of columns",
"type": int,
},
],
},
{
"parser": {
"name": "cut",
"help": "Cut the paper",
},
"defaults": {
"func": "cut",
},
"arguments": [
{
"option_strings": ("--mode",),
"help": "Type of cut",
"choices": ["FULL", "PART"],
},
],
},
{
"parser": {
"name": "cashdraw",
"help": "Kick the cash drawer",
},
"defaults": {
"func": "cashdraw",
},
"arguments": [
{
"option_strings": ("--pin",),
"help": "Which PIN to kick",
"choices": [2, 5],
},
],
},
{
"parser": {
"name": "image",
"help": "Print an image",
},
"defaults": {
"func": "image",
},
"arguments": [
{
"option_strings": ("--img_source",),
"help": "Path to image",
"required": True,
},
{
"option_strings": ("--impl",),
"help": "Implementation to use",
"choices": ["bitImageRaster", "bitImageColumn", "graphics"],
},
{
"option_strings": ("--high_density_horizontal",),
"help": "Image density (horizontal)",
"type": str_to_bool,
},
{
"option_strings": ("--high_density_vertical",),
"help": "Image density (vertical)",
"type": str_to_bool,
},
],
},
{
"parser": {
"name": "fullimage",
"help": "Print a fullimage",
},
"defaults": {
"func": "fullimage",
},
"arguments": [
{
"option_strings": ("--img",),
"help": "Path to img",
"required": True,
},
{
"option_strings": ("--max_height",),
"help": "Max height of image in px",
"type": int,
},
{
"option_strings": ("--width",),
"help": "Max width of image in px",
"type": int,
},
{
"option_strings": ("--histeq",),
"help": "Equalize the histogram",
"type": str_to_bool,
},
{
"option_strings": ("--bandsize",),
"help": "Size of bands to divide into when printing",
"type": int,
},
],
},
{
"parser": {
"name": "charcode",
"help": "Set character code table",
},
"defaults": {
"func": "charcode",
},
"arguments": [
{
"option_strings": ("--code",),
"help": "Character code",
"required": True,
},
],
},
{
"parser": {
"name": "set",
"help": "Set text properties",
},
"defaults": {
"func": "set",
},
"arguments": [
{
"option_strings": ("--align",),
"help": "Horizontal alignment",
"choices": ["left", "center", "right"],
},
{
"option_strings": ("--font",),
"help": "Font choice",
"choices": ["left", "center", "right"],
},
{
"option_strings": ("--text_type",),
"help": "Text properties",
"choices": ["B", "U", "U2", "BU", "BU2", "NORMAL"],
},
{
"option_strings": ("--width",),
"help": "Width multiplier",
"type": int,
},
{
"option_strings": ("--height",),
"help": "Height multiplier",
"type": int,
},
{
"option_strings": ("--density",),
"help": "Print density",
"type": int,
},
{
"option_strings": ("--invert",),
"help": "White on black printing",
"type": str_to_bool,
},
{
"option_strings": ("--smooth",),
"help": "Text smoothing. Effective on >: 4x4 text",
"type": str_to_bool,
},
{
"option_strings": ("--flip",),
"help": "Text smoothing. Effective on >: 4x4 text",
"type": str_to_bool,
},
],
},
{
"parser": {
"name": "hw",
"help": "Hardware operations",
},
"defaults": {
"func": "hw",
},
"arguments": [
{
"option_strings": ("--hw",),
"help": "Operation",
"choices": ["INIT", "SELECT", "RESET"],
"required": True,
},
],
},
{
"parser": {
"name": "control",
"help": "Control sequences",
},
"defaults": {
"func": "control",
},
"arguments": [
{
"option_strings": ("--ctl",),
"help": "Control sequence",
"choices": ["LF", "FF", "CR", "HT", "VT"],
"required": True,
},
{
"option_strings": ("--pos",),
"help": "Horizontal tab position (1-4)",
"type": int,
},
],
},
{
"parser": {
"name": "panel_buttons",
"help": "Controls panel buttons",
},
"defaults": {
"func": "panel_buttons",
},
"arguments": [
{
"option_strings": ("--enable",),
"help": "Feed button enabled",
"type": str_to_bool,
"required": True,
},
],
},
{
"parser": {
"name": "raw",
"help": "Raw data",
},
"defaults": {
"func": "_raw",
},
"arguments": [
{
"option_strings": ("--msg",),
"help": "Raw data to send",
"required": True,
},
],
},
]
def print_extended_information() -> None:
"""Print diagnostic information for bug reports."""
print(f"* python-escpos version: `{version.version}`")
print(
f"* python version: `{platform.python_implementation()} v{platform.python_version()}`"
)
print(f"* platform: `{platform.platform()}`")
print(
f"* printer driver `USB` is usable: `{escpos_printer_module.Usb.is_usable()}`"
)
print(
f"* printer driver `File` is usable: `{escpos_printer_module.File.is_usable()}`"
)
print(
f"* printer driver `Network` is usable: `{escpos_printer_module.Network.is_usable()}`"
)
print(
f"* printer driver `Serial` is usable: `{escpos_printer_module.Serial.is_usable()}`"
)
print(f"* printer driver `LP` is usable: `{escpos_printer_module.LP.is_usable()}`")
print(
f"* printer driver `Dummy` is usable: `{escpos_printer_module.Dummy.is_usable()}`"
)
print(
f"* printer driver `CupsPrinter` is usable: `{escpos_printer_module.CupsPrinter.is_usable()}`"
)
print(
f"* printer driver `Win32Raw` is usable: `{escpos_printer_module.Win32Raw.is_usable()}`"
)
def generate_parser() -> argparse.ArgumentParser:
"""Generate an argparse parser."""
parser = argparse.ArgumentParser(
description="CLI for python-escpos",
epilog="Printer configuration is defined in the python-escpos config"
"file. See documentation for details.",
)
parser.register("type", "bool", str_to_bool)
# Allow config file location to be passed
parser.add_argument(
"-c",
"--config",
help="Alternate path to the configuration file",
)
# Everything interesting runs off of a subparser so we can use the format
# cli [subparser] -args
command_subparsers = parser.add_subparsers(
title="ESCPOS Command",
dest="parser",
)
# fix inconsistencies in the behaviour of some versions of argparse
command_subparsers.required = False # force 'required' testing
# Build the ESCPOS command arguments
for command in ESCPOS_COMMANDS:
parser_command = command_subparsers.add_parser(**command["parser"])
parser_command.set_defaults(**command["defaults"])
for argument in command["arguments"]:
option_strings = argument.pop("option_strings")
parser_command.add_argument(*option_strings, **argument)
# Build any custom arguments
parser_command_demo = command_subparsers.add_parser(
"demo", help="Demonstrates various functions"
)
parser_command_demo.set_defaults(func="demo")
demo_group = parser_command_demo.add_mutually_exclusive_group()
demo_group.add_argument(
"--barcodes-a",
help="Print demo barcodes for function type A",
action="store_true",
)
demo_group.add_argument(
"--barcodes-b",
help="Print demo barcodes for function type B",
action="store_true",
)
demo_group.add_argument(
"--qr",
help="Print some demo QR codes",
action="store_true",
)
demo_group.add_argument(
"--text",
help="Print some demo text",
action="store_true",
)
parser_command_version = command_subparsers.add_parser(
"version", help="Print the version information of python-escpos"
)
parser_command_version.set_defaults(version=True)
parser_command_version_extended = command_subparsers.add_parser(
"version_extended",
help="Print the extended version information of python-escpos (for bug reports)",
)
parser_command_version_extended.set_defaults(version_extended=True)
return parser
def main() -> None:
"""Handle main entry point of CLI script.
Handles loading of configuration and creating and processing of command
line arguments. Called when run from a CLI.
"""
parser = generate_parser()
# hook in argcomplete
if "argcomplete" in globals():
argcomplete.autocomplete(parser)
# Get only arguments actually passed
args_dict = vars(parser.parse_args())
if not args_dict:
parser.print_help()
sys.exit()
command_arguments = dict([k, v] for k, v in args_dict.items() if v is not None)
# If version should be printed, do this, then exit
print_version = command_arguments.pop("version", None)
if print_version:
print(version.version)
sys.exit()
print_version_extended = command_arguments.pop("version_extended", None)
if print_version_extended:
print_extended_information()
sys.exit()
# If there was a config path passed, grab it
config_path = command_arguments.pop("config", None)
# Load the configuration and defined printer
saved_config = config.Config()
saved_config.load(config_path)
printer = saved_config.printer()
if not printer:
raise Exception("No printers loaded from config")
target_command = command_arguments.pop("func")
# remove helper-argument 'parser' from dict
command_arguments.pop("parser", None)
if hasattr(printer, target_command):
# print command with args
getattr(printer, target_command)(**command_arguments)
if target_command in REQUIRES_NEWLINE:
printer.text("\n")
else:
command_arguments["printer"] = printer
globals()[target_command](**command_arguments)
def demo(printer: escpos.Escpos, **kwargs) -> None:
"""Print demos.
Called when CLI is passed `demo`. This function
uses the DEMO_FUNCTIONS dictionary.
:param printer: A printer from escpos.printer
:param kwargs: A dict with a key for each function you want to test. It's
in this format since it usually comes from argparse.
"""
for demo_choice in kwargs.keys():
command = getattr(
printer,
demo_choice.replace("barcodes_a", "barcode").replace(
"barcodes_b", "barcode"
),
)
for params in DEMO_FUNCTIONS[demo_choice]:
command(**params)
printer.cut()
if __name__ == "__main__":
main()