epson_print_conf/ui.py

1971 lines
71 KiB
Python
Raw Permalink Normal View History

2024-09-18 22:49:14 -04:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Epson Printer Configuration via SNMP (TCP/IP) - GUI
"""
2024-08-08 20:41:19 -04:00
import sys
2024-07-27 07:53:03 -04:00
import re
import threading
import ipaddress
2024-07-28 13:15:10 -04:00
import inspect
2024-07-27 07:53:03 -04:00
from datetime import datetime
2024-09-18 22:49:14 -04:00
import socket
2024-09-20 02:35:28 -04:00
import traceback
import logging
import webbrowser
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
import black
import tkinter as tk
2024-07-25 16:08:05 -04:00
from tkinter import ttk, Menu
from tkinter.scrolledtext import ScrolledText
2024-07-25 16:08:05 -04:00
import tkinter.font as tkfont
2024-07-27 07:53:03 -04:00
from tkcalendar import DateEntry # Ensure you have: pip install tkcalendar
from tkinter import simpledialog, messagebox
2024-07-25 16:08:05 -04:00
2024-07-27 10:36:43 -04:00
import pyperclip
2024-07-27 07:53:03 -04:00
from epson_print_conf import EpsonPrinter
from find_printers import PrinterScanner
VERSION = "3.0"
2024-07-28 13:15:10 -04:00
NO_CONF_ERROR = (
2024-09-18 22:49:14 -04:00
"[ERROR] Please select a printer model and a valid IP address,"
" or press 'Detect Printers'.\n"
2024-07-28 13:15:10 -04:00
)
CONFIRM_MESSAGE = (
"Confirm Action",
"Please copy and save the codes in the [NOTE] shown on the screen."
" They can be used to restore the initial configuration"
" in case of problems.\n\n"
"Are you sure you want to proceed?"
)
2024-07-28 13:15:10 -04:00
def get_printer_models(input_string):
# Tokenize the string
tokens = re.split(" |/", input_string)
if not len(tokens):
return []
# Define the words to remove (uppercase, then case insensitive)
remove_tokens = {"EPSON", "SERIES"}
# Process tokens
processed_tokens = []
non_numeric_part = ""
pre_model = ""
for token in tokens:
upper_token = token.upper()
# Remove tokens that match remove_tokens
if any(word == upper_token for word in remove_tokens):
continue
if not any(char.isdigit() for char in token): # no alphanum inside
pre_model = pre_model + token + " "
continue
# Identify the non-numeric part of the first token
if not token.isnumeric() and not non_numeric_part:
non_numeric_part = "".join(c for c in token if not c.isdigit())
# if token is numeric, prepend the non-numeric part
if token.isnumeric():
processed_tokens.append(f"{pre_model}{non_numeric_part}{token}")
else:
processed_tokens.append(f"{pre_model}{token}")
if not processed_tokens and pre_model:
processed_tokens.append(pre_model.strip())
return processed_tokens
class MultiLineInputDialog(simpledialog.Dialog):
def __init__(self, parent, title=None, text=""):
self.text=text
super().__init__(parent, title)
def body(self, frame):
# Add a label with instructions
self.label = tk.Label(frame, text=self.text)
self.label.pack(pady=5)
# Create a Text widget for multiline input
self.textbox = tk.Text(frame, height=5, width=50)
self.textbox.configure(font=("TkDefaultFont"))
self.textbox.pack()
return self.textbox
def apply(self):
# Get the input from the Text widget
self.result = self.textbox.get("1.0", tk.END).strip()
2024-07-27 10:36:43 -04:00
class ToolTip:
2024-09-18 22:49:14 -04:00
def __init__(
self,
widget,
text="widget info",
wrap_length=10,
destroy=True
):
2024-07-27 10:36:43 -04:00
self.widget = widget
self.text = text
self.wrap_length = wrap_length
self.tooltip_window = None
2024-09-18 22:49:14 -04:00
# Check and remove existing bindings if they exist
if destroy:
self.remove_existing_binding("<Enter>")
self.remove_existing_binding("<Leave>")
self.remove_existing_binding("<Button-1>")
# Set new bindings
2024-07-28 13:15:10 -04:00
widget.bind("<Enter>", self.enter, "+") # Show the tooltip on hover
widget.bind("<Leave>", self.leave, "+") # Hide the tooltip on leave
widget.bind("<Button-1>", self.leave, "+") # Hide tooltip on mouse click
2024-09-18 22:49:14 -04:00
def remove_existing_binding(self, event):
# Check if there's already a binding for the event
if self.widget.bind(event):
self.widget.unbind(event) # Remove the existing binding
2024-07-27 10:36:43 -04:00
def enter(self, event=None):
if self.tooltip_window or not self.text:
return
x, y, width, height = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 20
y += self.widget.winfo_rooty() + 20
self.tooltip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
# Calculate the position for the tooltip
screen_width = self.widget.winfo_screenwidth()
screen_height = self.widget.winfo_screenheight()
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
tw.geometry(f"+{x}+{y + height + 2}") # Default position below the widget
2024-07-28 13:15:10 -04:00
label = tk.Label(
tw,
text=self.wrap_text(self.text),
justify="left",
background="LightYellow",
relief="solid",
borderwidth=1,
)
2024-07-27 10:36:43 -04:00
label.pack(ipadx=1)
# Check if the tooltip goes off the screen
tw.update_idletasks() # Ensures the tooltip size is calculated
tw_width = tw.winfo_width()
tw_height = tw.winfo_height()
2024-07-28 13:15:10 -04:00
2024-07-27 10:36:43 -04:00
if x + tw_width > screen_width: # If tooltip goes beyond screen width
x = screen_width - tw_width - 5
2024-07-28 13:15:10 -04:00
if (y + height + tw_height > screen_height): # If tooltip goes below screen height
2024-07-27 10:36:43 -04:00
y = y - tw_height - height - 2 # Position above the widget
tw.geometry(f"+{x}+{y}")
def leave(self, event=None):
2024-07-28 13:15:10 -04:00
if self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None
2024-07-27 10:36:43 -04:00
def wrap_text(self, text):
words = text.split()
lines = []
current_line = []
for word in words:
if len(current_line) + len(word.split()) <= self.wrap_length:
current_line.append(word)
else:
2024-07-28 13:15:10 -04:00
lines.append(" ".join(current_line))
2024-07-27 10:36:43 -04:00
current_line = [word]
if current_line:
2024-07-28 13:15:10 -04:00
lines.append(" ".join(current_line))
return "\n".join(lines)
2024-07-27 10:36:43 -04:00
2024-08-08 20:41:19 -04:00
class BugFixedDateEntry(DateEntry):
"""
Fixes a bug on the calendar that does not accept mouse selection with Linux
2024-09-18 22:49:14 -04:00
Fixes a drop down bug when the DateEntry widget is not focused
2024-08-08 20:41:19 -04:00
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def drop_down(self):
2024-09-18 22:49:14 -04:00
self.focus_set() # Set focus to the DateEntry widget
2024-08-08 20:41:19 -04:00
super().drop_down()
if self._top_cal is not None and not self._calendar.winfo_ismapped():
self._top_cal.lift()
class EpsonPrinterUI(tk.Tk):
2024-09-18 22:49:14 -04:00
def __init__(
self,
model: str = None,
hostname: str = None,
conf_dict={},
replace_conf=False
):
super().__init__()
2024-07-27 07:53:03 -04:00
self.title("Epson Printer Configuration - v" + VERSION)
2024-08-07 20:32:03 -04:00
self.geometry("500x500")
self.minsize(500, 500)
2024-07-28 13:15:10 -04:00
self.printer_scanner = PrinterScanner()
2024-07-27 10:36:43 -04:00
self.ip_list = []
self.ip_list_cycle = None
2024-07-28 21:29:30 -04:00
self.conf_dict = conf_dict
self.replace_conf = replace_conf
2024-09-18 22:49:14 -04:00
self.text_dump = ""
self.mode = black.Mode(line_length=200, magic_trailing_comma=False)
self.printer = None
2024-07-26 22:01:38 -04:00
# configure the main window to be resizable
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
2024-07-27 07:53:03 -04:00
FRAME_PAD = 10
2024-07-28 13:15:10 -04:00
PAD = (3, 0)
PADX = 4
2024-07-27 07:53:03 -04:00
PADY = 5
2024-07-28 13:15:10 -04:00
# main Frame
2024-07-27 07:53:03 -04:00
main_frame = ttk.Frame(self, padding=FRAME_PAD)
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
main_frame.columnconfigure(0, weight=1)
2024-10-06 02:23:00 -04:00
main_frame.rowconfigure(4, weight=1) # Number of rows
2024-07-27 10:36:43 -04:00
row_n = 0
# [row 0] Container frame for the two LabelFrames Power-off timer and TI Received Time
model_ip_frame = ttk.Frame(main_frame, padding=PAD)
model_ip_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
model_ip_frame.columnconfigure(0, weight=1) # Allow column to expand
model_ip_frame.columnconfigure(1, weight=1) # Allow column to expand
2024-09-18 22:49:14 -04:00
# BOX printer model selection
2024-07-28 13:15:10 -04:00
model_frame = ttk.LabelFrame(
model_ip_frame, text="Printer Model", padding=PAD
)
model_frame.grid(
row=0, column=0, pady=PADY, padx=(0, PADX), sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
model_frame.columnconfigure(0, weight=0)
model_frame.columnconfigure(1, weight=1)
2024-07-28 13:15:10 -04:00
2024-09-18 22:49:14 -04:00
# Model combobox
self.model_var = tk.StringVar()
2024-07-28 21:29:30 -04:00
if (
"internal_data" in conf_dict
and "default_model" in conf_dict["internal_data"]
):
self.model_var.set(conf_dict["internal_data"]["default_model"])
2024-09-18 22:49:14 -04:00
if model:
self.model_var.set(model)
2024-07-28 13:15:10 -04:00
ttk.Label(model_frame, text="Model:").grid(
row=0, column=0, sticky=tk.W, padx=PADX
)
self.model_dropdown = ttk.Combobox(
model_frame, textvariable=self.model_var, state="readonly"
)
2024-07-28 21:29:30 -04:00
self.model_dropdown["values"] = sorted(EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf
).valid_printers)
2024-07-28 13:15:10 -04:00
self.model_dropdown.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
ToolTip(
self.model_dropdown,
2024-09-18 22:49:14 -04:00
"Select the model of the printer, or press 'Detect Printers'.\n"
"Press F2 to dump the parameters associated to the printer model.",
2024-07-28 13:15:10 -04:00
)
self.model_dropdown.bind("<F2>", self.printer_config)
2024-07-28 13:15:10 -04:00
2024-09-18 22:49:14 -04:00
# BOX IP address
2024-07-28 13:15:10 -04:00
ip_frame = ttk.LabelFrame(
model_ip_frame, text="Printer IP Address", padding=PAD
)
ip_frame.grid(
row=0, column=1, pady=PADY, padx=(PADX, 0), sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
ip_frame.columnconfigure(0, weight=0)
ip_frame.columnconfigure(1, weight=1)
2024-07-28 13:15:10 -04:00
2024-09-18 22:49:14 -04:00
# IP address entry
self.ip_var = tk.StringVar()
2024-07-28 21:29:30 -04:00
if (
"internal_data" in conf_dict
and "hostname" in conf_dict["internal_data"]
):
self.ip_var.set(conf_dict["internal_data"]["hostname"])
2024-09-18 22:49:14 -04:00
if hostname:
self.ip_var.set(hostname)
2024-07-28 13:15:10 -04:00
ttk.Label(ip_frame, text="IP Address:").grid(
row=0, column=0, sticky=tk.W, padx=PADX
)
self.ip_entry = ttk.Entry(ip_frame, textvariable=self.ip_var)
2024-07-28 13:15:10 -04:00
self.ip_entry.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
2024-07-27 10:36:43 -04:00
self.ip_entry.bind("<F2>", self.next_ip)
2024-07-28 13:15:10 -04:00
ToolTip(
self.ip_entry,
2024-09-18 22:49:14 -04:00
"Enter the IP address, or press 'Detect Printers'"
" (you can also enter part of the IP address"
" to speed up the detection),"
" or press F2 more times to get the next local IP address,"
" which can then be edited"
" (by removing the last part before pressing 'Detect Printers').",
2024-07-28 13:15:10 -04:00
)
2024-07-27 07:53:03 -04:00
# Create a custom style for the button to center the text
style = ttk.Style()
style.configure("Centered.TButton", justify='center', anchor="center")
2024-07-27 10:36:43 -04:00
# [row 1] Container frame for the two LabelFrames Power-off timer and TI Received Time
row_n += 1
2024-07-27 07:53:03 -04:00
container_frame = ttk.Frame(main_frame, padding=PAD)
2024-07-28 13:15:10 -04:00
container_frame.grid(
row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
container_frame.columnconfigure(0, weight=1) # Allow column to expand
container_frame.columnconfigure(1, weight=1) # Allow column to expand
2024-10-06 02:23:00 -04:00
# BOX Power-off Timer (minutes)
2024-07-28 13:15:10 -04:00
po_timer_frame = ttk.LabelFrame(
2024-10-06 02:23:00 -04:00
container_frame, text="Power-off Timer (minutes)", padding=PAD
2024-07-28 13:15:10 -04:00
)
po_timer_frame.grid(
row=0, column=0, pady=PADY, padx=(0, PADX), sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
po_timer_frame.columnconfigure(0, weight=0) # Button column on the left
po_timer_frame.columnconfigure(1, weight=1) # Entry column
po_timer_frame.columnconfigure(2, weight=0) # Button column on the right
2024-07-28 13:15:10 -04:00
2024-07-27 07:53:03 -04:00
# Configure validation command for numeric entry
validate_cmd = self.register(self.validate_number_input)
2024-09-18 22:49:14 -04:00
# Power-off timer (minutes) - Get Button
button_width = 7
self.get_po_minutes = ttk.Button(
po_timer_frame,
text="Get",
width=button_width,
command=self.get_po_mins,
)
self.get_po_minutes.grid(
row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W
)
# Power-off timer (minutes) - minutes Entry
2024-07-27 07:53:03 -04:00
self.po_timer_var = tk.StringVar()
2024-07-28 13:15:10 -04:00
self.po_timer_entry = ttk.Entry(
po_timer_frame,
textvariable=self.po_timer_var,
validate="all",
validatecommand=(validate_cmd, "%P"),
width=6,
justify="center",
)
self.po_timer_entry.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
2024-09-18 22:49:14 -04:00
ToolTip(
self.po_timer_entry,
"Enter a number of minutes.",
destroy=False
2024-07-28 13:15:10 -04:00
)
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
# Power-off timer (minutes) - Set Button
self.set_po_minutes = ttk.Button(
2024-07-28 13:15:10 -04:00
po_timer_frame,
text="Set",
width=button_width,
command=self.set_po_mins,
)
2024-09-18 22:49:14 -04:00
self.set_po_minutes.grid(
row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E
)
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
# BOX TI Received Time (date)
2024-07-28 13:15:10 -04:00
ti_received_frame = ttk.LabelFrame(
container_frame, text="TI Received Time (date)", padding=PAD
)
ti_received_frame.grid(
row=0, column=1, pady=PADY, padx=(PADX, 0), sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
ti_received_frame.columnconfigure(0, weight=0) # Button column on the left
2024-07-27 10:36:43 -04:00
ti_received_frame.columnconfigure(1, weight=1) # Calendar column
2024-07-27 07:53:03 -04:00
ti_received_frame.columnconfigure(2, weight=0) # Button column on the right
2024-09-18 22:49:14 -04:00
# TI Received Time - Get Button
self.get_ti_received = ttk.Button(
ti_received_frame,
text="Get",
width=button_width,
command=self.get_ti_date,
)
self.get_ti_received.grid(
row=0, column=0, padx=PADX, pady=PADY, sticky=tk.W
)
# TI Received Time - Calendar Widget
2024-08-08 20:41:19 -04:00
self.date_entry = BugFixedDateEntry(
2024-07-28 13:15:10 -04:00
ti_received_frame, date_pattern="yyyy-mm-dd"
)
self.date_entry.grid(
row=0, column=1, padx=PADX, pady=PADY, sticky=(tk.W, tk.E)
)
2024-08-08 20:41:19 -04:00
self.date_entry.delete(0, "end") # blank the field removing the current date
2024-09-18 22:49:14 -04:00
ToolTip(
self.date_entry,
"Enter a valid date with format YYYY-MM-DD.",
destroy=False
2024-07-28 13:15:10 -04:00
)
2024-07-27 07:53:03 -04:00
2024-09-18 22:49:14 -04:00
# TI Received Time - Set Button
self.set_ti_received = ttk.Button(
2024-07-28 13:15:10 -04:00
ti_received_frame,
text="Set",
width=button_width,
command=self.set_ti_date,
)
2024-09-18 22:49:14 -04:00
self.set_ti_received.grid(
row=0, column=2, padx=PADX, pady=PADY, sticky=tk.E
)
2024-07-27 07:53:03 -04:00
# [row 2] Query Buttons
2024-07-27 10:36:43 -04:00
row_n += 1
2024-07-27 07:53:03 -04:00
button_frame = ttk.Frame(main_frame, padding=PAD)
2024-07-27 10:36:43 -04:00
button_frame.grid(row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E))
button_frame.columnconfigure((0, 1, 2), weight=1) # expand columns
2024-07-28 13:15:10 -04:00
# Query Printer Status
2024-07-28 13:15:10 -04:00
self.status_button = ttk.Button(
2024-10-06 02:23:00 -04:00
button_frame, text="Printer Status",
command=self.printer_status,
style="Centered.TButton"
2024-07-28 13:15:10 -04:00
)
self.status_button.grid(
row=0, column=0, padx=PADX, pady=PADY, sticky=(tk.W, tk.E)
2024-07-28 13:15:10 -04:00
)
# Query list of cartridge types
self.web_interface_button = ttk.Button(
2024-07-28 13:15:10 -04:00
button_frame,
text="Printer Web interface",
command=self.web_interface,
style="Centered.TButton"
2024-07-28 13:15:10 -04:00
)
self.web_interface_button.grid(
row=0, column=1, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
# Query firmware version
self.firmware_version_button = ttk.Button(
button_frame,
text="Firmware version",
command=self.firmware_version,
style="Centered.TButton"
)
self.firmware_version_button.grid(
2024-07-28 13:15:10 -04:00
row=0, column=2, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
2024-07-27 07:53:03 -04:00
# [row 3] Tweak Buttons
2024-10-06 02:23:00 -04:00
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
2024-10-06 02:23:00 -04:00
# Detect Printers
self.detect_button = ttk.Button(
tweak_frame,
text="Detect\nPrinters",
command=self.start_detect_printers,
style="Centered.TButton"
)
self.detect_button.grid(
row=0, column=0, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
# Detect Access Keys
self.detect_access_key_button = ttk.Button(
tweak_frame,
text="Detect\nAccess Keys",
command=self.detect_access_key,
style="Centered.TButton"
)
self.detect_access_key_button.grid(
row=0, column=1, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
# Read EEPROM
self.read_eeprom_button = ttk.Button(
2024-10-06 02:23:00 -04:00
tweak_frame,
text="Read\nEEPROM",
command=self.read_eeprom,
style="Centered.TButton"
)
self.read_eeprom_button.grid(
row=0, column=2, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
2024-10-05 11:45:04 -04:00
# Write EEPROM
self.write_eeprom_button = ttk.Button(
2024-10-06 02:23:00 -04:00
tweak_frame,
text="Write\nEEPROM",
command=self.write_eeprom,
style="Centered.TButton"
)
self.write_eeprom_button.grid(
row=0, column=3, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
)
# Reset Waste Ink Levels
self.reset_button = ttk.Button(
2024-10-06 02:23:00 -04:00
tweak_frame,
text="Reset Waste\nInk Levels",
command=self.reset_waste_ink,
2024-10-06 02:23:00 -04:00
style="Centered.TButton"
)
self.reset_button.grid(
row=0, column=4, padx=PADX, pady=PADX, sticky=(tk.W, tk.E)
2024-10-06 02:23:00 -04:00
)
# [row 4] Status display (including ScrolledText and Treeview)
2024-07-27 10:36:43 -04:00
row_n += 1
2024-07-27 07:53:03 -04:00
status_frame = ttk.LabelFrame(main_frame, text="Status", padding=PAD)
2024-07-28 13:15:10 -04:00
status_frame.grid(
row=row_n, column=0, pady=PADY, sticky=(tk.W, tk.E, tk.N, tk.S)
)
status_frame.columnconfigure(0, weight=1)
status_frame.rowconfigure(0, weight=1)
2024-07-28 13:15:10 -04:00
2024-07-25 16:08:05 -04:00
# ScrolledText widget
2024-07-28 13:15:10 -04:00
self.status_text = ScrolledText(
status_frame, wrap=tk.WORD, font=("TkDefaultFont")
)
self.status_text.grid(
row=0,
column=0,
pady=PADY,
padx=PADY,
sticky=(tk.W, tk.E, tk.N, tk.S),
)
2024-09-18 22:49:14 -04:00
self.status_text.bind("<Tab>", self.focus_next)
self.status_text.bind("<Shift-Tab>", self.focus_previous)
2024-07-27 07:53:03 -04:00
self.status_text.bind("<Key>", lambda e: "break") # disable editing text
2024-07-28 13:15:10 -04:00
self.status_text.bind(
"<Control-c>",
lambda event: self.copy_to_clipboard(self.status_text),
)
2024-07-27 07:53:03 -04:00
# self.status_text.bind("<Button-1>", lambda e: "break") # also disable the mouse
2024-07-25 16:08:05 -04:00
2024-07-27 07:53:03 -04:00
# Create a frame to contain the Treeview and its scrollbar
self.tree_frame = tk.Frame(status_frame)
self.tree_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.tree_frame.columnconfigure(0, weight=1)
self.tree_frame.rowconfigure(0, weight=1)
# Style configuration for the treeview
2024-07-25 16:08:05 -04:00
style = ttk.Style(self)
2024-07-27 07:53:03 -04:00
treeview_font = style.lookup("Treeview.Heading", "font")
2024-07-25 16:08:05 -04:00
2024-07-27 07:53:03 -04:00
# For the treeview, if the treeview_font is a tuple, split into components
if isinstance(treeview_font, tuple):
2024-07-28 13:15:10 -04:00
treeview_font_name, treeview_font_size = (
treeview_font[0],
treeview_font[1],
)
2024-07-25 16:08:05 -04:00
else:
# If font is not a tuple, it might be a font string or other format.
2024-07-28 13:15:10 -04:00
treeview_font_name, treeview_font_size = tkfont.Font().actual(
"family"
), tkfont.Font().actual("size")
style.configure(
"Treeview.Heading",
font=(treeview_font_name, treeview_font_size - 4, "bold"),
background="lightblue",
foreground="darkblue",
)
2024-07-25 16:08:05 -04:00
# Create and configure the Treeview widget
2024-07-27 07:53:03 -04:00
self.tree = ttk.Treeview(self.tree_frame, style="Treeview")
2024-07-25 16:08:05 -04:00
self.tree.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Create a vertical scrollbar for the Treeview
2024-07-28 13:15:10 -04:00
tree_scrollbar = ttk.Scrollbar(
self.tree_frame, orient="vertical", command=self.tree.yview
)
2024-07-25 16:08:05 -04:00
tree_scrollbar.grid(column=1, row=0, sticky=(tk.N, tk.S))
# Configure the Treeview to use the scrollbar
self.tree.configure(yscrollcommand=tree_scrollbar.set)
# Create a context menu
self.context_menu = Menu(self, tearoff=0)
2024-07-28 13:15:10 -04:00
self.context_menu.add_command(
2024-09-18 22:49:14 -04:00
label="Copy this item", command=self.copy_selected_item
)
self.context_menu.add_command(
label="Copy all items", command=self.copy_all_items
)
self.context_menu.add_command(
label="Print all items", command=self.print_items
2024-07-28 13:15:10 -04:00
)
2024-07-25 16:08:05 -04:00
# Bind the right-click event to the Treeview
self.tree.bind("<Button-3>", self.show_context_menu)
2024-07-27 07:53:03 -04:00
# Hide the Treeview initially
self.tree_frame.grid_remove()
2024-09-18 22:49:14 -04:00
self.model_var.trace('w', self.change_widget_states)
self.ip_var.trace('w', self.change_widget_states)
self.change_widget_states()
def focus_next(self, event):
event.widget.tk_focusNext().focus()
return("break")
def focus_previous(self, event):
event.widget.tk_focusPrev().focus()
return("break")
2024-09-18 22:49:14 -04:00
def change_widget_states(self, index=None, value=None, op=None):
"""
Enable or disable buttons when IP address and printer model change
"""
ToolTip(self.get_ti_received, "")
ToolTip(self.get_po_minutes, "")
ToolTip(self.read_eeprom_button, "")
ToolTip(self.write_eeprom_button, "")
ToolTip(self.reset_button, "")
2024-09-18 22:49:14 -04:00
if self.ip_var.get():
if not self.model_var.get():
self.reset_button.state(["disabled"])
2024-09-18 22:49:14 -04:00
self.status_button.state(["!disabled"])
self.firmware_version_button.state(["!disabled"])
self.web_interface_button.state(["!disabled"])
self.detect_access_key_button.state(["!disabled"])
2024-09-18 22:49:14 -04:00
self.printer = None
else:
self.reset_button.state(["disabled"])
2024-09-18 22:49:14 -04:00
self.status_button.state(["disabled"])
self.read_eeprom_button.state(["disabled"])
self.write_eeprom_button.state(["disabled"])
self.firmware_version_button.state(["disabled"])
self.web_interface_button.state(["disabled"])
self.detect_access_key_button.state(["disabled"])
2024-09-18 22:49:14 -04:00
if self.ip_var.get() and self.model_var.get():
self.printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=self.model_var.get(),
hostname=self.ip_var.get()
)
if not self.printer:
return
if not self.printer.parm:
self.reset_printer_model()
return
self.read_eeprom_button.state(["disabled"])
ToolTip(
self.read_eeprom_button,
"Feature not defined in the printer configuration."
)
self.write_eeprom_button.state(["disabled"])
ToolTip(
self.write_eeprom_button,
"Feature not defined in the printer configuration."
)
if self.printer and self.printer.parm:
if "read_key" in self.printer.parm:
self.read_eeprom_button.state(["!disabled"])
ToolTip(self.read_eeprom_button, "")
if "write_key" in self.printer.parm:
self.write_eeprom_button.state(["!disabled"])
ToolTip(
self.write_eeprom_button,
"Ensure you really want this before pressing this key."
)
2024-09-18 22:49:14 -04:00
if self.printer.parm.get("stats", {}).get("Power off timer"):
self.po_timer_entry.state(["!disabled"])
self.get_po_minutes.state(["!disabled"])
self.set_po_minutes.state(["!disabled"])
ToolTip(self.get_po_minutes, "")
else:
self.po_timer_entry.state(["disabled"])
self.get_po_minutes.state(["disabled"])
self.set_po_minutes.state(["disabled"])
ToolTip(
self.get_po_minutes,
"Feature not defined in the printer configuration."
)
2024-09-18 22:49:14 -04:00
if self.printer.parm.get("stats", {}).get("First TI received time"):
self.date_entry.state(["!disabled"])
self.get_ti_received.state(["!disabled"])
self.set_ti_received.state(["!disabled"])
ToolTip(self.get_ti_received, "")
else:
self.date_entry.state(["disabled"])
self.get_ti_received.state(["disabled"])
self.set_ti_received.state(["disabled"])
ToolTip(
self.get_ti_received,
"Feature not defined in the printer configuration."
)
2024-09-18 22:49:14 -04:00
if self.printer.reset_waste_ink_levels(dry_run=True):
self.reset_button.state(["!disabled"])
ToolTip(
self.reset_button,
"Ensure you really want this before pressing this key."
)
else:
self.reset_button.state(["disabled"])
ToolTip(
self.reset_button,
"Feature not defined in the printer configuration."
)
else:
self.status_button.state(["disabled"])
self.read_eeprom_button.state(["disabled"])
self.write_eeprom_button.state(["disabled"])
2024-09-18 22:49:14 -04:00
self.po_timer_entry.state(["disabled"])
self.get_po_minutes.state(["disabled"])
self.set_po_minutes.state(["disabled"])
self.date_entry.state(["disabled"])
self.get_ti_received.state(["disabled"])
self.set_ti_received.state(["disabled"])
self.update_idletasks()
2024-07-27 10:36:43 -04:00
def next_ip(self, event):
ip = self.ip_var.get()
if self.ip_list_cycle == None:
self.ip_list = self.printer_scanner.get_all_printers(local=True)
self.ip_list_cycle = 0
if not self.ip_list:
return
self.ip_var.set(self.ip_list[self.ip_list_cycle])
self.ip_list_cycle += 1
if self.ip_list_cycle >= len(self.ip_list):
self.ip_list_cycle = None
def copy_to_clipboard(self, text_widget):
try:
text = text_widget.selection_get()
pyperclip.copy(text)
except tk.TclError:
pass
return "break"
2024-09-20 02:35:28 -04:00
def handle_printer_error(self, e):
self.show_status_text_view()
if isinstance(e, TimeoutError):
self.status_text.insert(
2024-09-20 04:47:35 -04:00
tk.END, f"[ERROR] printer is unreachable or offline.\n"
2024-09-20 02:35:28 -04:00
)
else:
self.status_text.insert(
2024-09-20 04:47:35 -04:00
tk.END, f"[ERROR] {e}\n{traceback.format_exc()}\n"
2024-09-20 02:35:28 -04:00
)
2024-07-28 13:15:10 -04:00
def get_po_mins(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update()
2024-07-27 07:53:03 -04:00
return
2024-09-18 22:49:14 -04:00
if not self.printer:
return
if not self.printer.parm.get("stats", {}).get("Power off timer"):
2024-08-08 20:41:19 -04:00
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'Power off timer' in configuration\n",
)
self.config(cursor="")
self.update_idletasks()
return
2024-07-27 07:53:03 -04:00
try:
2024-09-18 22:49:14 -04:00
po_timer = self.printer.stats()["stats"]["Power off timer"]
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, f"[INFO] Power off timer: {po_timer} minutes.\n"
)
2024-07-27 07:53:03 -04:00
self.po_timer_var.set(po_timer)
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
def get_current_eeprom_values(self, values, label):
try:
org_values = ', '.join(
"" if v is None else f"{k}: {int(v, 16)}" for k, v in zip(
values, self.printer.read_eeprom_many(values, label=label)
)
)
if org_values:
self.status_text.insert(
tk.END,
f"[NOTE] Current EEPROM values for {label}: {org_values}.\n"
)
else:
self.status_text.insert(
tk.END,
f'[ERROR] Cannot read EEPROM values for "{label}"'
': invalid printer model selected.\n'
)
self.config(cursor="")
self.update_idletasks()
return False
self.config(cursor="")
self.update_idletasks()
return True
except Exception as e:
self.handle_printer_error(e)
self.config(cursor="")
self.update_idletasks()
return False
2024-07-28 13:15:10 -04:00
def set_po_mins(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-09-18 22:49:14 -04:00
if not self.printer:
return
if not self.printer.parm.get("stats", {}).get("Power off timer"):
2024-08-08 20:41:19 -04:00
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'Power off timer' in configuration\n",
)
2024-07-28 13:15:10 -04:00
self.config(cursor="")
self.update_idletasks()
2024-08-08 20:41:19 -04:00
return
po_timer = self.po_timer_var.get()
self.config(cursor="")
self.update_idletasks()
if not po_timer.isnumeric():
2024-07-28 13:15:10 -04:00
self.status_text.insert(
2024-08-08 20:41:19 -04:00
tk.END, "[ERROR] Please Use a valid value for minutes.\n"
2024-07-28 13:15:10 -04:00
)
self.config(cursor="")
self.update_idletasks()
2024-08-08 20:41:19 -04:00
return
try:
if not self.get_current_eeprom_values(
self.printer.parm["stats"]["Power off timer"],
"Power off timer"
):
self.config(cursor="")
self.update_idletasks()
return
except Exception as e:
self.handle_printer_error(e)
self.config(cursor="")
self.update_idletasks()
return
2024-08-08 20:41:19 -04:00
self.status_text.insert(
tk.END, f"[INFO] Set Power off timer: {po_timer} minutes.\n"
)
response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no')
2024-08-08 20:41:19 -04:00
if response:
try:
2024-09-18 22:49:14 -04:00
self.printer.write_poweroff_timer(int(po_timer))
2024-08-08 20:41:19 -04:00
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-08-08 20:41:19 -04:00
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
2024-08-08 20:41:19 -04:00
tk.END, f"[WARNING] Set Power off timer aborted.\n"
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def get_ti_date(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-09-18 22:49:14 -04:00
if not self.printer:
return
if not self.printer.parm.get("stats", {}).get("First TI received time"):
2024-08-08 20:41:19 -04:00
self.status_text.insert(
tk.END,
f"[ERROR]: Missing 'First TI received time' in configuration\n",
)
self.config(cursor="")
self.update_idletasks()
return
2024-07-27 07:53:03 -04:00
try:
2024-07-28 13:15:10 -04:00
date_string = datetime.strptime(
2024-09-18 22:49:14 -04:00
self.printer.stats(
)["stats"]["First TI received time"], "%d %b %Y"
2024-07-28 13:15:10 -04:00
).strftime("%Y-%m-%d")
self.status_text.insert(
tk.END,
f"[INFO] First TI received time (YYYY-MM-DD): {date_string}.\n",
)
2024-07-27 07:53:03 -04:00
self.date_entry.set_date(date_string)
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
def set_ti_date(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-27 07:53:03 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
2024-09-18 22:49:14 -04:00
if not self._is_valid_ip(ip_address):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
return
2024-09-18 22:49:14 -04:00
if not self.printer:
return
if not self.printer.parm.get("stats", {}).get("First TI received time"):
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-08-08 20:41:19 -04:00
f"[ERROR]: Missing 'First TI received time' in configuration\n",
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
return
date_string = self.date_entry.get_date()
try:
if not self.get_current_eeprom_values(
self.printer.parm["stats"]["First TI received time"],
"First TI received time"
):
self.config(cursor="")
self.update_idletasks()
return
except Exception as e:
self.handle_printer_error(e)
self.config(cursor="")
self.update_idletasks()
return
2024-08-08 20:41:19 -04:00
self.status_text.insert(
tk.END,
2024-09-18 22:49:14 -04:00
f"[INFO] Set 'First TI received time' (YYYY-MM-DD) to: "
f"{date_string.strftime('%Y-%m-%d')}.\n",
2024-08-08 20:41:19 -04:00
)
response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no')
2024-08-08 20:41:19 -04:00
if response:
try:
2024-09-18 22:49:14 -04:00
self.printer.write_first_ti_received_time(
2024-07-28 13:15:10 -04:00
date_string.year, date_string.month, date_string.day
)
2024-08-08 20:41:19 -04:00
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-08-08 20:41:19 -04:00
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-08-08 20:41:19 -04:00
f"[WARNING] Change of 'First TI received time' aborted.\n",
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
self.config(cursor="")
self.update_idletasks()
2024-07-27 07:53:03 -04:00
def validate_number_input(self, new_value):
# This function will be called with the new input value
if new_value == "" or new_value.isdigit():
return True
else:
return False
2024-07-25 16:08:05 -04:00
def show_status_text_view(self):
"""Show the status frame and hide the Treeview."""
2024-07-27 07:53:03 -04:00
self.tree_frame.grid_remove()
2024-07-25 16:08:05 -04:00
self.status_text.grid()
def show_treeview(self):
"""Show the Treeview and hide the status frame."""
self.status_text.grid_remove()
2024-07-27 07:53:03 -04:00
self.tree_frame.grid()
2024-07-25 16:08:05 -04:00
2024-07-28 13:15:10 -04:00
def printer_status(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
model = self.model_var.get()
ip_address = self.ip_var.get()
2024-08-07 20:32:03 -04:00
if not self._is_valid_ip(ip_address):
self.status_text.insert(
tk.END,
2024-09-18 22:49:14 -04:00
"[ERROR] Please enter a valid IP address, or "
"press 'Detect Printers'.\n"
2024-08-07 20:32:03 -04:00
)
2024-07-28 13:15:10 -04:00
self.config(cursor="")
self.update_idletasks()
return
2024-07-28 21:29:30 -04:00
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model,
hostname=ip_address
)
2024-09-18 22:49:14 -04:00
if not printer:
return
try:
2024-09-18 22:49:14 -04:00
self.text_dump = black.format_str(
f'"{printer.model}": ' + repr(printer.stats()), mode=self.mode
)
2024-07-25 16:08:05 -04:00
self.show_treeview()
2024-07-28 13:15:10 -04:00
# Configure tags
self.tree.tag_configure("key", foreground="black")
self.tree.tag_configure("key_value", foreground="dark blue")
self.tree.tag_configure("value", foreground="blue")
self.tree.heading("#0", text="Status Information", anchor="w")
2024-07-28 13:15:10 -04:00
2024-07-25 16:08:05 -04:00
# Populate the Treeview
self.tree.delete(*self.tree.get_children())
2024-07-28 13:15:10 -04:00
self.populate_treeview("", self.tree, printer.stats())
2024-07-25 16:08:05 -04:00
# Expand all nodes
self.expand_all(self.tree)
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-07-28 13:15:10 -04:00
finally:
self.config(cursor="")
self.update_idletasks()
2024-09-18 22:49:14 -04:00
def reset_printer_model(self):
self.show_status_text_view()
if self.model_var.get():
self.status_text.insert(
tk.END,
'[ERROR]: Unknown printer model '
f'"{self.model_var.get()}"\n',
)
else:
self.status_text.insert(
tk.END,
'[ERROR]: Select a valid printer model.\n'
)
self.config(cursor="")
self.update()
self.model_var.set("")
def printer_config(self, cursor=True):
2024-09-18 22:49:14 -04:00
"""
Pressing F2 dumps the printer configuration
"""
model = self.model_var.get()
printer = EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf,
model=model
)
2024-09-18 22:49:14 -04:00
if not printer:
return
if not printer.parm:
self.reset_printer_model()
return
try:
2024-09-18 22:49:14 -04:00
self.text_dump = black.format_str(
f'"{printer.model}": ' + repr(printer.parm),
mode=self.mode
)
self.show_treeview()
# Configure tags
self.tree.tag_configure("key", foreground="black")
self.tree.tag_configure("key_value", foreground="dark blue")
self.tree.tag_configure("value", foreground="blue")
self.tree.heading("#0", text="Printer parameters", anchor="w")
# Populate the Treeview
self.tree.delete(*self.tree.get_children())
self.populate_treeview("", self.tree, printer.parm)
# Expand all nodes
self.expand_all(self.tree)
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
finally:
self.update_idletasks()
def read_eeprom(self):
def parse_list_input(input_str):
try:
# Remove any character before ":" if not including digits
colon_index = input_str.find(':')
if colon_index != -1 and not any(
char.isdigit() for char in input_str[:colon_index]
):
input_str = input_str[colon_index + 1:]
# Remove any unwanted characters like brackets, if present
input_str = input_str.strip('{}[]()')
# Remove trailing ".", if present
input_str = input_str.rstrip('.')
parts = input_str.split(',')
addresses = []
for part in parts:
part = part.strip()
if '-' in part:
# Handle range like '2-10'
start, end = map(int, part.split('-'))
addresses.extend(range(start, end + 1)) # Generate sequence between start and end
else:
# Handle individual addresses
addresses.append(int(part))
return addresses
except ValueError:
# Show an error if the input is not valid
messagebox.showerror(
"Invalid Input for Read EEPROM",
"Please enter a valid list of integers."
)
return None
def get_input():
# Create a popup to accept the list input
dialog = MultiLineInputDialog(
self,
"Read EEPROM values",
"Enter a comma-separated list of addresses to\n"
"be read (e.g. 22, 23, 59 or [171, 170, 169, 168]).\n"
"Use hyphen to represent a range (e.g., 1, 3-10, 13):"
)
if dialog.result:
addresses = parse_list_input(dialog.result)
if addresses:
return addresses
return None
def get_values(addresses):
try:
values = ', '.join(
"" if v is None else f"{k}: {int(v, 16)}" for k, v in zip(
addresses,
self.printer.read_eeprom_many(
addresses,
label="read_EEPROM"
)
)
)
except Exception as e:
self.handle_printer_error(e)
self.config(cursor="")
self.update_idletasks()
return
if values:
self.status_text.insert(
tk.END,
f"[INFO] EEPROM values: {values}.\n"
)
else:
self.status_text.insert(
tk.END,
f'[ERROR] Cannot read EEPROM values'
': invalid printer model selected.\n'
)
self.config(cursor="")
self.update_idletasks()
self.show_status_text_view()
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
return
addresses = get_input()
if addresses is not None:
self.config(cursor="watch")
self.update()
self.after(100, lambda: get_values(addresses))
2024-10-06 02:23:00 -04:00
def detect_access_key(self):
def run_detection():
"""
Process:
- detect the read_key
- extract the serial number
- use the last character of the serial number to validate the write_key
- produce an ordered list of all the known write_key
- validate the write_key against any of the known values
"""
2024-10-06 02:23:00 -04:00
current_log_level = logging.getLogger().getEffectiveLevel()
logging.getLogger().setLevel(logging.ERROR)
if not self._is_valid_ip(ip_address):
self.status_text.insert(tk.END, NO_CONF_ERROR)
logging.getLogger().setLevel(current_log_level)
self.config(cursor="")
self.update_idletasks()
return
if not self.printer:
self.printer = EpsonPrinter(
hostname=self.ip_var.get()
)
self.printer.parm = {'read_key': None}
# Detect the read_key
self.status_text.insert(
tk.END,
f"[INFO] Detecting the read_key...\n"
)
self.update_idletasks()
read_key = None
2024-10-06 02:23:00 -04:00
try:
read_key = self.printer.brute_force_read_key()
self.status_text.insert(
tk.END, f"[INFO] Detected read_key: {read_key}.\n"
2024-10-06 02:23:00 -04:00
)
except Exception as e:
self.handle_printer_error(e)
logging.getLogger().setLevel(current_log_level)
self.config(cursor="")
self.update_idletasks()
return
if not read_key:
self.status_text.insert(
tk.END, f"[ERROR] Could not detect read_key.\n"
)
logging.getLogger().setLevel(current_log_level)
self.config(cursor="")
self.update_idletasks()
return
# Extract the serial number
self.status_text.insert(
tk.END,
f"[INFO] Detecting the serial number...\n"
)
self.update_idletasks()
last_ser_num_addr = None
if not self.printer.parm:
self.printer.parm = {'read_key': read_key}
if (
'read_key' not in self.printer.parm
or self.printer.parm['read_key'] is None
):
self.printer.parm['read_key'] = read_key
if self.printer.parm['read_key'] != read_key:
self.status_text.insert(
tk.END,
f"[INFO] You selected a model with the wrong read_key "
f"{self.printer.parm['read_key']} instead of "
f"{read_key}. Using the correct one now.\n"
)
self.printer.parm['read_key'] = read_key
hex_bytes, matches = self.printer.find_serial_number(range(2048))
if not matches:
self.status_text.insert(
tk.END,
f"[ERROR] Cannot detect the serial number.\n"
)
elif len(matches) != 1:
self.status_text.insert(
tk.END,
"[ERROR] More than one pattern appears to be"
" a serial number:\n"
)
for match in matches:
self.status_text.insert(
tk.END,
f'[ERROR] - found pattern "{match.group()}"'
f" at address {match.start()}\n"
)
else:
serial_number = matches[0].group()
serial_number_address = matches[0].start()
self.status_text.insert(
tk.END,
f'[INFO] Detected serial number "{serial_number}"'
f" at address {serial_number_address}.\n"
)
last_ser_num_addr = serial_number_address + 9
last_ser_num_value = int(hex_bytes[last_ser_num_addr], 16)
if last_ser_num_addr is None:
self.status_text.insert(
tk.END,
"[ERROR] Could not detect serial number.\n"
)
logging.getLogger().setLevel(current_log_level)
self.config(cursor="")
self.update_idletasks()
return
# Produce an ordered list of all the known write_key
write_key_list = self.printer.write_key_list(read_key)
# Validate the write_key against any of the known values
old_write_key = None
if 'write_key' in self.printer.parm:
old_write_key = self.printer.parm['write_key']
found_write_key = None
valid = False
for write_key in write_key_list:
self.printer.parm['write_key'] = write_key
try:
valid = self.printer.validate_write_key(
last_ser_num_addr,
last_ser_num_value,
label="test_write_eeprom"
)
assert valid is not None
except AssertionError:
self.status_text.insert(
tk.END,
"[ERROR] Write operation failed. Check whether the"
" serial number is changed and restore it manually.\n"
)
self.config(cursor="")
self.update_idletasks()
except Exception as e:
self.handle_printer_error(e)
logging.getLogger().setLevel(current_log_level)
self.config(cursor="")
self.update_idletasks()
return
if valid is None:
self.status_text.insert(
tk.END, "[ERROR] Operation interrupted with errors.\n"
)
logging.getLogger().setLevel(current_log_level)
self.config(cursor="")
self.update_idletasks()
return
if valid is False:
continue
found_write_key = write_key
self.status_text.insert(
tk.END, f"[INFO] Detected write_key: {found_write_key}\n"
)
if old_write_key and old_write_key != found_write_key:
self.status_text.insert(
tk.END,
f"[INFO] Found write key is different from"
f" the selected one: {old_write_key}\n"
)
self.printer.parm['write_key'] = old_write_key
# List conforming models
rk_kist = []
wk_kist = []
rwk_kist = []
for p, v in self.printer.PRINTER_CONFIG.items():
if not v:
continue
if v.get("read_key") == read_key:
rk_kist.append(p)
if v.get("write_key") == found_write_key:
wk_kist.append(p)
if (
v.get("read_key") == read_key
and v.get("write_key") == found_write_key
):
rwk_kist.append(p)
if rk_kist:
self.status_text.insert(
tk.END,
f"[INFO] Models with same read_key: {rk_kist}\n"
)
if wk_kist:
self.status_text.insert(
tk.END,
f"[INFO] Models with same write_key: {wk_kist}\n"
)
if rwk_kist:
self.status_text.insert(
tk.END,
f"[INFO] Models with same access keys: {rwk_kist}\n"
)
self.status_text.insert(
tk.END, "[INFO] Detect operation completed.\n"
)
break
if not found_write_key:
self.status_text.insert(
tk.END,
"[ERROR] Unable to detect the write key by validating"
" against any of the known ones.\n"
)
2024-10-06 02:23:00 -04:00
logging.getLogger().setLevel(current_log_level)
self.config(cursor="")
self.update_idletasks()
# Confirmation message
2024-10-06 02:23:00 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
self.status_text.insert(tk.END, NO_CONF_ERROR)
return
response = messagebox.askyesno(
"Confirm Action",
"Warning: this is a brute force operation, which takes several\n"
"minutes to complete.\n\n"
"Results will be shown in the status box.\n\n"
"Make sure not to switch off the printer while the process"
" is running and disable the auto power-off timer.\n\n"
"Are you sure you want to proceed?",
default='no'
2024-10-06 02:23:00 -04:00
)
if response:
self.status_text.insert(
tk.END, f"[INFO] Starting the operation, please wait...\n"
)
self.config(cursor="watch")
self.update()
self.after(100, lambda: run_detection())
else:
self.status_text.insert(
tk.END, f"[WARNING] Detect access key aborted.\n"
)
self.config(cursor="")
self.update_idletasks()
def web_interface(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
self.show_status_text_view()
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update()
return
if not self.printer:
return
try:
ret = webbrowser.open(ip_address)
if ret:
self.status_text.insert(
tk.END, f"[INFO] The browser is being opened.\n"
)
else:
self.status_text.insert(
tk.END, f"[ERROR] Cannot open browser.\n"
)
except Exception as e:
self.status_text.insert(
tk.END, f"[ERROR] Cannot open web browser: {e}\n"
)
finally:
self.config(cursor="")
self.update_idletasks()
def firmware_version(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
self.show_status_text_view()
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update()
return
if not self.printer:
return
try:
firmware_version = self.printer.get_firmware_version()
self.status_text.insert(
tk.END, f"[INFO] Firmware version: {firmware_version}.\n"
)
except Exception as e:
self.handle_printer_error(e)
finally:
self.config(cursor="")
self.update_idletasks()
def write_eeprom(self):
def parse_dict_input(input_str):
try:
# Remove any character before ":" if not including digits
colon_index = input_str.find(':')
if colon_index != -1 and not any(
char.isdigit() for char in input_str[:colon_index]
):
input_str = input_str[colon_index + 1:]
# Remove any unwanted characters like brackets, if present
input_str = input_str.strip('{}[]()')
# Remove trailing ".", if present
input_str = input_str.rstrip('.')
parts = input_str.split(',')
result_dict = {}
for part in parts:
part = part.strip()
if ':' not in part:
raise ValueError()
# Handle key: value pairs
key, value = map(str.strip, part.split(':', 1))
result_dict[int(key)] = int(value)
return result_dict
except ValueError:
messagebox.showerror(
"Invalid input for Write EEPROM",
"Please enter a valid comma-separated sequence "
"of 'address: value', like 24: 0, 25: 0, 30: 0 "
"using decimal numbers."
)
return {}
def get_input():
# Create a popup to accept the dictionary input
dialog = MultiLineInputDialog(
self,
"Write EEPROM values",
"Warning: this is a dangerous operation.\nContinue only "
"if you are very sure of what you do.\n\n"
"Enter a comma-separated sequence of 'address: value'\n"
"(like 24: 0, 25: 0, 30: 0) using decimal numbers:"
)
if dialog.result:
dict_addr_val = parse_dict_input(dialog.result)
if dict_addr_val:
return dict_addr_val
return None
def dialog_write_values(dict_addr_val):
try:
if not self.get_current_eeprom_values(
dict_addr_val.keys(),
"the entered addresses"
):
self.config(cursor="")
self.update_idletasks()
return
except Exception as e:
self.handle_printer_error(e)
self.config(cursor="")
self.update_idletasks()
return
self.config(cursor="")
self.update_idletasks()
response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no')
if response:
self.config(cursor="watch")
self.update()
self.after(200, lambda: write_eeprom_values(dict_addr_val))
else:
self.status_text.insert(
tk.END, f"[WARNING] Write EEPROM aborted.\n"
)
self.config(cursor="")
self.update_idletasks()
def write_eeprom_values(dict_addr_val):
try:
for oid, value in dict_addr_val.items():
if not self.printer.write_eeprom(
oid, value, label="write_eeprom"
):
return False
except Exception as e:
self.handle_printer_error(e)
self.status_text.insert(
tk.END, f"[INFO] Write EEPROM completed.\n"
)
self.config(cursor="")
self.update_idletasks()
self.show_status_text_view()
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
self.status_text.insert(tk.END, NO_CONF_ERROR)
return
dict_addr_val = get_input()
if dict_addr_val is not None:
self.config(cursor="watch")
self.update()
self.status_text.insert(
tk.END, f"[INFO] Going to write EEPROM: {dict_addr_val}.\n"
)
self.after(200, lambda: dialog_write_values(dict_addr_val))
2024-07-28 13:15:10 -04:00
def reset_waste_ink(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
ip_address = self.ip_var.get()
if (
not self._is_valid_ip(ip_address)
or not self.printer
or not self.printer.parm
or "read_key" not in self.printer.parm
or "write_key" not in self.printer.parm
):
2024-07-28 13:15:10 -04:00
self.status_text.insert(tk.END, NO_CONF_ERROR)
self.config(cursor="")
self.update_idletasks()
return
try:
if "raw_waste_reset" in self.printer.parm:
if not self.get_current_eeprom_values(
self.printer.parm["raw_waste_reset"].keys(),
2024-10-03 05:22:55 -04:00
"Raw waste reset"
):
self.config(cursor="")
self.update_idletasks()
return
for i in self.printer.parm:
if i.endswith("_waste") and "oids" in self.printer.parm[i]:
if not self.get_current_eeprom_values(
self.printer.parm[i]["oids"],
i.replace("_", " ").capitalize()
):
self.config(cursor="")
self.update_idletasks()
return
except Exception as e:
self.handle_printer_error(e)
self.config(cursor="")
self.update_idletasks()
return
response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no')
2024-09-18 22:49:14 -04:00
if not self.printer:
return
2024-08-08 20:41:19 -04:00
if response:
try:
2024-09-18 22:49:14 -04:00
self.printer.reset_waste_ink_levels()
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
"[INFO] Waste ink levels have been reset."
" Perform a power cycle of the printer now.\n"
2024-07-28 13:15:10 -04:00
)
2024-08-08 20:41:19 -04:00
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
2024-08-08 20:41:19 -04:00
else:
self.status_text.insert(
tk.END, f"[WARNING] Waste ink levels reset aborted.\n"
)
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def start_detect_printers(self):
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, "[INFO] Detecting printers... (this might take a while)\n"
)
# run printer detection in new thread, as it can take a while
2024-07-28 13:15:10 -04:00
threading.Thread(target=self.detect_printers_thread).start()
def detect_printers_thread(self, cursor=True):
if cursor:
self.config(cursor="watch")
self.update()
current_function_name = inspect.stack()[0][3]
method_to_call = getattr(self, current_function_name)
self.after(100, lambda: method_to_call(cursor=False))
return
self.detect_button.config(state=tk.DISABLED) # disable button while processing
2024-07-25 16:08:05 -04:00
self.show_status_text_view()
try:
2024-07-28 13:15:10 -04:00
printers = self.printer_scanner.get_all_printers(
self.ip_var.get().strip()
)
if len(printers) > 0:
2024-07-26 22:01:38 -04:00
if len(printers) == 1:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-09-18 22:49:14 -04:00
f"[INFO] Found printer '{printers[0]['name']}' "
f"at {printers[0]['ip']} "
f"(hostname: {printers[0]['hostname']})\n",
2024-07-28 13:15:10 -04:00
)
self.ip_var.set(printers[0]["ip"])
for model in get_printer_models(printers[0]["name"]):
2024-07-28 21:29:30 -04:00
if model in EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf
).valid_printers:
2024-07-26 22:01:38 -04:00
self.model_var.set(model)
break
if self.model_var.get() == "":
self.status_text.insert(
tk.END,
f'[ERROR] Printer model unknown.\n'
)
self.model_var.set("")
2024-07-26 22:01:38 -04:00
else:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END, f"[INFO] Found {len(printers)} printers:\n"
)
2024-07-26 22:01:38 -04:00
for printer in printers:
2024-07-28 13:15:10 -04:00
self.status_text.insert(
tk.END,
2024-09-18 22:49:14 -04:00
f"[INFO] {printer['name']} found at {printer['ip']}"
f" (hostname: {printer['hostname']})\n",
2024-07-28 13:15:10 -04:00
)
else:
self.status_text.insert(tk.END, "[WARN] No printers found.\n")
except Exception as e:
2024-09-20 02:35:28 -04:00
self.handle_printer_error(e)
finally:
2024-07-28 13:15:10 -04:00
self.detect_button.config(state=tk.NORMAL) # enable button after processing
self.config(cursor="")
self.update_idletasks()
2024-07-28 13:15:10 -04:00
def _is_valid_ip(self, ip):
try:
ip = ipaddress.ip_address(ip)
return True
except ValueError:
return False
2024-07-25 16:08:05 -04:00
def is_simple_type(self, data):
return isinstance(data, (str, int, float, bool))
def contains_parentheses(self, data):
"""Check if a string representation contains parentheses."""
if isinstance(data, (list, tuple, set)):
for item in data:
if isinstance(item, (tuple, list, set)):
return True
2024-07-28 13:15:10 -04:00
if isinstance(item, str) and ("(" in item or ")" in item):
2024-07-25 16:08:05 -04:00
return True
return False
def populate_treeview(self, parent, treeview, data):
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, (dict, list, set, tuple)):
2024-07-28 13:15:10 -04:00
node = treeview.insert(
parent, "end", text=key, tags=("key",)
)
2024-07-25 16:08:05 -04:00
self.populate_treeview(node, treeview, value)
else:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent,
"end",
text=f"{key}: {value}",
tags=("key_value"),
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, list):
2024-07-28 13:15:10 -04:00
if all(
self.is_simple_type(item) for item in data
) and not self.contains_parentheses(data):
treeview.insert(
parent,
"end",
text=", ".join(map(str, data)),
tags=("value",),
)
2024-07-25 16:08:05 -04:00
else:
for item in data:
if isinstance(item, (dict, list, set, tuple)):
self.populate_treeview(parent, treeview, item)
else:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent, "end", text=str(item), tags=("value",)
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, set):
if not self.contains_parentheses(data):
2024-07-28 13:15:10 -04:00
treeview.insert(
parent,
"end",
text=", ".join(map(str, data)),
tags=("value",),
)
2024-07-25 16:08:05 -04:00
else:
for item in data:
2024-07-28 13:15:10 -04:00
treeview.insert(
parent, "end", text=str(item), tags=("value",)
)
2024-07-25 16:08:05 -04:00
elif isinstance(data, tuple):
2024-07-28 13:15:10 -04:00
treeview.insert(parent, "end", text=str(data), tags=("value",))
2024-07-25 16:08:05 -04:00
else:
2024-07-28 13:15:10 -04:00
treeview.insert(parent, "end", text=str(data), tags=("value",))
2024-07-25 16:08:05 -04:00
def expand_all(self, treeview):
def recursive_expand(item):
treeview.item(item, open=True)
children = treeview.get_children(item)
for child in children:
recursive_expand(child)
root_children = treeview.get_children()
for child in root_children:
recursive_expand(child)
def show_context_menu(self, event):
"""Show the context menu."""
# Select the item under the cursor
item = self.tree.identify_row(event.y)
if item:
self.tree.selection_set(item)
self.context_menu.post(event.x_root, event.y_root)
def copy_selected_item(self):
"""Copy the selected Treeview item text to the clipboard."""
selected_item = self.tree.selection()
if selected_item:
item_text = self.tree.item(selected_item[0], "text")
self.clipboard_clear()
self.clipboard_append(item_text)
2024-09-18 22:49:14 -04:00
def copy_all_items(self):
"""Copy all items to the clipboard."""
self.clipboard_clear()
self.clipboard_append(self.text_dump)
def print_items(self):
"""Send items to the printer."""
exit_packet_mode = b'\x00\x00\x00\x1b\x01@EJL 1284.4\n@EJL \n'
initialize_printer = b"\x1B\x40"
form_feed = b"\f"
2024-09-18 22:49:14 -04:00
self.clipboard_append(self.text_dump)
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
return
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((ip_address, 9100))
sock.sendall(
exit_packet_mode
+ initialize_printer
+ b"Printer configuration\n"
+ self.text_dump.encode('utf-8')
+ form_feed
)
except Exception as e:
self.handle_printer_error(e)
2024-09-18 22:49:14 -04:00
2024-07-26 22:01:38 -04:00
2024-08-13 12:01:12 -04:00
def main():
2024-07-28 21:29:30 -04:00
import argparse
import pickle
parser = argparse.ArgumentParser(
epilog='epson_print_conf GUI'
)
2024-09-18 22:49:14 -04:00
parser.add_argument(
'-m',
'--model',
dest='model',
action="store",
help='Printer model. Example: -m XP-205',
default=None
)
parser.add_argument(
'-a',
'--address',
dest='hostname',
action="store",
help='Printer host name or IP address. (Example: -a 192.168.1.87)',
default=None
)
2024-07-28 21:29:30 -04:00
parser.add_argument(
'-P',
"--pickle",
dest='pickle',
type=argparse.FileType('rb'),
2024-07-29 19:18:02 -04:00
help="Load a pickle configuration archive saved by parse_devices.py",
2024-07-28 21:29:30 -04:00
default=None,
nargs=1,
metavar='PICKLE_FILE'
)
parser.add_argument(
'-O',
"--override",
dest='override',
action='store_true',
2024-07-29 19:18:02 -04:00
help="Replace the default configuration with the one in the pickle "
"file instead of merging (default is to merge)",
2024-07-28 21:29:30 -04:00
)
parser.add_argument(
'-d',
'--debug',
dest='debug',
action='store_true',
help='Print debug information'
)
2024-07-28 21:29:30 -04:00
args = parser.parse_args()
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
2024-07-28 21:29:30 -04:00
conf_dict = {}
if args.pickle:
conf_dict = pickle.load(args.pickle[0])
2024-09-18 22:49:14 -04:00
return EpsonPrinterUI(
model=args.model,
hostname=args.hostname,
conf_dict=conf_dict,
replace_conf=args.override
)
2024-08-13 12:01:12 -04:00
if __name__ == "__main__":
try:
main().mainloop()
2024-09-18 22:49:14 -04:00
except KeyboardInterrupt:
2024-08-08 20:41:19 -04:00
print("\nInterrupted.")
sys.exit(0)