Version 7
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -166,3 +166,5 @@ devices.xml
|
||||
*.pickle
|
||||
|
||||
.console_history
|
||||
|
||||
lpr_jobs/
|
||||
71
README.md
71
README.md
@@ -31,6 +31,16 @@ The software also includes a configurable printer dictionary, which can be easil
|
||||
|
||||
Uses a higher quantity of ink to perform a deeper cleaning cycle. Power cleaning also consumes more ink and fills the waste ink tank more quickly. It should only be used when normal cleaning is insufficient.
|
||||
|
||||
- Print Test Patterns.
|
||||
|
||||
Execute a set of test printing functions:
|
||||
|
||||
- Standard Nozzle Test – Ask the printer to print its internal predefined pattern.
|
||||
- Alternative Nozzle Test – Use an alternative predefined pattern.
|
||||
- Color Test Pattern – Print a b/w and color page, optimized for Epson XP-200 series printers.
|
||||
- Advance Paper – Move the loaded sheet forward by a specified number of lines without printing.
|
||||
- Feed Multiple Sheets – Pass a specified number of sheets through the printer without printing.
|
||||
|
||||
- Temporary reset of the ink waste counter.
|
||||
|
||||
The ink waste counters track the amount of ink discarded during maintenance tasks to prevent overflow in the waste ink pads. Once the counters indicate that one of the printer pads is full, the printer will stop working to avoid potential damage or ink spills. The "Printer status" button includes information showing the levels of the waste ink tanks; specifically, two sections are relevant: "Maintenance box information" ("maintenance_box_...") and "Waste Ink Levels" ("waste_ink_levels"). The former has a counter associated for each tank, which indicates the number of temporary resets performed by the user to temporarily restore a disabled printer.
|
||||
@@ -568,7 +578,7 @@ Two-bytes|Description | Notes | Parameters
|
||||
cd | | | (0)
|
||||
cs | | | (0 or 1)
|
||||
cx | | |
|
||||
di | Device Identification ("di" 01H 00H 01H) | Implemented in this program | (1)
|
||||
di | Get Device ID (identification) ("di" 01H 00H 01H), same as @EJL[SP]ID[CR][LF] | Implemented in this program | (1)
|
||||
ei | | | (0)
|
||||
ex | Set Vertical Print Page Line Mode, Roll Paper Mode | - ex BC=6 00 00 00 00 0x14 xx (Set Vertical Print Page Line Mode. xx=00 is off, xx=01 is on. If turned on, this prints vertical trim lines at the left and right margins).<br> - ex BC=6 00 00 00 00 0x05 xx (Set Roll Paper Mode. If xx is 0, roll paper mode is off; if xx is 1, roll paper mode is on).<br> - ex BC=3 00 xx yy (Appears to be a synonym for the SN command described above.) |
|
||||
fl | Firmware load. Enter recovery mode | |
|
||||
@@ -592,6 +602,8 @@ escutil.c also mentions [`ri\2\0\0\0`](https://github.com/echiu64/gutenprint/blo
|
||||
|
||||
[Other font](https://codeberg.org/KalleMP/reinkpy/src/branch/main/reinkpy/epson/core.py#L22) also mentions `pc:\x01:NA` in some printer firmwares.
|
||||
|
||||
Reply of any non supported commands: “XX:;” FF. (XX is the command string being invalid.)
|
||||
|
||||
### Examples for EEPROM access
|
||||
|
||||
#### Read EEPROM
|
||||
@@ -621,6 +633,8 @@ d1 : EEPROM address (00h - FFh)
|
||||
|
||||
SNMP OID example: `1.3.6.1.4.1.1248.1.2.2.44.1.1.2.1.124.124.7.0.73.8.65.190.160.48.0`
|
||||
|
||||
EEPROM data reply: “@BDC” SP “PS” CR LF “EE:” <addr> <data> “;” FF.
|
||||
|
||||
#### Write EEPROM
|
||||
|
||||
- 124.124: "||" = Read EEPROM (EPSON-CTRL)
|
||||
@@ -748,48 +762,29 @@ for i in ec_sequences:
|
||||
|
||||
## Remote Mode commands
|
||||
|
||||
Comprehensive, unified documentation for Epson’s Remote Mode commands does not exist: support varies by model, and command references are scattered across service manuals, programming guides and third-party sources (for example, the [Developer's Guide to Gutenprint](https://gimp-print.sourceforge.io/reference-html/x952.html) or [GIMP-Print - ESC/P2 Remote Mode Commands](http://osr507doc.xinuos.com/en/OSAdminG/OSAdminG_gimp/manual-html/gimpprint_37.html)).
|
||||
The [PyPrintLpr](https://github.com/Ircama/PyPrintLpr) module is used for sending Epson LPR commands over a LPR connection. This channel does not support receiving payload responses from the printer.
|
||||
|
||||
The `EpsonLpr` class is used for sending Epson LPR commands over a RAW, unidirectional TCP connection on port 9100. This channel does not support receiving responses from the printer.
|
||||
Refer to [Epson Remote Mode commands](https://github.com/Ircama/PyPrintLpr?tab=readme-ov-file#epson-remote-mode-commands) and to https://gimp-print.sourceforge.io/reference-html/x952.html for a description of the known Remote Mode commands.
|
||||
|
||||
| **Method** | **Description** |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `connect` | Opens a TCP socket connection to the printer at the specified host and port, with timeout. |
|
||||
| `disconnect` | Gracefully shuts down and closes the socket connection if open. |
|
||||
| `send(data)` | Sends raw `bytes` directly to the printer over the socket connection. |
|
||||
| `remote_cmd(cmd, args)` | Constructs a Remote Mode command: 2-byte ASCII command + 2-byte little-endian length + arguments. |
|
||||
Check `self.printer.check_nozzles()` and `self.printer.clean_nozzles(0)` for examples of usage.
|
||||
|
||||
Predefined remote mode commands in this class:
|
||||
|
||||
| **Command** | **Description** |
|
||||
| --------------------- | ------------------------------------------------------- |
|
||||
| `LF` | Line Feed (new line). |
|
||||
| `FF` | Form Feed; flushes the buffer / ejects the page. |
|
||||
| `EXIT_PACKET_MODE` | Exits IEEE 1284.4 (D4) packet mode. |
|
||||
| `INITIALIZE_PRINTER` | Resets printer to default state (ESC @). |
|
||||
| `REMOTE_MODE` | Enter Epson Remote Command mode. |
|
||||
| `ENTER_REMOTE_MODE` | Initialize printer and enter Epson Remote Command mode. |
|
||||
| `EXIT_REMOTE_MODE` | Exits Remote Mode. |
|
||||
| `JOB_START` | Begins a print job (JS). |
|
||||
| `JOB_END` | Ends a print job (JE). |
|
||||
| `PRINT_NOZZLE_CHECK` | Triggers a nozzle check print pattern (NC). |
|
||||
| `VERSION_INFORMATION` | Requests firmware or printer version info (VI). |
|
||||
| `LD` | (unknown). |
|
||||
|
||||
Check `self.printer.check_nozzles()` and `self.printer.clean_nozzles(0)` for examples of usage. The following code prints the nozzle-check print pattern (copy and paste the code to the Interactive Console after selecting a printer and related host address):
|
||||
The following code prints the nozzle-check print pattern (copy and paste the code to the Interactive Console after selecting a printer and related host address):
|
||||
|
||||
```python
|
||||
from epson_print_conf import EpsonLpr
|
||||
lpr = EpsonLpr(self.printer.hostname)
|
||||
data = (
|
||||
lpr.EXIT_PACKET_MODE + # Exit packet mode
|
||||
lpr.ENTER_REMOTE_MODE + # Engage remote mode commands
|
||||
lpr.PRINT_NOZZLE_CHECK + # Issue nozzle-check print pattern
|
||||
lpr.EXIT_REMOTE_MODE + # Disengage remote control
|
||||
lpr.JOB_END # Mark maintenance job complete
|
||||
)
|
||||
print(f"\nDump of data:\n{self.printer.hexdump(data)}\n")
|
||||
lpr.connect().send(data).disconnect()
|
||||
from pyprintlpr import LprClient
|
||||
from hexdump2 import hexdump
|
||||
|
||||
with LprClient('192.168.1.100', port="LPR", queue='PASSTHRU') as lpr:
|
||||
data = (
|
||||
lpr.EXIT_PACKET_MODE + # Exit packet mode
|
||||
lpr.ENTER_REMOTE_MODE + # Engage remote mode commands
|
||||
lpr.PRINT_NOZZLE_CHECK + # Issue nozzle-check print pattern
|
||||
lpr.EXIT_REMOTE_MODE + # Disengage remote control
|
||||
lpr.JOB_END # Mark maintenance job complete
|
||||
)
|
||||
print("\nDump of data:\n")
|
||||
hexdump(data)
|
||||
lpr.send(data)
|
||||
```
|
||||
|
||||
## ST2 Status Reply Codes
|
||||
|
||||
@@ -21,7 +21,6 @@ import pickle
|
||||
import abc
|
||||
import hashlib
|
||||
import struct
|
||||
import socket
|
||||
|
||||
from pysnmp.hlapi.v1arch.asyncio import *
|
||||
from pyasn1.type.univ import OctetString as OctetStringType
|
||||
@@ -32,82 +31,7 @@ from pysnmp_sync_adapter import (
|
||||
cluster_varbinds
|
||||
)
|
||||
from pysnmp.proto.errind import RequestTimedOut
|
||||
|
||||
|
||||
class EpsonLpr:
|
||||
"""
|
||||
Interface for sending Epson LPR commands over RAW (port 9100)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
hostname: str,
|
||||
port: int = 9100,
|
||||
timeout: float = 5.0,
|
||||
recv_buffer: int = 4096):
|
||||
self.hostname = hostname
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.recv_buffer = recv_buffer
|
||||
self.sock: Optional[socket.socket] = None
|
||||
|
||||
# Define Epson sequences
|
||||
self.LF = b'\x0a'
|
||||
self.FF = b'\x0c' # flush buffer
|
||||
self.UNIVERSAL_EXIT = b'\x00\x00\x00\x1b\x01' # required by newer printers
|
||||
self.EXIT_PACKET_MODE = (
|
||||
self.UNIVERSAL_EXIT + b'@EJL 1284.4\n@EJL ' + self.LF
|
||||
)
|
||||
self.INITIALIZE_PRINTER = b'\x1b@'
|
||||
self.REMOTE_MODE = b'\x1b' + self.remote_cmd("(R", b'\x00REMOTE1')
|
||||
self.ENTER_REMOTE_MODE = (
|
||||
self.INITIALIZE_PRINTER +
|
||||
self.INITIALIZE_PRINTER +
|
||||
self.REMOTE_MODE
|
||||
)
|
||||
self.EXIT_REMOTE_MODE = b'\x1b\x00\x00\x00'
|
||||
self.JOB_START = self.remote_cmd("JS", b'\x00\x00\x00\x00')
|
||||
self.JOB_END = self.remote_cmd("JE", b'\x00')
|
||||
self.PRINT_NOZZLE_CHECK = self.remote_cmd("NC", b'\x00\x00')
|
||||
self.VERSION_INFORMATION = self.remote_cmd("VI", b'\x00\x00')
|
||||
self.LD = self.remote_cmd("LD", b'')
|
||||
|
||||
def __enter__(self) -> "EpsonLpr":
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
self.disconnect()
|
||||
|
||||
def connect(self) -> None:
|
||||
"""Establish a TCP connection to the printer."""
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(self.timeout)
|
||||
self.sock.connect((self.hostname, self.port))
|
||||
return self
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Shutdown and close the socket."""
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
except Exception:
|
||||
pass
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
return self
|
||||
|
||||
def send(self, data: bytes) -> None:
|
||||
"""Send raw bytes to the printer."""
|
||||
if not self.sock:
|
||||
raise RuntimeError("Not connected to printer")
|
||||
self.sock.sendall(data)
|
||||
return self
|
||||
|
||||
def remote_cmd(self, cmd, args):
|
||||
"Generate a Remote Mode command."
|
||||
if len(cmd) != 2:
|
||||
raise ValueError("command should be exactly 2 characters")
|
||||
return cmd.encode() + struct.pack('<H', len(args)) + args
|
||||
from pyprintlpr import LprClient
|
||||
|
||||
|
||||
class EpsonPrinter:
|
||||
@@ -1147,46 +1071,6 @@ class EpsonPrinter:
|
||||
"""
|
||||
return(filter(lambda x: x.startswith("get_"), dir(self)))
|
||||
|
||||
def hexdump(self, data: Union[bytes, bytearray], width: int = 16) -> str:
|
||||
"""
|
||||
Produce a hex + ASCII dump of the given data.
|
||||
|
||||
Each line shows:
|
||||
- 8-digit hex offset
|
||||
- hex bytes (grouped by width, with extra space every 8 bytes)
|
||||
- printable ASCII (non-printables as '.')
|
||||
|
||||
:param data: Bytes to dump.
|
||||
:param width: Number of bytes per line (default: 16).
|
||||
:return: The formatted hexdump.
|
||||
"""
|
||||
lines = []
|
||||
for offset in range(0, len(data), width):
|
||||
chunk = data[offset : offset + width]
|
||||
|
||||
# Hex part, with a space every byte and extra gap at half‑width
|
||||
hex_bytes = ' '.join(f"{b:02X}" for b in chunk)
|
||||
half = width // 2
|
||||
if len(chunk) > half:
|
||||
# insert extra space between halves
|
||||
parts = hex_bytes.split(' ')
|
||||
hex_bytes = (
|
||||
' '.join(parts[:half]) + ' ' + ' '.join(parts[half:])
|
||||
)
|
||||
|
||||
# Pad hex part so ASCII column aligns
|
||||
expected_len = width * 2 + (width - 1) + 2 # bytes*2 hex + spaces + extra half‑split
|
||||
hex_part = hex_bytes.ljust(expected_len)
|
||||
|
||||
# ASCII part: printable or '.'
|
||||
ascii_part = (
|
||||
''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
|
||||
)
|
||||
|
||||
lines.append(f"{offset:08X} {hex_part} {ascii_part}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def expand_printer_conf(self, conf):
|
||||
"""
|
||||
Expand "alias" and "same-as" of a printer database for all printers
|
||||
@@ -2628,20 +2512,23 @@ class EpsonPrinter:
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_nozzles(self):
|
||||
def check_nozzles(self, type=0):
|
||||
"""
|
||||
Print nozzle-check pattern.
|
||||
"""
|
||||
if not self.hostname:
|
||||
return None
|
||||
status = True
|
||||
lpr = EpsonLpr(self.hostname)
|
||||
lpr = LprClient(self.hostname, port="LPR", label="Check nozzles")
|
||||
|
||||
# Sequence list
|
||||
nozzle_check = lpr.PRINT_NOZZLE_CHECK # Issue nozzle-check print pattern
|
||||
if type == 1:
|
||||
nozzle_check[-1] = b'\x10'
|
||||
commands = [
|
||||
lpr.EXIT_PACKET_MODE, # Exit packet mode
|
||||
lpr.ENTER_REMOTE_MODE, # Engage remote mode commands
|
||||
lpr.PRINT_NOZZLE_CHECK, # Issue nozzle-check print pattern
|
||||
nozzle_check,
|
||||
lpr.EXIT_REMOTE_MODE, # Disengage remote control
|
||||
lpr.JOB_END # Mark maintenance job complete
|
||||
]
|
||||
@@ -2654,40 +2541,257 @@ class EpsonPrinter:
|
||||
lpr.disconnect()
|
||||
return status
|
||||
|
||||
def clean_nozzles(self, group_index, power_clean=False):
|
||||
def print_test_color_pattern(self):
|
||||
"""
|
||||
Print a one-page test color pattern with different qualities.
|
||||
For XP-200, XP-205
|
||||
"""
|
||||
if not self.hostname:
|
||||
return None
|
||||
status = True
|
||||
lpr = LprClient(self.hostname, port="LPR", label="Check nozzles")
|
||||
|
||||
# Transfer Raster image commands (ESC i), Color, Run Length Encoding, 2bits per pixel
|
||||
TRI_BLACK = "1b6900010250008000" # ESC i 0: Black
|
||||
TRI_YELLOW = "1b6901010250002a00" # ESC i 1: Yellow
|
||||
TRI_MAGENTA = "1b6904010250002a00" # ESC i 4: Magenta
|
||||
TRI_CYAN = "1b6902010250002a00" # ESC i 2: Cyan
|
||||
|
||||
SET_H_POS = "1b28240400" # ESC ( $ = Set absolute horizontal print position (first part)
|
||||
|
||||
USE_MONOCHROME = "1b284b02000001" # ESC ( K = Monochrome Mode / Color Mode Selection, 01H: Monochrome mode
|
||||
USE_COLOR = "1b284b02000000" # ESC ( K = Monochrome Mode / Color Mode Selection, 00H: Default mode (color mode)
|
||||
|
||||
vsd_code = { # Variable Sized Droplet
|
||||
-1: "00", # VSD1 1bit or MC1-1 1 bit (for DOS)
|
||||
0: "10", # Economy, Fast Draft
|
||||
1: "11", # VSD1 2bit - fast eco, economy or speed/normal,
|
||||
2: "12", # VSD2 2bit - fine/quality,
|
||||
3: "13", # VSD3 2bit - super fine/high quality,
|
||||
4: "21", # MC1-1 Fine
|
||||
5: "25", # MC1-5 Super Photo
|
||||
6: "32", # Foto
|
||||
7: "22", # MC1-2 Foto
|
||||
8: "21", # Photo Draft
|
||||
9: "31", # MC2-1 Normal
|
||||
10: "32", # MC2-2
|
||||
}
|
||||
|
||||
# Solid fill patterns for homogeneous areas
|
||||
PATTERN_SOLID_HIGH = "d9ff" # High density solid - d9 = 11011001 (5 out of 8 bits = 62.5%) - FF (11111111) = 100% bit density = HIGH
|
||||
PATTERN_SOLID_MEDIUM = "d9aa" # Medium density solid - d9 = 11011001 (5 out of 8 bits = 62.5%) - AA (10101010) = 50% bit density = MEDIUM
|
||||
PATTERN_SOLID_LOW = "d955" # Low density solid - d9 = 11011001 (5 out of 8 bits = 62.5%) - 55 (01010101) = 50% bit density = LOW
|
||||
PATTERN_SOLID_VERY_LOW = "d900d900"
|
||||
|
||||
# High contrast alternating patterns (0xFF and 0x00)
|
||||
PATTERN_HIGH_CONTRAST_ALT = (
|
||||
PATTERN_SOLID_HIGH + PATTERN_SOLID_VERY_LOW + PATTERN_SOLID_HIGH # 11011001 11111111 11011001 00000000...
|
||||
)
|
||||
# Medium contrast alternating patterns (0xAA - 10101010 pattern)
|
||||
PATTERN_MEDIUM_CONTRAST_ALT = (
|
||||
PATTERN_SOLID_MEDIUM + PATTERN_SOLID_VERY_LOW + PATTERN_SOLID_MEDIUM # 11011001 10101010 11011001 00000000...
|
||||
)
|
||||
# Low contrast alternating patterns (0x55 - 01010101 pattern)
|
||||
PATTERN_LOW_CONTRAST_ALT = (
|
||||
PATTERN_SOLID_LOW + PATTERN_SOLID_VERY_LOW + PATTERN_SOLID_LOW # 11011001 01010101 11011001 00000000...
|
||||
)
|
||||
# Define the printing segments - each represents a label with different patterns and text
|
||||
printing_segments = [
|
||||
{
|
||||
"label_sequence": lpr.EXIT_REMOTE_MODE
|
||||
+ b'\r\n\r\nEconomy\r\n',
|
||||
"vsd": 0,
|
||||
"alternating_pattern": PATTERN_HIGH_CONTRAST_ALT,
|
||||
"solid_pattern": PATTERN_SOLID_HIGH,
|
||||
},
|
||||
{
|
||||
"label_sequence": lpr.INITIALIZE_PRINTER
|
||||
+ b"\r\n\n\n\nVSD1 - M. - Normal\r\n",
|
||||
"vsd": 1,
|
||||
"alternating_pattern": PATTERN_MEDIUM_CONTRAST_ALT,
|
||||
"solid_pattern": PATTERN_SOLID_MEDIUM,
|
||||
},
|
||||
{
|
||||
"label_sequence": lpr.INITIALIZE_PRINTER
|
||||
+ b"\r\n\n\n\nVSD2 - M. - Quality\r\n",
|
||||
"vsd": 2,
|
||||
"alternating_pattern": PATTERN_MEDIUM_CONTRAST_ALT,
|
||||
"solid_pattern": PATTERN_SOLID_MEDIUM,
|
||||
},
|
||||
{
|
||||
"label_sequence": lpr.INITIALIZE_PRINTER
|
||||
+ b"\r\n\n\n\nVSD3 - L. - High Quality\r\n",
|
||||
"vsd": 3,
|
||||
"alternating_pattern": PATTERN_HIGH_CONTRAST_ALT,
|
||||
"solid_pattern": PATTERN_SOLID_HIGH,
|
||||
},
|
||||
{
|
||||
"label_sequence": lpr.INITIALIZE_PRINTER
|
||||
+ b"\r\n\n\n\nVSD3 - M. - High Quality\r\n",
|
||||
"vsd": 3,
|
||||
"alternating_pattern": PATTERN_MEDIUM_CONTRAST_ALT,
|
||||
"solid_pattern": PATTERN_SOLID_MEDIUM,
|
||||
},
|
||||
{
|
||||
"label_sequence": lpr.INITIALIZE_PRINTER
|
||||
+ b"\r\n\n\n\nVSD3 - S. - High Quality\r\n",
|
||||
"vsd": 3,
|
||||
"alternating_pattern": PATTERN_LOW_CONTRAST_ALT,
|
||||
"solid_pattern": PATTERN_SOLID_LOW,
|
||||
},
|
||||
]
|
||||
|
||||
def generate_patterns():
|
||||
"""
|
||||
Generate the complete ESC/P2 command sequence for the patterns.
|
||||
"""
|
||||
command_parts = []
|
||||
|
||||
for segment in printing_segments:
|
||||
|
||||
# Label
|
||||
command_parts.append(segment["label_sequence"].hex())
|
||||
|
||||
# Initialization
|
||||
command_parts.append(
|
||||
"1b2847010001" # Select graphics mode
|
||||
+ "1b28550500010101a005" # ESC (U = Sets 360 DPI resolutio
|
||||
+ "1b28430400c6410000" # ESC (C = Configures page lenght
|
||||
+ "1b28630800ffffffffc6410000" # ESC (c = Set page format
|
||||
+ "1b28530800822e0000c6410000" # ESC (S = paper dimension specification
|
||||
+ "1b2844040068010301" # ESC (D = raster image resolution
|
||||
+ "1b2865020000" + vsd_code[segment["vsd"]] # ESC (e = Select Ink Drop Size
|
||||
+ "1b5502" # ESC U 02H = selects automatic printing direction control
|
||||
+ USE_MONOCHROME
|
||||
+ "1b2876040000010000" # ESC (v = Set relative vertical print position
|
||||
)
|
||||
|
||||
# First block - black alternating
|
||||
command_parts.append(SET_H_POS + "00010000") # ESC ( $ = Set absolute horizontal print position
|
||||
command_parts.append(TRI_BLACK)
|
||||
command_parts.append(segment["alternating_pattern"] * 64)
|
||||
|
||||
# Second block - Yellow/Magenta/Cyan alternating
|
||||
command_parts.append(USE_COLOR + SET_H_POS + "80060000") # ESC ( $ = Set absolute horizontal print position
|
||||
|
||||
command_parts.append(TRI_YELLOW)
|
||||
command_parts.append(segment["alternating_pattern"] * 64)
|
||||
|
||||
command_parts.append(SET_H_POS + "80060000")
|
||||
command_parts.append(TRI_MAGENTA)
|
||||
command_parts.append(segment["alternating_pattern"] * 64)
|
||||
|
||||
command_parts.append(SET_H_POS + "80060000")
|
||||
command_parts.append(TRI_CYAN)
|
||||
command_parts.append(segment["alternating_pattern"] * 64)
|
||||
|
||||
# Third block - Black solid
|
||||
command_parts.append(USE_MONOCHROME + SET_H_POS + "000c0000") # ESC ( $ = Set absolute horizontal print position
|
||||
|
||||
command_parts.append(TRI_BLACK)
|
||||
command_parts.append(segment["solid_pattern"] * 256)
|
||||
|
||||
# Fourth block - Yellow/Magenta/Cyan solid
|
||||
command_parts.append(USE_COLOR + SET_H_POS + "80110000") # ESC ( $ = Set absolute horizontal print position
|
||||
|
||||
command_parts.append(TRI_YELLOW)
|
||||
command_parts.append(segment["solid_pattern"] * 256)
|
||||
|
||||
command_parts.append(SET_H_POS + "80110000")
|
||||
command_parts.append(TRI_MAGENTA)
|
||||
command_parts.append(segment["solid_pattern"] * 256)
|
||||
|
||||
command_parts.append(SET_H_POS + "80110000")
|
||||
command_parts.append(TRI_CYAN)
|
||||
command_parts.append(segment["solid_pattern"] * 256)
|
||||
|
||||
command_parts.append("1b2876040000030000") # ESC (v = Set relative vertical print position (move down)
|
||||
|
||||
command_parts.append(
|
||||
(
|
||||
lpr.INITIALIZE_PRINTER
|
||||
+ b"\r\n\n\n\n"
|
||||
+ b"Epson Printer Configuration - Print Test Patterns"
|
||||
+ b"\r\n"
|
||||
).hex()
|
||||
)
|
||||
# Join all command parts into final hex string
|
||||
return "".join(command_parts)
|
||||
|
||||
pattern = (
|
||||
lpr.INITIALIZE_PRINTER
|
||||
+ lpr.REMOTE_MODE
|
||||
+ lpr.PRINT_NOZZLE_CHECK
|
||||
|
||||
+ bytes.fromhex(generate_patterns())
|
||||
|
||||
+ lpr.INITIALIZE_PRINTER
|
||||
+ b'\r'
|
||||
+ lpr.FF
|
||||
+ lpr.INITIALIZE_PRINTER
|
||||
+ lpr.REMOTE_MODE
|
||||
+ lpr.LD
|
||||
+ lpr.EXIT_REMOTE_MODE
|
||||
+ lpr.INITIALIZE_PRINTER
|
||||
+ lpr.REMOTE_MODE
|
||||
+ lpr.LD
|
||||
+ lpr.JOB_END
|
||||
+ lpr.EXIT_REMOTE_MODE
|
||||
)
|
||||
|
||||
try:
|
||||
lpr.connect()
|
||||
resp = lpr.send(pattern)
|
||||
except Exception as e:
|
||||
status = False
|
||||
finally:
|
||||
lpr.disconnect()
|
||||
return status
|
||||
|
||||
def clean_nozzles(self, group_index, power_clean=False, has_alt_mode=None):
|
||||
"""
|
||||
Initiates nozzles cleaning routine with optional power clean.
|
||||
"""
|
||||
if not self.hostname:
|
||||
return None
|
||||
if group_index > 5 or group_index < 0:
|
||||
if has_alt_mode and (group_index > has_alt_mode or group_index) < 0:
|
||||
return None
|
||||
if not has_alt_mode and (group_index > 5 or group_index) < 0:
|
||||
return None
|
||||
status = True
|
||||
lpr = EpsonLpr(self.hostname)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
t_data = bytearray()
|
||||
t_data = b'\x00'
|
||||
t_data += now.year.to_bytes(2, 'big') # Year
|
||||
t_data += bytes([now.month, now.day, now.hour, now.minute, now.second])
|
||||
lpr = LprClient(self.hostname, port="LPR", label="Clean nozzles")
|
||||
|
||||
group = group_index # https://github.com/abrasive/x900-otsakupuhastajat/blob/master/emanage.py#L148-L154
|
||||
if power_clean:
|
||||
group |= 0x10 # https://github.com/abrasive/x900-otsakupuhastajat/blob/master/emanage.py#L220
|
||||
|
||||
# Sequence list
|
||||
# Sequence list (Epson XP-205 207 Series Printing Preferences > Utilty > Clean Heads)
|
||||
commands = [
|
||||
lpr.EXIT_PACKET_MODE, # Exit packet mode
|
||||
lpr.ENTER_REMOTE_MODE, # Engage remote mode commands
|
||||
lpr.remote_cmd("TI", t_data), # Synchronize RTC
|
||||
lpr.set_timer(), # Sync RTC
|
||||
lpr.remote_cmd("CH", b'\x00' + bytes([group])), # Run print-head cleaning
|
||||
lpr.EXIT_REMOTE_MODE, # Disengage remote control
|
||||
lpr.JOB_END # Mark maintenance job complete
|
||||
lpr.ENTER_REMOTE_MODE, # Prepare for JOB_END
|
||||
lpr.JOB_END, # Mark maintenance job complete
|
||||
lpr.EXIT_REMOTE_MODE # Close sequence
|
||||
]
|
||||
|
||||
if has_alt_mode and group_index == has_alt_mode:
|
||||
commands = [
|
||||
lpr.INITIALIZE_PRINTER,
|
||||
bytes.fromhex("1B 7C 00 06 00 19 07 84 7B 42 02") # Head cleaning
|
||||
]
|
||||
if has_alt_mode and group_index == has_alt_mode and power_clean:
|
||||
commands = [
|
||||
lpr.INITIALIZE_PRINTER,
|
||||
bytes.fromhex("1B 7C 00 06 00 19 07 84 7B 42 0A") # Ink charge
|
||||
]
|
||||
try:
|
||||
lpr.connect()
|
||||
lpr.send(b"".join(commands))
|
||||
except Exception as e:
|
||||
logging.error("LPR error: %s", e)
|
||||
status = False
|
||||
finally:
|
||||
lpr.disconnect()
|
||||
|
||||
@@ -6,3 +6,5 @@ pyperclip
|
||||
black
|
||||
tomli
|
||||
text-console>=2.0.7
|
||||
hexdump2
|
||||
pyprintlpr
|
||||
|
||||
236
ui.py
236
ui.py
@@ -30,13 +30,14 @@ from tkcalendar import DateEntry # Ensure you have: pip install tkcalendar
|
||||
from tkinter import simpledialog, messagebox, filedialog
|
||||
|
||||
import pyperclip
|
||||
from epson_print_conf import EpsonPrinter, get_printer_models, EpsonLpr
|
||||
from epson_print_conf import EpsonPrinter, get_printer_models
|
||||
from pyprintlpr import LprClient
|
||||
from parse_devices import generate_config_from_toml, generate_config_from_xml, normalize_config
|
||||
from find_printers import PrinterScanner
|
||||
from text_console import TextConsole
|
||||
|
||||
|
||||
VERSION = "6.2.12"
|
||||
VERSION = "7.0.0"
|
||||
|
||||
NO_CONF_ERROR = (
|
||||
" Please select a printer model and a valid IP address,"
|
||||
@@ -791,7 +792,7 @@ class EpsonPrinterUI(tk.Tk):
|
||||
row_n += 1
|
||||
tweak_frame = ttk.Frame(main_frame, padding=PAD)
|
||||
tweak_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
|
||||
tweak_frame.columnconfigure((0, 1, 2, 3, 4), weight=1) # expand columns
|
||||
tweak_frame.columnconfigure((0, 1, 2, 3, 4, 5), weight=1) # expand columns
|
||||
|
||||
# Detect Printers
|
||||
self.detect_button = ttk.Button(
|
||||
@@ -815,6 +816,17 @@ class EpsonPrinterUI(tk.Tk):
|
||||
row=0, column=1, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
|
||||
)
|
||||
|
||||
# Print test
|
||||
self.print_tests_button = ttk.Button(
|
||||
tweak_frame,
|
||||
text="Print\nTests",
|
||||
command=self.print_tests,
|
||||
style="Centered.TButton"
|
||||
)
|
||||
self.print_tests_button.grid(
|
||||
row=0, column=2, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
|
||||
)
|
||||
|
||||
# Read EEPROM
|
||||
self.read_eeprom_button = ttk.Button(
|
||||
tweak_frame,
|
||||
@@ -823,7 +835,7 @@ class EpsonPrinterUI(tk.Tk):
|
||||
style="Centered.TButton"
|
||||
)
|
||||
self.read_eeprom_button.grid(
|
||||
row=0, column=2, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
|
||||
row=0, column=3, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
|
||||
)
|
||||
|
||||
# Write EEPROM
|
||||
@@ -834,7 +846,7 @@ class EpsonPrinterUI(tk.Tk):
|
||||
style="Centered.TButton"
|
||||
)
|
||||
self.write_eeprom_button.grid(
|
||||
row=0, column=3, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
|
||||
row=0, column=4, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
|
||||
)
|
||||
|
||||
# Reset Waste Ink Levels
|
||||
@@ -845,7 +857,7 @@ class EpsonPrinterUI(tk.Tk):
|
||||
style="Centered.TButton"
|
||||
)
|
||||
self.reset_button.grid(
|
||||
row=0, column=4, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
|
||||
row=0, column=5, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
|
||||
)
|
||||
|
||||
# [row 4] Status display (including ScrolledText and Treeview)
|
||||
@@ -1262,6 +1274,7 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
ToolTip(self.read_eeprom_button, "")
|
||||
ToolTip(self.detect_configuration_button, "")
|
||||
ToolTip(self.clean_nozzles_button, "")
|
||||
ToolTip(self.print_tests_button, "")
|
||||
ToolTip(self.temp_reset_ink_waste_button, "")
|
||||
ToolTip(self.write_eeprom_button, "")
|
||||
ToolTip(self.reset_button, "")
|
||||
@@ -1277,6 +1290,7 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
self.status_button.state(["disabled"])
|
||||
self.read_eeprom_button.state(["disabled"])
|
||||
self.clean_nozzles_button.state(["disabled"])
|
||||
self.print_tests_button.state(["disabled"])
|
||||
self.temp_reset_ink_waste_button.state(["disabled"])
|
||||
self.detect_configuration_button.state(["disabled"])
|
||||
self.write_eeprom_button.state(["disabled"])
|
||||
@@ -1310,6 +1324,11 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
self.clean_nozzles_button,
|
||||
"Select the printer first."
|
||||
)
|
||||
self.print_tests_button.state(["disabled"])
|
||||
ToolTip(
|
||||
self.print_tests_button,
|
||||
"Select the printer first."
|
||||
)
|
||||
self.detect_configuration_button.state(["disabled"])
|
||||
ToolTip(
|
||||
self.detect_configuration_button,
|
||||
@@ -1325,10 +1344,12 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
self.read_eeprom_button.state(["!disabled"])
|
||||
ToolTip(self.read_eeprom_button, "")
|
||||
self.clean_nozzles_button.state(["!disabled"])
|
||||
self.print_tests_button.state(["!disabled"])
|
||||
self.temp_reset_ink_waste_button.state(["!disabled"])
|
||||
self.detect_configuration_button.state(["!disabled"])
|
||||
ToolTip(self.detect_configuration_button, "")
|
||||
ToolTip(self.clean_nozzles_button, "")
|
||||
ToolTip(self.print_tests_button, "")
|
||||
ToolTip(self.temp_reset_ink_waste_button, "")
|
||||
if "write_key" in self.printer.parm:
|
||||
self.write_eeprom_button.state(["!disabled"])
|
||||
@@ -1410,6 +1431,7 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
self.read_eeprom_button.state(["disabled"])
|
||||
self.detect_configuration_button.state(["disabled"])
|
||||
self.clean_nozzles_button.state(["disabled"])
|
||||
self.print_tests_button.state(["disabled"])
|
||||
self.temp_reset_ink_waste_button.state(["disabled"])
|
||||
self.write_eeprom_button.state(["disabled"])
|
||||
|
||||
@@ -2633,6 +2655,7 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
)
|
||||
self.detect_configuration_button.state(["!disabled"])
|
||||
self.clean_nozzles_button.state(["!disabled"])
|
||||
self.print_tests_button.state(["!disabled"])
|
||||
self.temp_reset_ink_waste_button.state(["!disabled"])
|
||||
self.status_text.insert(tk.END, '[INFO]', "info")
|
||||
self.status_text.insert(
|
||||
@@ -2728,6 +2751,165 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
self.config(cursor="")
|
||||
self.update_idletasks()
|
||||
|
||||
def print_tests(self) -> None:
|
||||
"""
|
||||
Print nozzle, Print color, Print paper pass and Print paper feed tests.
|
||||
"""
|
||||
options = [
|
||||
"Print standard nozzle test", # 0
|
||||
"Print alternative nozzle test", # 1
|
||||
"Color test pattern (for XP-200 range)", # 2
|
||||
"Advance paper of one or n lines", # 3
|
||||
"Feed one or more sheets", # 4
|
||||
]
|
||||
|
||||
def get_test_dialog():
|
||||
dialog = tk.Toplevel(self)
|
||||
dialog.title("Print Test Options")
|
||||
dialog.transient(self)
|
||||
dialog.grab_set()
|
||||
dialog.focus_force()
|
||||
|
||||
# Test selection
|
||||
ttk.Label(dialog, text="Select test:").grid(
|
||||
row=0, column=0, padx=10, pady=(10, 5), sticky="w"
|
||||
)
|
||||
combo_var = tk.StringVar(value=options[0])
|
||||
combo = ttk.Combobox(
|
||||
dialog,
|
||||
textvariable=combo_var,
|
||||
values=options,
|
||||
state="readonly",
|
||||
width=35
|
||||
)
|
||||
combo.current(0)
|
||||
combo.grid(row=0, column=1, padx=10)
|
||||
|
||||
# Number of tests
|
||||
spin_label = ttk.Label(dialog, text="Number of tests:")
|
||||
spin_label.grid(row=1, column=0, padx=10, pady=5, sticky="w")
|
||||
num_tests_var = tk.IntVar(value=1)
|
||||
spin = ttk.Spinbox(
|
||||
dialog, from_=1, to=999, textvariable=num_tests_var, width=5
|
||||
)
|
||||
spin.grid(row=1, column=1, padx=10, pady=10, sticky="w")
|
||||
|
||||
# Disable num test entry for first two tests
|
||||
def on_combo_change(event=None) -> None:
|
||||
if combo.current() < 3:
|
||||
spin.state(["disabled"])
|
||||
spin_label.state(["disabled"])
|
||||
else:
|
||||
spin.state(["!disabled"])
|
||||
spin_label.state(["!disabled"])
|
||||
|
||||
combo.bind('<<ComboboxSelected>>', on_combo_change)
|
||||
on_combo_change()
|
||||
|
||||
# Buttons
|
||||
result: dict[str, tuple[int, int] | None] = {"value": None}
|
||||
def on_confirm(event=None) -> None:
|
||||
idx = combo.current()
|
||||
result["value"] = (idx, num_tests_var.get())
|
||||
dialog.destroy()
|
||||
|
||||
def on_cancel(event=None) -> None:
|
||||
dialog.destroy()
|
||||
|
||||
frame = ttk.Frame(dialog)
|
||||
frame.grid(
|
||||
row=2, column=0, columnspan=2, pady=(5, 10), padx=10, sticky="ew"
|
||||
)
|
||||
frame.columnconfigure((0, 1), weight=1)
|
||||
ttk.Button(frame, text="Confirm", command=on_confirm).grid(
|
||||
row=0, column=0, padx=(0, 5), sticky="ew"
|
||||
)
|
||||
ttk.Button(frame, text="Cancel", command=on_cancel).grid(
|
||||
row=0, column=1, sticky="ew"
|
||||
)
|
||||
|
||||
# Key bindings
|
||||
dialog.bind('<Return>', on_confirm)
|
||||
dialog.bind('<Escape>', on_cancel)
|
||||
|
||||
# Center
|
||||
dialog.update_idletasks()
|
||||
w, h = dialog.winfo_reqwidth(), dialog.winfo_reqheight()
|
||||
x = self.winfo_x() + (self.winfo_width() - w) // 2
|
||||
y = self.winfo_y() + (self.winfo_height() - h) // 2
|
||||
dialog.geometry(f"{w}x{h}+{x}+{y}")
|
||||
|
||||
dialog.wait_window()
|
||||
return result["value"]
|
||||
|
||||
def run_tests(index: int, num_tests: int) -> None:
|
||||
if index == 0:
|
||||
self.printer.check_nozzles(type=0)
|
||||
self.set_cursor(self, '')
|
||||
self.update_idletasks()
|
||||
return
|
||||
if index == 1:
|
||||
self.printer.check_nozzles(type=1)
|
||||
self.set_cursor(self, '')
|
||||
self.update_idletasks()
|
||||
return
|
||||
if index == 2:
|
||||
self.printer.print_test_color_pattern()
|
||||
self.set_cursor(self, '')
|
||||
self.update_idletasks()
|
||||
return
|
||||
try:
|
||||
with LprClient(
|
||||
self.ip_var.get(), port="LPR", label="Print tests"
|
||||
) as client:
|
||||
payload = (
|
||||
client.EXIT_PACKET_MODE
|
||||
+ client.INITIALIZE_PRINTER
|
||||
+ f"{options[index]} for {num_tests} tests\n".encode()
|
||||
+ client.FF
|
||||
)
|
||||
if index == 3:
|
||||
payload = bytes.fromhex("0d 0a") * num_tests
|
||||
if index == 4:
|
||||
payload = (
|
||||
client.INITIALIZE_PRINTER
|
||||
+ bytes.fromhex("0d 0a")
|
||||
+ client.FF
|
||||
+ client.INITIALIZE_PRINTER
|
||||
) * num_tests
|
||||
client.send(payload)
|
||||
except Exception:
|
||||
self.show_status_text_view()
|
||||
self.status_text.insert(
|
||||
tk.END, '[ERROR] Printer unreachable or offline.\n', 'error'
|
||||
)
|
||||
finally:
|
||||
self.set_cursor(self, '')
|
||||
self.update_idletasks()
|
||||
|
||||
ip = self.ip_var.get()
|
||||
if not self._is_valid_ip(ip):
|
||||
self.status_text.insert(
|
||||
tk.END, '[ERROR] Invalid IP address.\n', 'error'
|
||||
)
|
||||
self.set_cursor(self, '')
|
||||
return
|
||||
|
||||
if not self.printer:
|
||||
return
|
||||
|
||||
result = get_test_dialog()
|
||||
if result is None:
|
||||
self.status_text.insert(
|
||||
tk.END, '[WARNING] Print test aborted by user.\n', 'warn'
|
||||
)
|
||||
self.set_cursor(self, '')
|
||||
return
|
||||
|
||||
test_index, num_tests = result
|
||||
self.set_cursor(self, 'watch')
|
||||
self.after(100, lambda: run_tests(test_index, num_tests))
|
||||
|
||||
def clean_nozzles(self):
|
||||
"""
|
||||
Initiates nozzles cleaning routine with optional power clean.
|
||||
@@ -2740,6 +2922,7 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
"Clean all nozzles", # 0
|
||||
"Clean the black ink nozzle", # 1
|
||||
"Clean the color ink nozzles", # 2
|
||||
"Head cleaning (alternative mode)", # 3
|
||||
]
|
||||
|
||||
# Create modal dialog
|
||||
@@ -2784,7 +2967,7 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
|
||||
note = (
|
||||
'The default action is to clean all nozzles.\n'
|
||||
'Other actions might not be supported on your printer.'
|
||||
'The clean black/color ink nozzles might not be supported on your printer.'
|
||||
)
|
||||
tk.Label(
|
||||
dialog,
|
||||
@@ -2859,9 +3042,11 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
dialog.wait_window()
|
||||
return result['value']
|
||||
|
||||
def run_cleaning(group_index, power_clean):
|
||||
def run_cleaning(group_index, power_clean, has_alt_mode=None):
|
||||
try:
|
||||
ret = self.printer.clean_nozzles(group_index, power_clean)
|
||||
ret = self.printer.clean_nozzles(
|
||||
group_index, power_clean, has_alt_mode
|
||||
)
|
||||
except Exception as e:
|
||||
self.status_text.insert(tk.END, '[ERROR]', "error")
|
||||
self.status_text.insert(
|
||||
@@ -2880,7 +3065,9 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
else:
|
||||
self.status_text.insert(tk.END, '[INFO]', "info")
|
||||
self.status_text.insert(tk.END,
|
||||
f" Initiated cleaning of nozzles.\n"
|
||||
f" Initiated cleaning of nozzles."
|
||||
#f" Selected procedure: {group_index}, {power_clean}, {has_alt_mode}"
|
||||
f"\n"
|
||||
)
|
||||
self.set_cursor(self, "")
|
||||
self.update_idletasks()
|
||||
@@ -2912,7 +3099,10 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
|
||||
self.set_cursor(self, "watch")
|
||||
self.update_idletasks()
|
||||
self.after(100, lambda: run_cleaning(group_index, power_clean))
|
||||
self.after(
|
||||
100,
|
||||
lambda: run_cleaning(group_index, power_clean, has_alt_mode=3)
|
||||
)
|
||||
|
||||
def detect_configuration(self, cursor=True):
|
||||
def detect_sequence(eeprom, sequence):
|
||||
@@ -3396,7 +3586,7 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
self.ip_var.get().strip()
|
||||
)
|
||||
if len(printers) > 0:
|
||||
if len(printers) == 1:
|
||||
if len(printers) == 1 and printers[0]['name'] != None:
|
||||
self.status_text.insert(tk.END, '[INFO]', "info")
|
||||
self.status_text.insert(
|
||||
tk.END,
|
||||
@@ -3425,12 +3615,20 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
tk.END, f" Found {len(printers)} printers:\n"
|
||||
)
|
||||
for printer in printers:
|
||||
self.status_text.insert(tk.END, '[INFO]', "info")
|
||||
self.status_text.insert(
|
||||
tk.END,
|
||||
f" {printer['name']} found at {printer['ip']}"
|
||||
f" (hostname: {printer['hostname']})\n",
|
||||
)
|
||||
if printers[0]['name']:
|
||||
self.status_text.insert(tk.END, '[INFO]', "info")
|
||||
self.status_text.insert(
|
||||
tk.END,
|
||||
f" {printer['name']} found at {printer['ip']}"
|
||||
f" (hostname: {printer['hostname']})\n",
|
||||
)
|
||||
else:
|
||||
self.status_text.insert(tk.END, '[WARN]', "warn")
|
||||
self.status_text.insert(
|
||||
tk.END,
|
||||
f" Cannot contact printer {printer['ip']}"
|
||||
f" (hostname: {printer['hostname']}).\n",
|
||||
)
|
||||
else:
|
||||
self.status_text.insert(tk.END, '[WARN]', "warn")
|
||||
self.status_text.insert(tk.END, " No printers found.\n")
|
||||
@@ -3575,7 +3773,7 @@ Web site: https://github.com/Ircama/epson_print_conf
|
||||
)
|
||||
return
|
||||
try:
|
||||
with EpsonLpr(ip_address) as lpr:
|
||||
with LprClient(ip_address, port="LPR", label="Print items") as lpr:
|
||||
lpr.send(
|
||||
lpr.EXIT_PACKET_MODE
|
||||
+ lpr.INITIALIZE_PRINTER
|
||||
|
||||
Reference in New Issue
Block a user