paperang-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paperang_cli/__init__.py +3 -0
- paperang_cli/__main__.py +5 -0
- paperang_cli/cli.py +82 -0
- paperang_cli/commands/__init__.py +1 -0
- paperang_cli/commands/battery.py +28 -0
- paperang_cli/commands/config_cmd.py +80 -0
- paperang_cli/commands/discover.py +23 -0
- paperang_cli/commands/mac.py +28 -0
- paperang_cli/commands/print_cmd.py +260 -0
- paperang_cli/commands/probe.py +57 -0
- paperang_cli/commands/status.py +33 -0
- paperang_cli/config.py +109 -0
- paperang_cli/drivers/__init__.py +1 -0
- paperang_cli/drivers/base.py +86 -0
- paperang_cli/drivers/paperang_p1.py +383 -0
- paperang_cli/drivers/registry.py +24 -0
- paperang_cli/errors.py +30 -0
- paperang_cli/models.py +101 -0
- paperang_cli/output.py +52 -0
- paperang_cli/protocol/const.py +62 -0
- paperang_cli/protocol/hardware_bleak.py +425 -0
- paperang_cli/protocol/image_data.py +234 -0
- paperang_cli/render.py +207 -0
- paperang_cli-0.1.0.dist-info/METADATA +258 -0
- paperang_cli-0.1.0.dist-info/RECORD +29 -0
- paperang_cli-0.1.0.dist-info/WHEEL +5 -0
- paperang_cli-0.1.0.dist-info/entry_points.txt +3 -0
- paperang_cli-0.1.0.dist-info/licenses/LICENSE +23 -0
- paperang_cli-0.1.0.dist-info/top_level.txt +1 -0
paperang_cli/__init__.py
ADDED
paperang_cli/__main__.py
ADDED
paperang_cli/cli.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Standalone paperang-cli entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from paperang_cli import __version__
|
|
12
|
+
from paperang_cli.commands.battery import battery_command
|
|
13
|
+
from paperang_cli.commands.config_cmd import config_group
|
|
14
|
+
from paperang_cli.commands.discover import discover_command
|
|
15
|
+
from paperang_cli.commands.mac import mac_command
|
|
16
|
+
from paperang_cli.commands.print_cmd import print_group
|
|
17
|
+
from paperang_cli.commands.probe import probe_command
|
|
18
|
+
from paperang_cli.commands.status import status_command
|
|
19
|
+
from paperang_cli.config import load_config
|
|
20
|
+
from paperang_cli.errors import PaperangCliError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configure_logging(debug: bool) -> None:
|
|
24
|
+
level = logging.DEBUG if debug else logging.INFO
|
|
25
|
+
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
29
|
+
@click.option(
|
|
30
|
+
"--config",
|
|
31
|
+
"config_path",
|
|
32
|
+
type=click.Path(path_type=Path, dir_okay=False),
|
|
33
|
+
help="Path to a paperang-cli JSON config file.",
|
|
34
|
+
)
|
|
35
|
+
@click.option("--json", "json_output", is_flag=True, help="Emit machine-readable JSON output.")
|
|
36
|
+
@click.option("--debug", is_flag=True, help="Enable verbose logging.")
|
|
37
|
+
@click.version_option(version=__version__)
|
|
38
|
+
@click.pass_context
|
|
39
|
+
def cli(ctx: click.Context, config_path: Path | None, json_output: bool, debug: bool) -> None:
|
|
40
|
+
"""Standalone CLI for Paperang printers."""
|
|
41
|
+
configure_logging(debug)
|
|
42
|
+
try:
|
|
43
|
+
settings, resolved_path, config_exists = load_config(config_path)
|
|
44
|
+
except PaperangCliError as exc:
|
|
45
|
+
if json_output:
|
|
46
|
+
click.echo(
|
|
47
|
+
json.dumps(
|
|
48
|
+
{
|
|
49
|
+
"status": "error",
|
|
50
|
+
"code": exc.code,
|
|
51
|
+
"message": str(exc),
|
|
52
|
+
"exit_code": exc.exit_code,
|
|
53
|
+
},
|
|
54
|
+
indent=2,
|
|
55
|
+
ensure_ascii=False,
|
|
56
|
+
sort_keys=True,
|
|
57
|
+
),
|
|
58
|
+
err=True,
|
|
59
|
+
)
|
|
60
|
+
raise SystemExit(exc.exit_code)
|
|
61
|
+
raise click.ClickException(str(exc)) from exc
|
|
62
|
+
|
|
63
|
+
ctx.obj = {
|
|
64
|
+
"settings": settings,
|
|
65
|
+
"config_path": resolved_path,
|
|
66
|
+
"config_exists": config_exists,
|
|
67
|
+
"json_output": json_output,
|
|
68
|
+
"debug": debug,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
cli.add_command(status_command)
|
|
73
|
+
cli.add_command(battery_command)
|
|
74
|
+
cli.add_command(mac_command)
|
|
75
|
+
cli.add_command(probe_command)
|
|
76
|
+
cli.add_command(discover_command)
|
|
77
|
+
cli.add_command(print_group)
|
|
78
|
+
cli.add_command(config_group)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> None:
|
|
82
|
+
cli()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Battery command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from paperang_cli.drivers import registry
|
|
8
|
+
from paperang_cli.output import cli_error_boundary, emit_result
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("battery")
|
|
12
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def battery_command(ctx: click.Context, address: str | None) -> None:
|
|
15
|
+
"""Query only the current printer battery level without printing."""
|
|
16
|
+
with cli_error_boundary(ctx):
|
|
17
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
18
|
+
battery = driver.battery(address=address)
|
|
19
|
+
emit_result(
|
|
20
|
+
ctx,
|
|
21
|
+
{"status": "ok", "result": battery.to_dict()},
|
|
22
|
+
[
|
|
23
|
+
f"Model: {battery.model}",
|
|
24
|
+
f"Address: {battery.address}",
|
|
25
|
+
f"Connected: {battery.connected}",
|
|
26
|
+
f"Battery: {battery.battery_percent}",
|
|
27
|
+
],
|
|
28
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Config commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from paperang_cli.config import write_example_config
|
|
10
|
+
from paperang_cli.drivers.registry import supported_models
|
|
11
|
+
from paperang_cli.output import cli_error_boundary, emit_result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group("config")
|
|
15
|
+
def config_group() -> None:
|
|
16
|
+
"""Inspect or initialize paperang-cli configuration."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@config_group.command("show")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def show_config_command(ctx: click.Context) -> None:
|
|
22
|
+
"""Show the resolved config and whether it came from a file."""
|
|
23
|
+
settings = ctx.obj["settings"]
|
|
24
|
+
config_path = ctx.obj["config_path"]
|
|
25
|
+
config_exists = ctx.obj["config_exists"]
|
|
26
|
+
emit_result(
|
|
27
|
+
ctx,
|
|
28
|
+
{
|
|
29
|
+
"status": "ok",
|
|
30
|
+
"result": {
|
|
31
|
+
"config_path": str(config_path),
|
|
32
|
+
"config_exists": config_exists,
|
|
33
|
+
"config": settings.to_dict(),
|
|
34
|
+
"supported_models": supported_models(),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
[
|
|
38
|
+
f"Config path: {config_path}",
|
|
39
|
+
f"Config exists: {config_exists}",
|
|
40
|
+
f"Model: {settings.model}",
|
|
41
|
+
f"MAC address: {settings.macaddress or '<unset>'}",
|
|
42
|
+
f"Printer width: {settings.printerwidth}",
|
|
43
|
+
f"Print density: {settings.print_density}",
|
|
44
|
+
f"Post-print feed mm: {settings.post_print_feed_mm}",
|
|
45
|
+
f"Supported models: {', '.join(supported_models())}",
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@config_group.command("path")
|
|
51
|
+
@click.pass_context
|
|
52
|
+
def config_path_command(ctx: click.Context) -> None:
|
|
53
|
+
"""Show the resolved config path."""
|
|
54
|
+
config_path = ctx.obj["config_path"]
|
|
55
|
+
emit_result(
|
|
56
|
+
ctx,
|
|
57
|
+
{"status": "ok", "result": {"config_path": str(config_path)}},
|
|
58
|
+
[str(config_path)],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@config_group.command("init")
|
|
63
|
+
@click.option(
|
|
64
|
+
"--path",
|
|
65
|
+
"destination",
|
|
66
|
+
type=click.Path(path_type=Path, dir_okay=False),
|
|
67
|
+
help="Write the example config to a custom destination.",
|
|
68
|
+
)
|
|
69
|
+
@click.option("--force", is_flag=True, help="Overwrite an existing destination if present.")
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def config_init_command(ctx: click.Context, destination: Path | None, force: bool) -> None:
|
|
72
|
+
"""Write an example config file for the standalone CLI project."""
|
|
73
|
+
with cli_error_boundary(ctx):
|
|
74
|
+
target = destination or ctx.obj["config_path"]
|
|
75
|
+
written_path = write_example_config(target, overwrite=force)
|
|
76
|
+
emit_result(
|
|
77
|
+
ctx,
|
|
78
|
+
{"status": "ok", "result": {"config_path": str(written_path)}},
|
|
79
|
+
[f"Wrote example config to {written_path}"],
|
|
80
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Discover command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from paperang_cli.drivers import registry
|
|
8
|
+
from paperang_cli.output import cli_error_boundary, emit_result
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("discover")
|
|
12
|
+
@click.pass_context
|
|
13
|
+
def discover_command(ctx: click.Context) -> None:
|
|
14
|
+
"""Discover nearby supported printers without printing."""
|
|
15
|
+
with cli_error_boundary(ctx):
|
|
16
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
17
|
+
devices = driver.discover()
|
|
18
|
+
payload = [device.to_dict() for device in devices]
|
|
19
|
+
if devices:
|
|
20
|
+
human_lines = [f"{device.name} [{device.address}]" for device in devices]
|
|
21
|
+
else:
|
|
22
|
+
human_lines = ["No supported printers discovered."]
|
|
23
|
+
emit_result(ctx, {"status": "ok", "result": payload}, human_lines)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Bluetooth MAC command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from paperang_cli.drivers import registry
|
|
8
|
+
from paperang_cli.output import cli_error_boundary, emit_result
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("mac")
|
|
12
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def mac_command(ctx: click.Context, address: str | None) -> None:
|
|
15
|
+
"""Query the printer-reported Bluetooth MAC address without printing."""
|
|
16
|
+
with cli_error_boundary(ctx):
|
|
17
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
18
|
+
mac_status = driver.bluetooth_mac(address=address)
|
|
19
|
+
emit_result(
|
|
20
|
+
ctx,
|
|
21
|
+
{"status": "ok", "result": mac_status.to_dict()},
|
|
22
|
+
[
|
|
23
|
+
f"Model: {mac_status.model}",
|
|
24
|
+
f"Address: {mac_status.address}",
|
|
25
|
+
f"Connected: {mac_status.connected}",
|
|
26
|
+
f"Bluetooth MAC: {mac_status.bluetooth_mac}",
|
|
27
|
+
],
|
|
28
|
+
)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Printing commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from paperang_cli.drivers import registry
|
|
10
|
+
from paperang_cli.output import cli_error_boundary, emit_result
|
|
11
|
+
from paperang_cli.render import resolve_image_conversion
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group("print")
|
|
15
|
+
def print_group() -> None:
|
|
16
|
+
"""Print text, print images, or run the built-in self-test."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@print_group.command("text")
|
|
20
|
+
@click.argument("text")
|
|
21
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
22
|
+
@click.option("--font-size", type=int, help="Override font size for this print job.")
|
|
23
|
+
@click.option("--feed-mm", type=float, help="Override post-print feed in millimeters.")
|
|
24
|
+
@click.option("--allow-paper-use", is_flag=True, help="Permit a real paper-consuming print.")
|
|
25
|
+
@click.option("--dry-run", is_flag=True, help="Render and validate without sending anything to the printer.")
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def print_text_command(
|
|
28
|
+
ctx: click.Context,
|
|
29
|
+
text: str,
|
|
30
|
+
address: str | None,
|
|
31
|
+
font_size: int | None,
|
|
32
|
+
feed_mm: float | None,
|
|
33
|
+
allow_paper_use: bool,
|
|
34
|
+
dry_run: bool,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Print a single line or short block of text."""
|
|
37
|
+
_run_print(
|
|
38
|
+
ctx,
|
|
39
|
+
text=text,
|
|
40
|
+
paragraph=False,
|
|
41
|
+
address=address,
|
|
42
|
+
font_size=font_size,
|
|
43
|
+
feed_mm=feed_mm,
|
|
44
|
+
allow_paper_use=allow_paper_use,
|
|
45
|
+
dry_run=dry_run,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@print_group.command("paragraph")
|
|
50
|
+
@click.argument("text")
|
|
51
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
52
|
+
@click.option("--font-size", type=int, help="Override font size for this print job.")
|
|
53
|
+
@click.option("--feed-mm", type=float, help="Override post-print feed in millimeters.")
|
|
54
|
+
@click.option("--allow-paper-use", is_flag=True, help="Permit a real paper-consuming print.")
|
|
55
|
+
@click.option("--dry-run", is_flag=True, help="Render and validate without sending anything to the printer.")
|
|
56
|
+
@click.pass_context
|
|
57
|
+
def print_paragraph_command(
|
|
58
|
+
ctx: click.Context,
|
|
59
|
+
text: str,
|
|
60
|
+
address: str | None,
|
|
61
|
+
font_size: int | None,
|
|
62
|
+
feed_mm: float | None,
|
|
63
|
+
allow_paper_use: bool,
|
|
64
|
+
dry_run: bool,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Print a wrapped paragraph using the paragraph rendering mode."""
|
|
67
|
+
_run_print(
|
|
68
|
+
ctx,
|
|
69
|
+
text=text,
|
|
70
|
+
paragraph=True,
|
|
71
|
+
address=address,
|
|
72
|
+
font_size=font_size,
|
|
73
|
+
feed_mm=feed_mm,
|
|
74
|
+
allow_paper_use=allow_paper_use,
|
|
75
|
+
dry_run=dry_run,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run_print(
|
|
80
|
+
ctx: click.Context,
|
|
81
|
+
*,
|
|
82
|
+
text: str,
|
|
83
|
+
paragraph: bool,
|
|
84
|
+
address: str | None,
|
|
85
|
+
font_size: int | None,
|
|
86
|
+
feed_mm: float | None,
|
|
87
|
+
allow_paper_use: bool,
|
|
88
|
+
dry_run: bool,
|
|
89
|
+
) -> None:
|
|
90
|
+
with cli_error_boundary(ctx):
|
|
91
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
92
|
+
result = driver.print_text(
|
|
93
|
+
text,
|
|
94
|
+
paragraph=paragraph,
|
|
95
|
+
font_size=font_size,
|
|
96
|
+
feed_mm=feed_mm,
|
|
97
|
+
allow_paper_use=allow_paper_use,
|
|
98
|
+
dry_run=dry_run,
|
|
99
|
+
address=address,
|
|
100
|
+
)
|
|
101
|
+
_emit_print_result(ctx, result)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@print_group.command("image")
|
|
105
|
+
@click.argument("image_path", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
106
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
107
|
+
@click.option("--feed-mm", type=float, help="Override post-print feed in millimeters.")
|
|
108
|
+
@click.option(
|
|
109
|
+
"--mode",
|
|
110
|
+
type=click.Choice(["sticker", "photo"], case_sensitive=False),
|
|
111
|
+
default="sticker",
|
|
112
|
+
show_default=True,
|
|
113
|
+
help="High-level image preset. Sticker uses hard contrast, while photo uses dithering.",
|
|
114
|
+
)
|
|
115
|
+
@click.option(
|
|
116
|
+
"--conversion",
|
|
117
|
+
type=click.Choice(["threshold", "edge", "dither"], case_sensitive=False),
|
|
118
|
+
default=None,
|
|
119
|
+
help="Optional low-level conversion override. When provided, it takes precedence over --mode.",
|
|
120
|
+
)
|
|
121
|
+
@click.option("--allow-paper-use", is_flag=True, help="Permit a real paper-consuming image print.")
|
|
122
|
+
@click.option("--dry-run", is_flag=True, help="Render and validate without sending anything to the printer.")
|
|
123
|
+
@click.pass_context
|
|
124
|
+
def print_image_command(
|
|
125
|
+
ctx: click.Context,
|
|
126
|
+
image_path: Path,
|
|
127
|
+
address: str | None,
|
|
128
|
+
feed_mm: float | None,
|
|
129
|
+
mode: str,
|
|
130
|
+
conversion: str,
|
|
131
|
+
allow_paper_use: bool,
|
|
132
|
+
dry_run: bool,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Print a local image file after converting it to a monochrome printer bitstream."""
|
|
135
|
+
with cli_error_boundary(ctx):
|
|
136
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
137
|
+
resolved_conversion = resolve_image_conversion(mode=mode, conversion=conversion)
|
|
138
|
+
result = driver.print_image(
|
|
139
|
+
image_path,
|
|
140
|
+
conversion=resolved_conversion,
|
|
141
|
+
feed_mm=feed_mm,
|
|
142
|
+
allow_paper_use=allow_paper_use,
|
|
143
|
+
dry_run=dry_run,
|
|
144
|
+
address=address,
|
|
145
|
+
)
|
|
146
|
+
_emit_print_result(ctx, result)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@print_group.command("compose")
|
|
150
|
+
@click.argument("text")
|
|
151
|
+
@click.argument("image_path", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
152
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
153
|
+
@click.option("--font-size", type=int, help="Override font size for the text block.")
|
|
154
|
+
@click.option("--feed-mm", type=float, help="Override post-print feed in millimeters.")
|
|
155
|
+
@click.option(
|
|
156
|
+
"--layout",
|
|
157
|
+
type=click.Choice(["text-above", "image-above"], case_sensitive=False),
|
|
158
|
+
default="text-above",
|
|
159
|
+
show_default=True,
|
|
160
|
+
help="Vertical arrangement for the composed print.",
|
|
161
|
+
)
|
|
162
|
+
@click.option(
|
|
163
|
+
"--mode",
|
|
164
|
+
type=click.Choice(["sticker", "photo"], case_sensitive=False),
|
|
165
|
+
default="sticker",
|
|
166
|
+
show_default=True,
|
|
167
|
+
help="Image preset for the image part of the composed print.",
|
|
168
|
+
)
|
|
169
|
+
@click.option(
|
|
170
|
+
"--conversion",
|
|
171
|
+
type=click.Choice(["threshold", "edge", "dither"], case_sensitive=False),
|
|
172
|
+
default=None,
|
|
173
|
+
help="Optional low-level conversion override for the image part. When provided, it takes precedence over --mode.",
|
|
174
|
+
)
|
|
175
|
+
@click.option("--allow-paper-use", is_flag=True, help="Permit a real paper-consuming composed print.")
|
|
176
|
+
@click.option("--dry-run", is_flag=True, help="Render and validate without sending anything to the printer.")
|
|
177
|
+
@click.pass_context
|
|
178
|
+
def print_compose_command(
|
|
179
|
+
ctx: click.Context,
|
|
180
|
+
text: str,
|
|
181
|
+
image_path: Path,
|
|
182
|
+
address: str | None,
|
|
183
|
+
font_size: int | None,
|
|
184
|
+
feed_mm: float | None,
|
|
185
|
+
layout: str,
|
|
186
|
+
mode: str,
|
|
187
|
+
conversion: str,
|
|
188
|
+
allow_paper_use: bool,
|
|
189
|
+
dry_run: bool,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Print wrapped text together with a local image in one combined layout."""
|
|
192
|
+
with cli_error_boundary(ctx):
|
|
193
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
194
|
+
resolved_conversion = resolve_image_conversion(mode=mode, conversion=conversion)
|
|
195
|
+
result = driver.print_compose(
|
|
196
|
+
text,
|
|
197
|
+
image_path,
|
|
198
|
+
layout=layout.lower(),
|
|
199
|
+
font_size=font_size,
|
|
200
|
+
conversion=resolved_conversion,
|
|
201
|
+
feed_mm=feed_mm,
|
|
202
|
+
allow_paper_use=allow_paper_use,
|
|
203
|
+
dry_run=dry_run,
|
|
204
|
+
address=address,
|
|
205
|
+
)
|
|
206
|
+
_emit_print_result(ctx, result)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@print_group.command("self-test")
|
|
210
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
211
|
+
@click.option(
|
|
212
|
+
"--allow-large-paper-use",
|
|
213
|
+
is_flag=True,
|
|
214
|
+
help="Permit the built-in self-test, which consumes substantially more paper than ordinary prints.",
|
|
215
|
+
)
|
|
216
|
+
@click.option("--dry-run", is_flag=True, help="Validate the command path without sending the self-test to the printer.")
|
|
217
|
+
@click.pass_context
|
|
218
|
+
def print_self_test_command(
|
|
219
|
+
ctx: click.Context,
|
|
220
|
+
address: str | None,
|
|
221
|
+
allow_large_paper_use: bool,
|
|
222
|
+
dry_run: bool,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Run the printer self-test with an explicit large-paper-use warning."""
|
|
225
|
+
with cli_error_boundary(ctx):
|
|
226
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
227
|
+
result = driver.self_test(
|
|
228
|
+
allow_large_paper_use=allow_large_paper_use,
|
|
229
|
+
dry_run=dry_run,
|
|
230
|
+
address=address,
|
|
231
|
+
)
|
|
232
|
+
_emit_print_result(ctx, result)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _emit_print_result(ctx: click.Context, result) -> None:
|
|
236
|
+
human_lines = [
|
|
237
|
+
f"Operation: {result.operation}",
|
|
238
|
+
f"Dry run: {result.dry_run}",
|
|
239
|
+
f"Address: {result.address}",
|
|
240
|
+
]
|
|
241
|
+
if result.warning:
|
|
242
|
+
human_lines.append(f"Warning: {result.warning}")
|
|
243
|
+
if result.source_path:
|
|
244
|
+
human_lines.append(f"Source path: {result.source_path}")
|
|
245
|
+
if result.conversion:
|
|
246
|
+
human_lines.append(f"Conversion: {result.conversion}")
|
|
247
|
+
if result.layout:
|
|
248
|
+
human_lines.append(f"Layout: {result.layout}")
|
|
249
|
+
if result.feed_mm is not None:
|
|
250
|
+
human_lines.append(f"Feed mm: {result.feed_mm}")
|
|
251
|
+
if result.feed_units is not None:
|
|
252
|
+
human_lines.append(f"Feed units: {result.feed_units}")
|
|
253
|
+
if result.font_size is not None:
|
|
254
|
+
human_lines.append(f"Font size: {result.font_size}")
|
|
255
|
+
if result.bytes_sent is not None:
|
|
256
|
+
human_lines.append(f"Bytes prepared: {result.bytes_sent}")
|
|
257
|
+
if result.battery_after is not None:
|
|
258
|
+
human_lines.append(f"Battery after: {result.battery_after}")
|
|
259
|
+
|
|
260
|
+
emit_result(ctx, {"status": "ok", "result": result.to_dict()}, human_lines)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Probe command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from paperang_cli.drivers import registry
|
|
8
|
+
from paperang_cli.models import ProbeResult
|
|
9
|
+
from paperang_cli.output import cli_error_boundary, emit_result
|
|
10
|
+
|
|
11
|
+
LOCAL_TRANSPORT_NOTE = "Paperang P1 currently has no validated cable/local data mode in this project. Use Bluetooth for live printer communication."
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("probe")
|
|
15
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def probe_command(ctx: click.Context, address: str | None) -> None:
|
|
18
|
+
"""Run a safe multi-query probe and summarize the current printer state."""
|
|
19
|
+
with cli_error_boundary(ctx):
|
|
20
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
21
|
+
status = driver.status(address=address)
|
|
22
|
+
bluetooth_mac = driver.bluetooth_mac(address=address)
|
|
23
|
+
result = ProbeResult(
|
|
24
|
+
model=status.model,
|
|
25
|
+
address=status.address,
|
|
26
|
+
transport=status.transport,
|
|
27
|
+
connected=status.connected,
|
|
28
|
+
battery_percent=status.battery_percent,
|
|
29
|
+
serial_number=status.serial_number,
|
|
30
|
+
firmware_version=status.firmware_version,
|
|
31
|
+
hardware_info=status.hardware_info,
|
|
32
|
+
density=status.density,
|
|
33
|
+
power_off_time=status.power_off_time,
|
|
34
|
+
bluetooth_mac=bluetooth_mac.bluetooth_mac,
|
|
35
|
+
local_transport_supported=False,
|
|
36
|
+
local_transport_note=LOCAL_TRANSPORT_NOTE,
|
|
37
|
+
raw=dict(status.raw),
|
|
38
|
+
)
|
|
39
|
+
emit_result(
|
|
40
|
+
ctx,
|
|
41
|
+
{"status": "ok", "result": result.to_dict()},
|
|
42
|
+
[
|
|
43
|
+
f"Model: {result.model}",
|
|
44
|
+
f"Address: {result.address}",
|
|
45
|
+
f"Transport: {result.transport}",
|
|
46
|
+
f"Connected: {result.connected}",
|
|
47
|
+
f"Battery: {result.battery_percent}",
|
|
48
|
+
f"Serial: {result.serial_number}",
|
|
49
|
+
f"Firmware: {result.firmware_version}",
|
|
50
|
+
f"Density: {result.density}",
|
|
51
|
+
f"Power-off time: {result.power_off_time}",
|
|
52
|
+
f"Hardware info: {result.hardware_info}",
|
|
53
|
+
f"Bluetooth MAC: {result.bluetooth_mac}",
|
|
54
|
+
f"Local transport supported: {result.local_transport_supported}",
|
|
55
|
+
f"Local transport note: {result.local_transport_note}",
|
|
56
|
+
],
|
|
57
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Status command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from paperang_cli.drivers import registry
|
|
8
|
+
from paperang_cli.output import cli_error_boundary, emit_result
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("status")
|
|
12
|
+
@click.option("--address", type=str, help="Optional printer MAC address override.")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def status_command(ctx: click.Context, address: str | None) -> None:
|
|
15
|
+
"""Query printer status without printing."""
|
|
16
|
+
with cli_error_boundary(ctx):
|
|
17
|
+
driver = registry.get_driver(ctx.obj["settings"])
|
|
18
|
+
status = driver.status(address=address)
|
|
19
|
+
emit_result(
|
|
20
|
+
ctx,
|
|
21
|
+
{"status": "ok", "result": status.to_dict()},
|
|
22
|
+
[
|
|
23
|
+
f"Model: {status.model}",
|
|
24
|
+
f"Address: {status.address}",
|
|
25
|
+
f"Connected: {status.connected}",
|
|
26
|
+
f"Battery: {status.battery_percent}",
|
|
27
|
+
f"Serial: {status.serial_number}",
|
|
28
|
+
f"Firmware: {status.firmware_version}",
|
|
29
|
+
f"Density: {status.density}",
|
|
30
|
+
f"Power-off time: {status.power_off_time}",
|
|
31
|
+
f"Hardware info: {status.hardware_info}",
|
|
32
|
+
],
|
|
33
|
+
)
|