UI menu bar, import/export conf files from the UI, exclude L3250

This commit is contained in:
Ircama
2024-11-03 13:13:56 +01:00
parent 1f993d77a2
commit 6304a02a83
5 changed files with 434 additions and 89 deletions

388
ui.py
View File

@@ -5,6 +5,7 @@
Epson Printer Configuration via SNMP (TCP/IP) - GUI
"""
import os
import sys
import re
import threading
@@ -15,6 +16,7 @@ import socket
import traceback
import logging
import webbrowser
import pickle
import black
import tkinter as tk
@@ -22,14 +24,15 @@ from tkinter import ttk, Menu
from tkinter.scrolledtext import ScrolledText
import tkinter.font as tkfont
from tkcalendar import DateEntry # Ensure you have: pip install tkcalendar
from tkinter import simpledialog, messagebox
from tkinter import simpledialog, messagebox, filedialog
import pyperclip
from epson_print_conf import EpsonPrinter
from epson_print_conf import EpsonPrinter, get_printer_models
from parse_devices import generate_config_from_toml, generate_config_from_xml, normalize_config
from find_printers import PrinterScanner
VERSION = "4.0"
VERSION = "5.0"
NO_CONF_ERROR = (
"[ERROR] Please select a printer model and a valid IP address,"
@@ -44,40 +47,6 @@ CONFIRM_MESSAGE = (
"Are you sure you want to proceed?"
)
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=""):
@@ -205,7 +174,7 @@ class EpsonPrinterUI(tk.Tk):
self,
model: str = None,
hostname: str = None,
conf_dict={},
conf_dict = {},
replace_conf=False
):
super().__init__()
@@ -225,6 +194,89 @@ class EpsonPrinterUI(tk.Tk):
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# Setup the menu
menubar = tk.Menu(self)
self.config(menu=menubar)
# Create File menu
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", menu=file_menu)
LOAD_LABEL_NAME = "%s printer configuration file or web URL..."
LOAD_LABEL_TITLE = "Select a %s printer configuration file, or enter a Web URL"
LOAD_LABEL_TYPE = "%s files"
file_menu.add_command(
label=LOAD_LABEL_NAME % "Load a PICKLE",
command=lambda: self.load_from_file(
file_type={
"title": LOAD_LABEL_TITLE % "PICKLE",
"filetypes": [
(LOAD_LABEL_TYPE % "PICKLE", "*.pickle"),
("All files", "*.*")
]
},
type=0
)
)
file_menu.add_command(
label=LOAD_LABEL_NAME % "Import a XML",
command=lambda: self.load_from_file(
file_type={
"title": LOAD_LABEL_TITLE % "XML",
"filetypes": [
(LOAD_LABEL_TYPE % "XML", "*.xml"),
("All files", "*.*")
]
},
type=1
)
)
file_menu.add_command(
label=LOAD_LABEL_NAME % "Import a TOML",
command=lambda: self.load_from_file(
file_type={
"title": LOAD_LABEL_TITLE % "TOML",
"filetypes": [
(LOAD_LABEL_TYPE % "TOML", "*.toml"),
("All files", "*.*")
]
},
type=2
)
)
file_menu.add_command(
label="Save the selected printer configuration to a PICKLE file...",
command=self.save_to_file
)
# Create Help menu
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Settings", menu=help_menu)
help_menu.add_command(label="Show printer parameters of the selected model", command=self.printer_config)
help_menu.entryconfig("Show printer parameters of the selected model", accelerator="F2")
help_menu.add_command(label="Show printer keys of the selected model", command=self.key_values)
help_menu.entryconfig("Show printer keys of the selected model", accelerator="F3")
help_menu.add_command(label="Remove selected printer configuration", command=self.remove_printer_conf)
help_menu.entryconfig("Remove selected printer configuration", accelerator="F4")
help_menu.add_command(label="Keep only selected printer configuration", command=self.keep_printer_conf)
help_menu.entryconfig("Keep only selected printer configuration", accelerator="F5")
help_menu.add_command(label="Clear printer list", command=self.clear_printer_list)
help_menu.entryconfig("Clear printer list", accelerator="F6")
help_menu.add_command(label="Get next local IP addresss", command=lambda: self.next_ip(0))
help_menu.entryconfig("Get next local IP addresss", accelerator="F9")
# Create Help menu
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="Help", command=self.open_help_browser)
help_menu.add_command(label="Program Information", command=self.show_program_info)
# Setup frames
FRAME_PAD = 10
PAD = (3, 0)
PADX = 4
@@ -277,12 +329,14 @@ class EpsonPrinterUI(tk.Tk):
)
ToolTip(
self.model_dropdown,
"Select the model of the printer, or press 'Detect Printers'.\n"
"Press F2 to dump the parameters associated to the printer model."
" Press F3 to get the values of the keys from the configuration.",
"Select the model of the printer, or press 'Detect Printers'."
" Special features are allowed via F2, F3, F4, F5, or F6.\n"
)
self.model_dropdown.bind("<F2>", self.printer_config)
self.model_dropdown.bind("<F3>", self.key_values)
self.model_dropdown.bind("<F4>", lambda event: self.remove_printer_conf())
self.model_dropdown.bind("<F5>", lambda event: self.keep_printer_conf())
self.model_dropdown.bind("<F6>", lambda event: self.clear_printer_list())
# BOX IP address
ip_frame = ttk.LabelFrame(
@@ -310,13 +364,13 @@ class EpsonPrinterUI(tk.Tk):
self.ip_entry.grid(
row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E)
)
self.ip_entry.bind("<F2>", self.next_ip)
self.ip_entry.bind("<F9>", self.next_ip)
ToolTip(
self.ip_entry,
"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,"
" or press F9 more times to get the next local IP address,"
" which can then be edited"
" (by removing the last part before pressing 'Detect Printers').",
)
@@ -779,6 +833,219 @@ class EpsonPrinterUI(tk.Tk):
self.ip_var.trace('w', self.change_widget_states)
self.change_widget_states()
def save_to_file(self):
if not self.model_var.get():
self.show_status_text_view()
self.status_text.insert(
tk.END,
'[ERROR]: Unknown printer model.'
)
return
if not self.printer:
self.printer = EpsonPrinter(
conf_dict=self.conf_dict,
model=self.model_var.get(),
)
if not self.printer or not self.printer.parm:
self.show_status_text_view()
self.status_text.insert(
tk.END,
'[ERROR]: No printer configuration defined.'
)
return
# Open file dialog to enter the file
file_path = filedialog.asksaveasfilename(
defaultextension=".pickle",
title="PICKLE file name",
initialfile=self.model_var.get(),
filetypes=[("PICKLE files", "*.pickle")]
)
if not file_path:
self.show_status_text_view()
self.status_text.insert(
tk.END,
f"[WARNING] File save operation aborted.\n"
)
return
# Ensure the file has the desired extension
if "." not in file_path and not file_path.endswith(".pickle"):
file_path += ".pickle"
normalized_config = { self.model_var.get(): self.printer.parm.copy() }
normalized_config["internal_data"] = {}
normalized_config["internal_data"]["default_model"] = self.model_var.get()
if self.ip_var.get():
normalized_config["internal_data"]["hostname"] = self.ip_var.get()
try:
with open(file_path, "wb") as file:
pickle.dump(normalized_config, file) # serialize the list
except Exception:
self.show_status_text_view()
self.status_text.insert(
tk.END,
f"[ERROR] File save operation failed.\n"
)
return
self.status_text.insert(
tk.END,
f'[INFO] "{os.path.basename(file_path)}" file save operation completed.\n'
)
def load_from_file(self, file_type, type):
# Open file dialog to select the file
file_path = filedialog.askopenfilename(**file_type)
if not file_path:
self.show_status_text_view()
self.status_text.insert(
tk.END,
f"[WARNING] File load operation aborted.\n"
)
return
if type == 0:
try:
with open(file_path, 'rb') as pickle_file:
self.conf_dict = pickle.load(pickle_file)
except Exception as e:
self.show_status_text_view()
if not file_path.tell():
self.status_text.insert(
tk.END,
f"[ERROR] Empty PICKLE FILE {file_path}.\n"
)
else:
self.status_text.insert(
tk.END,
f"[ERROR] Cannot load PICKLE file {file_path}. {e}\n"
)
return
if (
"internal_data" in self.conf_dict
and "hostname" in self.conf_dict["internal_data"]
):
self.ip_var.set(self.conf_dict["internal_data"]["hostname"])
if (
"internal_data" in self.conf_dict
and "default_model" in self.conf_dict["internal_data"]
):
self.model_var.set(self.conf_dict["internal_data"]["default_model"])
else:
self.status_text.insert(
tk.END,
f"[INFO] Converting file, please wait...\n"
)
self.update_idletasks()
if type == 1:
printer_config = generate_config_from_xml(config=file_path)
if type == 2:
printer_config = generate_config_from_toml(config=file_path)
if not printer_config:
self.show_status_text_view()
self.status_text.insert(
tk.END,
f"[ERROR] Cannot load file {file_path}\n"
)
return
self.conf_dict = normalize_config(config=printer_config)
self.model_dropdown["values"] = sorted(EpsonPrinter(
conf_dict=self.conf_dict,
replace_conf=self.replace_conf
).valid_printers)
if file_path:
self.show_status_text_view()
self.status_text.insert(
tk.END,
f"[INFO] Loaded file {os.path.basename(file_path)}.\n"
)
def keep_printer_conf(self):
self.show_status_text_view()
if not self.model_var.get():
self.status_text.insert(
tk.END,
'[ERROR]: Select a valid printer model.\n'
)
return
keep_model = self.model_var.get()
self.model_dropdown["values"] = tuple(
model for model in self.model_dropdown["values"] if model == keep_model
)
self.replace_conf = True
self.show_status_text_view()
self.update_idletasks()
self.status_text.insert(
tk.END,
f"[INFO] Printer {keep_model} is the only one in the list.\n"
)
def remove_printer_conf(self):
self.show_status_text_view()
if not self.model_var.get():
self.status_text.insert(
tk.END,
'[ERROR]: Select a valid printer model.\n'
)
return
remove_model = self.model_var.get()
self.model_var.set("")
self.model_dropdown["values"] = tuple(
model for model in self.model_dropdown["values"] if model != remove_model
)
self.replace_conf = True
self.show_status_text_view()
self.update_idletasks()
self.status_text.insert(
tk.END,
f"[INFO] Configuation of printer {remove_model} removed.\n"
)
def clear_printer_list(self):
self.conf_dict = {}
self.model_var.set("")
self.model_dropdown["values"] = {}
self.replace_conf = True
self.show_status_text_view()
self.update_idletasks()
self.status_text.insert(
tk.END,
f"[INFO] Printer list cleared.\n"
)
def open_help_browser(self):
# Opens a web browser to a help URL
url = "https://github.com/Ircama/epson_print_conf/?tab=readme-ov-file#epson_print_conf"
self.show_status_text_view()
try:
ret = webbrowser.open(url)
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 show_program_info(self):
# Show program information in a popup
program_version = "1.0.0" # Specify your program version
description = """
Epson Printer Configuration tool via SNMP (TCP/IP).
A tool for managing settings of Epson printers connected via Wi-Fi over the SNMP protocol.
Web site: https://github.com/Ircama/epson_print_conf
"""
self.title("Epson Printer Configuration - v" + VERSION)
messagebox.showinfo("Program Information",
f"Version: {VERSION}\n{description}"
)
def focus_next(self, event):
event.widget.tk_focusNext().focus()
return("break")
@@ -1783,6 +2050,7 @@ class EpsonPrinterUI(tk.Tk):
return
if not self.printer:
self.printer = EpsonPrinter(
conf_dict=self.conf_dict,
hostname=self.ip_var.get()
)
self.printer.parm = {'read_key': None}
@@ -1796,16 +2064,17 @@ class EpsonPrinterUI(tk.Tk):
read_key = None
try:
read_key = self.printer.brute_force_read_key()
self.status_text.insert(
tk.END, f"[INFO] Detected read_key: {read_key}.\n"
)
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:
if read_key:
self.status_text.insert(
tk.END, f"[INFO] Detected read_key: {read_key}.\n"
)
else:
self.status_text.insert(
tk.END, f"[ERROR] Could not detect read_key.\n"
)
@@ -2163,13 +2432,14 @@ class EpsonPrinterUI(tk.Tk):
try:
addr = range(0, 2048)
eeprom = {
k: int(v, 16) for k, v in zip(
addr,self.printer.read_eeprom_many(
k: None if v == None else int(v, 16) for k, v in zip(
addr,
self.printer.read_eeprom_many(
addr, label="dump_EEPROM"
)
)
}
if not eeprom:
if not eeprom or eeprom == {0: None}:
self.status_text.insert(
tk.END,
'[ERROR] Cannot read EEPROM values'
@@ -2658,6 +2928,10 @@ class EpsonPrinterUI(tk.Tk):
ip_address = self.ip_var.get()
if not self._is_valid_ip(ip_address):
self.show_status_text_view()
self.status_text.insert(
tk.END, f"[ERROR] Missing IP address or printer host name.\n"
)
return
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
@@ -2670,7 +2944,10 @@ class EpsonPrinterUI(tk.Tk):
+ form_feed
)
except Exception as e:
self.handle_printer_error(e)
self.show_status_text_view()
self.status_text.insert(
tk.END, f"[ERROR] Printer is unreachable or offline.\n"
)
def main():
@@ -2726,7 +3003,14 @@ def main():
logging.getLogger().setLevel(logging.DEBUG)
conf_dict = {}
if args.pickle:
conf_dict = pickle.load(args.pickle[0])
try:
conf_dict = pickle.load(args.pickle[0])
except Exception as e:
if not args.pickle[0].tell():
print("Error. Empty PICKLE FILE.")
else:
print("Error. Cannot load PICKLE FILE:", e)
quit()
return EpsonPrinterUI(
model=args.model,