Version 7

This commit is contained in:
Ircama
2025-07-21 09:44:55 +02:00
parent d7159ece79
commit cfaeaf8b0a
5 changed files with 490 additions and 189 deletions

2
.gitignore vendored
View File

@@ -166,3 +166,5 @@ devices.xml
*.pickle
.console_history
lpr_jobs/

View File

@@ -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 Epsons 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

View File

@@ -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 halfwidth
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 halfsplit
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()

View File

@@ -6,3 +6,5 @@ pyperclip
black
tomli
text-console>=2.0.7
hexdump2
pyprintlpr

236
ui.py
View File

@@ -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