Add clean nozzles and revise documentation

Ref. https://github.com/Ircama/epson_print_conf/discussions/57#discussioncomment-13418667
This commit is contained in:
Ircama
2025-06-14 13:37:46 +02:00
parent ccde22a3b0
commit 1edff856e5
3 changed files with 455 additions and 62 deletions

View File

@@ -23,6 +23,14 @@ The software also includes a configurable printer dictionary, which can be easil
- Open the Web interface of the printer (via the default browser). - Open the Web interface of the printer (via the default browser).
- Clean Nozzles.
Performs a standard cleaning cycle on the selected nozzle group.
- Power Clean of the nozzles.
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.
- Temporary reset of the ink waste counter. - 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. 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.
@@ -510,13 +518,23 @@ printer = EpsonPrinter(hostname="192.168.1.87")
pprint.pprint(printer.status_parser(printer.fetch_snmp_values("1.3.6.1.4.1.1248.1.2.2.1.1.1.4.1")[1])) pprint.pprint(printer.status_parser(printer.fetch_snmp_values("1.3.6.1.4.1.1248.1.2.2.1.1.1.4.1")[1]))
``` ```
## END4 EPSON-CTRL commands over SNMP ## EPSON-CTRL commands over SNMP
END4 commands (totally undocumented) might be a limited set of bidirectional remote commands that can be sent without establishing a D4 connection. [Communication between PC and Printer can be done by several transport protocols](https://github.com/lion-simba/reink/blob/master/reink.c#L79C5-L85): ESCP/2, EJL, D4. And in addition SNMP, END4. “D4” (or “Dot 4”) is an abbreviated form of the IEEE-1284.4 specification: it provides a bi-directional, packetized link with multiple logical “sockets”. The two primary Epson-defined channels are:
END4 EPSON-CTRL commands can be converted into OIDs and sent via SNMP. - EPSON-CTRL
Carries printer-control commands, status queries, configuration
- Structure: 2 lowercase letters + length + payload
Also tunneled via END4
- undocumented commands.
Ref. excellent analysis from [ciprian](https://codeberg.org/atufi/reinkpy/issues/12#issuecomment-1660026) and [dger](https://codeberg.org/atufi/reinkpy/issues/12#issuecomment-1661250). - EPSON-DATA
Carries the actual print-job content: raster image streams, font/download data, macros, etc.
- Allow "Remote Mode" commands, entered and terminated via a special sequence (`ESC (R BC=8 00 R E M O T E 1`, `ESC 00 00 00`); [remote mode commands](https://gimp-print.sourceforge.io/reference-html/x952.html) are partially documented and have a similar structure as EPSON-CTRL (2 letters + length + payload), but the letters are uppercase and cannot be mapped to SNMP.
EPSON-CTRL can be transported over D4, or encapsulated in SNMP OIDs. Some EPSON-CTRL instructions implement a subset of Epsons Remote Mode protocol, while others are proprietary.
END4 is a proprietary protocol to transport EPSON-CTRL commands [over the standard print channel](https://codeberg.org/atufi/reinkpy/issues/12#issuecomment-1660026), without using the EPSON-CTRL channel.
OID Header: OID Header:
@@ -532,13 +550,11 @@ Full OID header sequence: `1.3.6.1.4.1.1248.1.2.2.44.1.1.2.1.`
Subsequent digits: Subsequent digits:
- Two ASCII characters that identify the command (e.g., "st", "ex"). These are command identifiers of the END4 EPSON-CTRL messages (Remote Mode) - Two ASCII characters that identify the command (e.g., "st", "ex"). These are command identifiers of the EPSON-CTRL messages (Remote Mode)
- 2-byte little-endian length field (gives the number of bytes in the parameter section that follows) - 2-byte little-endian length field (gives the number of bytes in the parameter section that follows)
- payload (a block of bytes that are specific to the command). - payload (a block of bytes that are specific to the command).
END4 commands partially overlap with Epsons Remote Mode bi-directional printer-control language, though they are not strictly equivalent. 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)). Some END4 instructions implement subsets of Epsons Remote Mode protocol, while others are proprietary extensions and lie outside Epsons published command set. The following is the list of EPSON-CTRL commands supported by the XP-205.
The following is the list of END4 commands supported by the XP-205.
Two-bytes|Description | Notes | Parameters Two-bytes|Description | Notes | Parameters
:--:| ---------------------------------------------- | ----------------| ------------- :--:| ---------------------------------------------- | ----------------| -------------
@@ -560,8 +576,8 @@ pm | Select control language ("PM" 02H 00H 00H m1m1=0(ESC/P), 2(IBM 238x Plus em
rj | Resume jobs (?) | | rj | Resume jobs (?) | |
rp | (serial number ? ) | | (0) rp | (serial number ? ) | | (0)
rs | Initialize | | (1) rs | Initialize | | (1)
rw | Reset Waste | Implemented in this program | (1, 0) + Serial SHA1 hash (20 bytes) rw | Reset Waste | Implemented in this program | (1, 0) + [Serial SHA1 hash](https://codeberg.org/atufi/reinkpy/issues/12#issuecomment-1661250) (20 bytes)
st | Get printer status ("st" 01H 00H 01H) | Implemented in this program | (1) st | Get printer status ("st" 01H 00H 01H) | Implemented in this program; se below "ST2 Status Reply Codes" | (1)
ti | Set printer time | (" TI" 08H 00H 00H YYYY MM DD hh mm ss) | ti | Set printer time | (" TI" 08H 00H 00H YYYY MM DD hh mm ss) |
vi | Version Information | Implemented in this program | (0) vi | Version Information | Implemented in this program | (0)
xi | | | (1) xi | | | (1)
@@ -574,8 +590,8 @@ xi | | | (1)
- 7.0: Two-byte payload length = 7 bytes - 7.0: Two-byte payload length = 7 bytes
- two bytes for the read key - two bytes for the read key
- 65: 'A' = read - 65: 'A' = read
- 190: Take the bitwise NOT of the ASCII value of 'A' = read, then mask to the lowest 8 bits. The result is 190. - 190: [Take the bitwise NOT of the ASCII value of 'A' = read, then mask to the lowest 8 bits](https://github.com/lion-simba/reink/blob/master/reink.c#L1414). The result is 190.
- 160: Shift the ASCII value of 'A' (read) right by 1 and mask to 7 bits, then OR it with the highest bit of the value shifted left by 7. The result is 160. - 160: [Shift the ASCII value of 'A' (read) right by 1 and mask to 7 bits, then OR it with the highest bit of the value shifted left by 7](https://github.com/lion-simba/reink/blob/master/reink.c#L1415). The result is 160.
- two bytes for the EEPROM address - two bytes for the EEPROM address
``` ```
@@ -625,7 +641,7 @@ AC = Value
#### epctrl_snmp_oid() #### epctrl_snmp_oid()
`self.epctrl_snmp_oid(two-char-command, payload)` converts an END4 EPSON-CTRL Remote command into a SNMP OID format suitable for use in SNMP operations. `self.epctrl_snmp_oid(two-char-command, payload)` converts an EPSON-CTRL Remote command into a SNMP OID format suitable for use in SNMP operations.
**Parameters** **Parameters**
@@ -651,7 +667,7 @@ It returns a SNMP OID string to be used by `self.printer.fetch_oid_values()`.
To return the value of the OID query: `self.fetch_oid_values(oid)[0][1]`. To return the value of the OID query: `self.fetch_oid_values(oid)[0][1]`.
### Testing END4 remote commands ### Testing EPSON-CTRL commands
Open the *epson_print_conf* application, set printer model and IP address, test printer connection. Then: Settings > Debug Shell. Open the *epson_print_conf* application, set printer model and IP address, test printer connection. Then: Settings > Debug Shell.
@@ -712,29 +728,11 @@ for i in ec_sequences:
print(r) print(r)
``` ```
Examples of commands ("CH" = Clean Heads and "NC" = Nozzle Check) that are not included in the END4 set, so they have to be delivered via TCP (port 9100 or port 515) and cannot be mapped to SNMP. ## Remote Mode commands
- CH BC=2 00 xx Perform a head cleaning cycle. "00" cleans all heads 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)).
| Name | Bytes (hex) | Purpose |
| ----------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| EXIT\_PACKET\_MODE | `00 00 00 1B 01 40 45 4A 4C 20 31 32 38 34 2E 34 0A 40 45 4A 4C` | Exit packet mode. |
| ENTER\_REMOTE\_MODE | `1B 40 1B 40 1B 28 52 08 00 00 52 45 4D 4F 54 45 31` | Put the device into "remote" control mode. |
| SET\_CLOCK | `54 49 08 00 00 07 E9 06 08 17 0E 22` | "TI": Write the internal real-time clock to 2025-06-08 17:14:34. |
| CLEAN\_HEADS | `43 48 02 00 00 00` | "CH": trigger print-head cleaning cycle. |
| EXIT\_REMOTE\_MODE | `1B 00 00 00` | Exit remote control mode. |
| JOB\_END | `4A 45 01 00 00` | Finalize the maintenance job. |
- NC BC=2 00 00 Print a nozzle check pattern
| Name | Bytes (hex) | Purpose |
| ------------------------ | ---------------------------------------- | -------------------------------------------------------------- |
| EXIT\_PACKET\_MODE | `00 00 00 1B 01 40 45 4A 4C 20 31 32 38 34 2E 34 0A 40 45 4A 4C` | Exit packet mode. |
| ENTER\_REMOTE\_MODE | `1B 40 1B 40 1B 28 52 08 00 00 52 45 4D 4F 54 45 31` | Put the device into "remote" control mode. |
| PRINT\_NOZZLE\_CHECK | `4E 43 02 00 00 00` | "NC": issue nozzle-check print pattern. |
| EXIT\_REMOTE\_MODE | `1B 00 00 00` | Exit remote control mode. |
| JOB\_END | `4A 45 01 00 00` | Finalize the maintenance job. |
Check `self.printer.check_nozzles()` and `self.printer.clean_nozzles(0)` for examples of usage of remote commands.
## ST2 Status Reply Codes ## ST2 Status Reply Codes
@@ -983,12 +981,12 @@ snmpget -v1 -d -c public 192.168.1.87 1.3.6.1.4.1.1248.1.2.2.44.1.1.2.1.124.124.
### References ### References
ReInk: <https://github.com/lion-simba/reink> (especially <https://github.com/lion-simba/reink/issues/1>)
epson-printer-snmp: <https://github.com/Zedeldi/epson-printer-snmp> (and <https://github.com/Zedeldi/epson-printer-snmp/issues/1>) epson-printer-snmp: <https://github.com/Zedeldi/epson-printer-snmp> (and <https://github.com/Zedeldi/epson-printer-snmp/issues/1>)
ReInkPy: <https://codeberg.org/atufi/reinkpy/> ReInkPy: <https://codeberg.org/atufi/reinkpy/>
ReInk: <https://github.com/lion-simba/reink> (especially <https://github.com/lion-simba/reink/issues/1>)
reink-net: <https://github.com/gentu/reink-net> reink-net: <https://github.com/gentu/reink-net>
epson-l4160-ink-waste-resetter: <https://github.com/nicootto/epson-l4160-ink-waste-resetter> epson-l4160-ink-waste-resetter: <https://github.com/nicootto/epson-l4160-ink-waste-resetter>
@@ -997,6 +995,8 @@ epson-l3160-ink-waste-resetter: <https://github.com/k3dt/epson-l3160-ink-waste-r
emanage x900: <https://github.com/abrasive/x900-otsakupuhastajat/> emanage x900: <https://github.com/abrasive/x900-otsakupuhastajat/>
Reversing Epson printers: <https://github.com/abrasive/epson-reversing/>
### Other programs ### Other programs
- Epson One-Time Maintenance Ink Pad Reset Utility: <https://epson.com/Support/wa00369> - Epson One-Time Maintenance Ink Pad Reset Utility: <https://epson.com/Support/wa00369>

View File

@@ -21,6 +21,7 @@ import pickle
import abc import abc
import hashlib import hashlib
import struct import struct
import socket
from pysnmp.hlapi.v1arch.asyncio import * from pysnmp.hlapi.v1arch.asyncio import *
from pyasn1.type.univ import OctetString as OctetStringType from pyasn1.type.univ import OctetString as OctetStringType
@@ -33,6 +34,72 @@ from pysnmp_sync_adapter import (
from pysnmp.proto.errind import RequestTimedOut 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.EXIT_PACKET_MODE = (
b'\x00\x00\x00\x1b\x01@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 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))
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
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)
def remote_cmd(self, cmd, args):
"Generate a Remote Mode command."
if len(cmd) != 2:
raise ValueError("command should be 2 bytes")
return cmd.encode() + struct.pack('<H', len(args)) + args
class EpsonPrinter: class EpsonPrinter:
"""SNMP Epson Printer Configuration.""" """SNMP Epson Printer Configuration."""
@@ -2510,6 +2577,71 @@ class EpsonPrinter:
return False return False
return True return True
def check_nozzles(self):
"""
Print nozzle-check pattern.
"""
if not self.hostname:
return None
status = True
lpr = EpsonLpr(self.hostname)
# Sequence list
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
lpr.EXIT_REMOTE_MODE, # Disengage remote control
lpr.JOB_END # Mark maintenance job complete
]
try:
lpr.connect()
resp = lpr.send(b"".join(commands))
except Exception as e:
status = False
finally:
lpr.disconnect()
return status
def clean_nozzles(self, group_index, power_clean=False):
"""
Initiates nozzles cleaning routine with optional power clean.
"""
if not self.hostname:
return None
if 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])
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
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.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
]
try:
lpr.connect()
resp = lpr.send(b"".join(commands))
except Exception as e:
status = False
finally:
lpr.disconnect()
return status
def write_first_ti_received_time( def write_first_ti_received_time(
self, year: int, month: int, day: int) -> bool: self, year: int, month: int, day: int) -> bool:
"""Update first TI received time""" """Update first TI received time"""

311
ui.py
View File

@@ -37,7 +37,7 @@ from find_printers import PrinterScanner
from text_console import TextConsole from text_console import TextConsole
VERSION = "6.0.2" VERSION = "6.1.0"
NO_CONF_ERROR = ( NO_CONF_ERROR = (
" Please select a printer model and a valid IP address," " Please select a printer model and a valid IP address,"
@@ -45,7 +45,7 @@ NO_CONF_ERROR = (
) )
CONFIRM_MESSAGE = ( CONFIRM_MESSAGE = (
"Confirm Action", "EEPROM update - Confirm Action",
"Please copy and save the codes in the [NOTE] shown on the screen." "Please copy and save the codes in the [NOTE] shown on the screen."
" They can be used to restore the initial configuration" " They can be used to restore the initial configuration"
" in case of problems.\n\n" " in case of problems.\n\n"
@@ -723,7 +723,7 @@ class EpsonPrinterUI(tk.Tk):
row_n += 1 row_n += 1
button_frame = ttk.Frame(main_frame, padding=PAD) button_frame = ttk.Frame(main_frame, padding=PAD)
button_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E)) button_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
button_frame.columnconfigure((0, 1, 2, 3), weight=1) # expand columns button_frame.columnconfigure((0, 1, 2, 3, 4), weight=1) # expand columns
# Query Printer Status # Query Printer Status
self.status_button = ttk.Button( self.status_button = ttk.Button(
@@ -746,6 +746,17 @@ class EpsonPrinterUI(tk.Tk):
row=0, column=1, padx=PADX, pady=PADX, sticky=(tk.W, tk.E) row=0, column=1, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
) )
# Clean nozzles
self.clean_nozzles_button = ttk.Button(
button_frame,
text="Clean\nNozzles",
command=self.clean_nozzles,
style="Centered.TButton"
)
self.clean_nozzles_button.grid(
row=0, column=2, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
# Detect configuration values # Detect configuration values
self.detect_configuration_button = ttk.Button( self.detect_configuration_button = ttk.Button(
button_frame, button_frame,
@@ -754,18 +765,18 @@ class EpsonPrinterUI(tk.Tk):
style="Centered.TButton" style="Centered.TButton"
) )
self.detect_configuration_button.grid( self.detect_configuration_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)
) )
# Temporary Reset Waste Ink Levels # Temporary Reset Waste Ink Levels
self.detect_configuration_button = ttk.Button( self.temp_reset_ink_waste_button = ttk.Button(
button_frame, button_frame,
text="Temporary Reset\nWaste Ink Levels", text="Temporary Reset\nWaste Ink Levels",
command=self.temp_reset_waste_ink, command=self.temp_reset_waste_ink,
style="Centered.TButton" style="Centered.TButton"
) )
self.detect_configuration_button.grid( self.temp_reset_ink_waste_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)
) )
# [row 4] Tweak Buttons # [row 4] Tweak Buttons
@@ -984,9 +995,10 @@ class EpsonPrinterUI(tk.Tk):
) )
if not file_path: if not file_path:
self.show_status_text_view() self.show_status_text_view()
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, tk.END,
f"[WARNING] File save operation aborted.\n" f" File save operation aborted.\n"
) )
return return
# Ensure the file has the desired extension # Ensure the file has the desired extension
@@ -1025,9 +1037,10 @@ class EpsonPrinterUI(tk.Tk):
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
self.show_status_text_view() self.show_status_text_view()
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, tk.END,
f"[WARNING] File load operation aborted.\n" f" File load operation aborted.\n"
) )
return return
if type == 0: if type == 0:
@@ -1225,6 +1238,8 @@ Web site: https://github.com/Ircama/epson_print_conf
ToolTip(self.set_mac_addr, "") ToolTip(self.set_mac_addr, "")
ToolTip(self.read_eeprom_button, "") ToolTip(self.read_eeprom_button, "")
ToolTip(self.detect_configuration_button, "") ToolTip(self.detect_configuration_button, "")
ToolTip(self.clean_nozzles_button, "")
ToolTip(self.temp_reset_ink_waste_button, "")
ToolTip(self.write_eeprom_button, "") ToolTip(self.write_eeprom_button, "")
ToolTip(self.reset_button, "") ToolTip(self.reset_button, "")
if self.ip_var.get(): if self.ip_var.get():
@@ -1238,6 +1253,8 @@ Web site: https://github.com/Ircama/epson_print_conf
self.reset_button.state(["disabled"]) self.reset_button.state(["disabled"])
self.status_button.state(["disabled"]) self.status_button.state(["disabled"])
self.read_eeprom_button.state(["disabled"]) self.read_eeprom_button.state(["disabled"])
self.clean_nozzles_button.state(["disabled"])
self.temp_reset_ink_waste_button.state(["disabled"])
self.detect_configuration_button.state(["disabled"]) self.detect_configuration_button.state(["disabled"])
self.write_eeprom_button.state(["disabled"]) self.write_eeprom_button.state(["disabled"])
self.web_interface_button.state(["disabled"]) self.web_interface_button.state(["disabled"])
@@ -1260,10 +1277,20 @@ Web site: https://github.com/Ircama/epson_print_conf
self.read_eeprom_button, self.read_eeprom_button,
"Feature not defined in the printer configuration." "Feature not defined in the printer configuration."
) )
self.temp_reset_ink_waste_button.state(["disabled"])
ToolTip(
self.temp_reset_ink_waste_button,
"Select the printer first."
)
self.clean_nozzles_button.state(["disabled"])
ToolTip(
self.clean_nozzles_button,
"Select the printer first."
)
self.detect_configuration_button.state(["disabled"]) self.detect_configuration_button.state(["disabled"])
ToolTip( ToolTip(
self.detect_configuration_button, self.detect_configuration_button,
"Feature not defined in the printer configuration." "Select the printer first."
) )
self.write_eeprom_button.state(["disabled"]) self.write_eeprom_button.state(["disabled"])
ToolTip( ToolTip(
@@ -1274,8 +1301,12 @@ Web site: https://github.com/Ircama/epson_print_conf
if "read_key" in self.printer.parm: if "read_key" in self.printer.parm:
self.read_eeprom_button.state(["!disabled"]) self.read_eeprom_button.state(["!disabled"])
ToolTip(self.read_eeprom_button, "") ToolTip(self.read_eeprom_button, "")
self.clean_nozzles_button.state(["!disabled"])
self.temp_reset_ink_waste_button.state(["!disabled"])
self.detect_configuration_button.state(["!disabled"]) self.detect_configuration_button.state(["!disabled"])
ToolTip(self.detect_configuration_button, "") ToolTip(self.detect_configuration_button, "")
ToolTip(self.clean_nozzles_button, "")
ToolTip(self.temp_reset_ink_waste_button, "")
if "write_key" in self.printer.parm: if "write_key" in self.printer.parm:
self.write_eeprom_button.state(["!disabled"]) self.write_eeprom_button.state(["!disabled"])
ToolTip( ToolTip(
@@ -1355,6 +1386,8 @@ Web site: https://github.com/Ircama/epson_print_conf
self.status_button.state(["disabled"]) self.status_button.state(["disabled"])
self.read_eeprom_button.state(["disabled"]) self.read_eeprom_button.state(["disabled"])
self.detect_configuration_button.state(["disabled"]) self.detect_configuration_button.state(["disabled"])
self.clean_nozzles_button.state(["disabled"])
self.temp_reset_ink_waste_button.state(["disabled"])
self.write_eeprom_button.state(["disabled"]) self.write_eeprom_button.state(["disabled"])
self.po_timer_entry.state(["disabled"]) self.po_timer_entry.state(["disabled"])
@@ -1642,8 +1675,9 @@ Web site: https://github.com/Ircama/epson_print_conf
except Exception as e: except Exception as e:
self.handle_printer_error(e) self.handle_printer_error(e)
else: else:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, f"[WARNING] Set Power off timer aborted.\n" tk.END, f" Set Power off timer aborted.\n"
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
@@ -1684,8 +1718,9 @@ Web site: https://github.com/Ircama/epson_print_conf
"if you are very sure of what you do.\n\n", "if you are very sure of what you do.\n\n",
default='no') default='no')
if not response: if not response:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, "[WARNING] Operation aborted.\n" tk.END, " Operation aborted.\n"
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
@@ -1710,8 +1745,9 @@ Web site: https://github.com/Ircama/epson_print_conf
) )
response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no') response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no')
if not response: if not response:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, "[WARNING] Operation aborted.\n" tk.END, " Operation aborted.\n"
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
@@ -1797,8 +1833,9 @@ Web site: https://github.com/Ircama/epson_print_conf
) )
response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no') response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no')
if not response: if not response:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, "[WARNING] Operation aborted.\n" tk.END, " Operation aborted.\n"
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
@@ -1943,9 +1980,10 @@ Web site: https://github.com/Ircama/epson_print_conf
except Exception as e: except Exception as e:
self.handle_printer_error(e) self.handle_printer_error(e)
else: else:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, tk.END,
f"[WARNING] Change of 'First TI received time' aborted.\n", f" Change of 'First TI received time' aborted.\n",
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
@@ -2571,6 +2609,8 @@ Web site: https://github.com/Ircama/epson_print_conf
f'{self.printer.PRINTER_CONFIG[DETECTED]}.\n' f'{self.printer.PRINTER_CONFIG[DETECTED]}.\n'
) )
self.detect_configuration_button.state(["!disabled"]) self.detect_configuration_button.state(["!disabled"])
self.clean_nozzles_button.state(["!disabled"])
self.temp_reset_ink_waste_button.state(["!disabled"])
self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert(tk.END, '[INFO]', "info")
self.status_text.insert( self.status_text.insert(
tk.END, " Detect operation completed.\n" tk.END, " Detect operation completed.\n"
@@ -2595,7 +2635,7 @@ Web site: https://github.com/Ircama/epson_print_conf
self.status_text.insert(tk.END, NO_CONF_ERROR) self.status_text.insert(tk.END, NO_CONF_ERROR)
return return
response = messagebox.askyesno( response = messagebox.askyesno(
"Confirm Action", "Detect Access Keys - Confirm Action",
"Warning: this is a brute force operation, which takes several\n" "Warning: this is a brute force operation, which takes several\n"
"minutes to complete.\n\n" "minutes to complete.\n\n"
"Results will be shown in the status box.\n\n" "Results will be shown in the status box.\n\n"
@@ -2614,12 +2654,18 @@ Web site: https://github.com/Ircama/epson_print_conf
self.update() self.update()
self.after(100, lambda: run_detection()) self.after(100, lambda: run_detection())
else: else:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, f"[WARNING] Detect access key aborted.\n" tk.END, f" Detect access key aborted.\n"
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
def set_cursor(self, widget, cursor_type):
widget.config(cursor=cursor_type)
for child in widget.winfo_children():
self.set_cursor(child, cursor_type)
def web_interface(self, cursor=True): def web_interface(self, cursor=True):
if cursor: if cursor:
self.config(cursor="watch") self.config(cursor="watch")
@@ -2659,6 +2705,195 @@ Web site: https://github.com/Ircama/epson_print_conf
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
def clean_nozzles(self):
"""
Initiates nozzles cleaning routine with optional power clean.
Displays a dialog to select a nozzle group and power clean option.
"""
def show_clean_dialog():
# Define groups
groups = [
"Clean all nozzles",
"Cyan + Vivid Magenta",
"Photo Black + Matte Black + Light Black",
"Orange + Green",
"Light Light Black + Yellow",
"Vivid Light Magenta + Light Cyan"
]
# Create modal dialog
dialog = tk.Toplevel(self)
dialog.title("Clean Nozzles Options")
dialog.transient(self)
dialog.grab_set()
dialog.focus_force()
introduction = (
'The printer performs nozzles cleaning by flushing excess ink '
'through the nozzles.'
)
tk.Label(
dialog,
text=introduction,
wraplength=400,
justify='left',
foreground='gray30'
).pack(padx=10)
# Label
tk.Label(dialog, text="Select Nozzle Group:").pack(
padx=10, pady=(10, 0)
)
# Compute width in characters for combobox
max_len = max(len(item) for item in groups)
combo_var = tk.StringVar()
combo = ttk.Combobox(
dialog,
textvariable=combo_var,
values=groups,
state="readonly",
width=max_len
)
combo.current(0)
combo.configure(justify='center') # Center the displayed text in the combobox
combo.pack(padx=10, pady=5)
combo.pack(padx=10, pady=5)
combo.focus_set()
note = (
'The default action is to clean all nozzles.\n'
'Other actions might not be supported on your printer.'
)
tk.Label(
dialog,
text=note,
wraplength=400,
justify='left',
foreground='gray30'
).pack(padx=10, pady=(10, 5))
# Checkbutton for power clean
power_var = tk.BooleanVar(value=False)
chk = ttk.Checkbutton(
dialog, text="Power Clean", variable=power_var
)
chk.pack(padx=10, pady=5)
# Warning message for power clean ink usage
warning_text = (
"Power Clean uses a significant amount of ink "
"to flush the nozzles, "
"and more rapidly fills the internal waste ink tank, "
"which collects "
"the excess ink used during the cleaning process."
)
msg = tk.Message(
dialog,
text=warning_text, width=(max_len+2)*8,
foreground='gray30'
)
msg.pack(padx=10, pady=(2, 5))
# Container for buttons
btn_frame = ttk.Frame(dialog)
btn_frame.pack(padx=10, pady=(5, 10), fill=tk.X)
btn_frame.columnconfigure((0, 1), weight=1)
result = {'value': None}
def on_confirm(event=None):
sel = combo.current()
if sel < 0:
return
result['value'] = (sel, power_var.get())
dialog.destroy()
def on_cancel(event=None):
dialog.destroy()
# Confirm and Cancel buttons
confirm_btn = ttk.Button(
btn_frame, text="Confirm", command=on_confirm
)
cancel_btn = ttk.Button(
btn_frame, text="Cancel", command=on_cancel
)
confirm_btn.grid(row=0, column=0, sticky=tk.EW, padx=(0, 5))
cancel_btn.grid(row=0, column=1, sticky=tk.EW)
# Highlight Cancel as default and bind keys
cancel_btn.focus_set()
dialog.bind('<Return>', on_confirm)
dialog.bind('<Escape>', on_cancel)
# Center dialog
dialog.update_idletasks()
w = dialog.winfo_reqwidth()
h = 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_cleaning(group_index, power_clean):
try:
ret = self.printer.clean_nozzles(group_index, power_clean)
except Exception as e:
self.status_text.insert(tk.END, '[ERROR]', "error")
self.status_text.insert(
tk.END, f" Clean nozzless failure: {e}\n"
)
if ret is None:
self.status_text.insert(tk.END, '[ERROR]', "error")
self.status_text.insert(
tk.END, f" clean_nozzles internal error.\n"
)
elif ret is False:
self.status_text.insert(tk.END, '[ERROR]', "error")
self.status_text.insert(
tk.END, f" Printer is unreachable or offline.\n"
)
else:
self.status_text.insert(tk.END, '[INFO]', "info")
self.status_text.insert(tk.END,
f" Initiated cleaning of nozzles.\n"
)
self.set_cursor(self, "")
self.update_idletasks()
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
self.status_text.insert(tk.END, '[ERROR]', "error")
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.set_cursor(self, "")
self.update_idletasks()
return
if not self.printer:
return
# Call the dialog
selection = show_clean_dialog()
if selection is None:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert(
tk.END,
f" Nozzles cleaning operation aborted.\n"
)
self.set_cursor(self, "")
self.update_idletasks()
return # User cancelled
group_index, power_clean = selection
self.set_cursor(self, "watch")
self.update_idletasks()
self.after(100, lambda: run_cleaning(group_index, power_clean))
def detect_configuration(self, cursor=True): def detect_configuration(self, cursor=True):
def detect_sequence(eeprom, sequence): def detect_sequence(eeprom, sequence):
seq_len = len(sequence) seq_len = len(sequence)
@@ -2928,8 +3163,9 @@ Web site: https://github.com/Ircama/epson_print_conf
self.update() self.update()
self.after(200, lambda: write_eeprom_values(dict_addr_val)) self.after(200, lambda: write_eeprom_values(dict_addr_val))
else: else:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, f"[WARNING] Write EEPROM aborted.\n" tk.END, f" Write EEPROM aborted.\n"
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
@@ -2974,6 +3210,24 @@ Web site: https://github.com/Ircama/epson_print_conf
method_to_call = getattr(self, current_function_name) method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False)) self.after(100, lambda: method_to_call(cursor=False))
return return
msg = (
"Reset Waste Ink Levels - Confirm Action",
"This feature permanently resets the ink waste tank full counters."
"\n\nAlways replace the waste ink pads before "
"continuing. Carefully monitor the ink flow and "
"consider risks of ink overflow into printer internals and "
"also environmental contamination that possibly cannot be cleaned."
"\n\nAre you sure you want to proceed?"
)
response = messagebox.askyesno(*msg, default='no')
if not response:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert(
tk.END, f" Waste ink levels reset aborted.\n"
)
self.config(cursor="")
self.update_idletasks()
return
self.show_status_text_view() self.show_status_text_view()
ip_address = self.ip_var.get() ip_address = self.ip_var.get()
if ( if (
@@ -3026,8 +3280,9 @@ Web site: https://github.com/Ircama/epson_print_conf
except Exception as e: except Exception as e:
self.handle_printer_error(e) self.handle_printer_error(e)
else: else:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, f"[WARNING] Waste ink levels reset aborted.\n" tk.END, f" Waste ink levels reset aborted.\n"
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()
@@ -3057,10 +3312,14 @@ Web site: https://github.com/Ircama/epson_print_conf
if not self.printer: if not self.printer:
return return
msg = ( msg = (
"Confirm Action", "Temporary Bypass of the Waste Ink Lock - Confirm Action",
"This feature temporarily bypasses the ink waste tank full warning," "This feature temporarily bypasses the ink waste tank full"
" which would otherwise disable printing. " " message, which would otherwise disable printing. "
"\n\nThis setting does not persist a reboot. " "\n\nThis setting does not persist a reboot. "
"\n\nAlways replace the waste ink pads before "
"continuing. Carefully monitor the ink flow and "
"consider risks of ink overflow into printer internals and "
"also environmental contamination that possibly cannot be cleaned."
"\n\nAre you sure you want to proceed?" "\n\nAre you sure you want to proceed?"
) )
response = messagebox.askyesno(*msg, default='no') response = messagebox.askyesno(*msg, default='no')
@@ -3070,21 +3329,23 @@ Web site: https://github.com/Ircama/epson_print_conf
self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert(tk.END, '[INFO]', "info")
self.status_text.insert( self.status_text.insert(
tk.END, tk.END,
" Waste ink levels have been temporarily reset." " Waste ink levels have been temporarily bypassed."
" You can now print.\n" " You can now print.\n"
) )
else: else:
self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, '[ERROR]', "error")
self.status_text.insert( self.status_text.insert(
tk.END, tk.END,
" Failed to perform the temporary reset of the " " Failed to perform the temporary bypass of the "
"waste ink levels." "waste ink levels."
) )
except Exception as e: except Exception as e:
self.handle_printer_error(e) self.handle_printer_error(e)
else: else:
self.status_text.insert(tk.END, '[WARNING]', "warn")
self.status_text.insert( self.status_text.insert(
tk.END, f"[WARNING] Waste ink levels reset aborted.\n" tk.END,
" Temporary bypass of the waste ink levels aborted.\n"
) )
self.config(cursor="") self.config(cursor="")
self.update_idletasks() self.update_idletasks()