From befa212a4fab501e5f51a2a0e79ded80c7593c1e Mon Sep 17 00:00:00 2001 From: Mareks <47260097+sshsphere@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:40:38 +0300 Subject: [PATCH] Add user interface (#6) - Add user interface - Add L386 - Fix pysnmp for newer python versions - Add requirements.txt - Add printer discovery script - Add UI script - Fix ping opening window in pyinstaller exe - remove scapy from requirements.txt - improve comment formatting - scan all local ips - Fix power off timer write function - Update L386 config --- epson_print_conf.py | 47 ++++++++++++++++- find_printers.py | 81 ++++++++++++++++++++++++++++ requirements.txt | 4 ++ ui.py | 126 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 find_printers.py create mode 100644 requirements.txt create mode 100644 ui.py diff --git a/epson_print_conf.py b/epson_print_conf.py index 39cd2f5..c16124c 100644 --- a/epson_print_conf.py +++ b/epson_print_conf.py @@ -16,7 +16,17 @@ import logging import os import yaml from pathlib import Path + +# The pysnmp module uses functionality from importlib.util and +# importlib.machinery, which were seperated from the importlib module +# in python>=3.11 +try: + import importlib.util + import importlib.machinery +except ImportError: + pass from pysnmp.hlapi.v1arch import * + from pyasn1.type.univ import OctetString as OctetStringType from itertools import chain @@ -25,6 +35,39 @@ class EpsonPrinter: """SNMP Epson Printer Configuration.""" PRINTER_CONFIG = { # Known Epson models + "L386": { + "read_key": [16, 8], + "write_key": b"Sinabung", + "printer_head_id_h": range(122, 126), + "printer_head_id_f": [129], + "main_waste": {"oids": [24, 25, 30], "divider": 62.07}, + "borderless_waste": {"oids": [26, 27, 34], "divider": 24.2}, + "raw_waste_reset": { + 24: 0, 25: 0, 30: 0, # Data of 1st counter + 28: 0, 29: 0, # another store of 1st counter + 46: 94, # Maintenance required level of 1st counter + 26: 0, 27: 0, 34: 0, # Data of 2nd counter + 47: 94, # Maintenance required level of 2nd counter + 49: 0 # ? + }, + "stats": { + "Manual cleaning counter": [147], + "Timer cleaning counter": [149], + "Power cleaning counter": [148], + "Total print pass counter": [171, 170, 169, 168], + "Total print page counter": [167, 166, 165, 164], + "Total scan counter": [471, 470, 469, 468], + "First TI received time": [173, 172], + "Maintenance required level of 1st waste ink counter": [46], + "Maintenance required level of 2nd waste ink counter": [47], + "Power off timer": [359, 358], + }, + "serial_number": range(192, 202), + "last_printer_fatal_errors": [ + 453, 452, 455, 454, 457, 456, 459, 458, 461, 460, 467, + 499, 498, 501, 500, 503, 502, 505, 504, 507, 506 + ], + }, "XP-205": { "alias": ["XP-200", "XP-207"], "read_key": [25, 7], @@ -1702,8 +1745,8 @@ class EpsonPrinter: logging.error("EpsonPrinter - invalid API usage") return None try: - msb = self.parm["stats"]["poweroff_timer"][0] - lsb = self.parm["stats"]["poweroff_timer"][1] + msb = self.parm["stats"]["Power off timer"][0] + lsb = self.parm["stats"]["Power off timer"][1] except KeyError: logging.info("write_poweroff_timer: missing parameter") return False diff --git a/find_printers.py b/find_printers.py new file mode 100644 index 0000000..358efa6 --- /dev/null +++ b/find_printers.py @@ -0,0 +1,81 @@ +import os +import socket +import subprocess +import threading +import warnings + +from epson_print_conf import EpsonPrinter + + +# suppress pysnmp warnings +warnings.filterwarnings("ignore", category=SyntaxWarning) + +# common printer ports +PRINTER_PORTS = [9100, 515, 631] + +class PrinterScanner: + + def ping(self, host): + result = subprocess.run(['ping', '-n', '1', host], stdout=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW) + return 'Reply from' in result.stdout.decode('utf-8') + + def check_printer(self, ip, port): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + sock.connect((ip, port)) + sock.close() + return True + except socket.error: + return False + + def get_printer_name(self, ip): + printer = EpsonPrinter(hostname=ip) + try: + printer_info = printer.get_snmp_info("Model") + return printer_info["Model"] + except: + return None + + def scan_ip(self, ip): + if self.ping(ip): + for port in PRINTER_PORTS: + if self.check_printer(ip, port): + try: + hostname = socket.gethostbyaddr(ip)[0] + except socket.herror: + hostname = "Unknown" + + printer_name = self.get_printer_name(ip) + if printer_name: + return {"ip": ip, "hostname": hostname, "name": printer_name} + else: + return {"ip": ip, "hostname": hostname, "name": "Unknown"} + return None + def get_all_printers(self): + local_device_ip_list = socket.gethostbyname_ex(socket.gethostname())[2] + for local_device_ip in local_device_ip_list: + base_ip = local_device_ip[:local_device_ip.rfind('.') + 1] + ips=[f"{base_ip}{i}" for i in range(1, 255)] + printers = [] + threads = [] + + def worker(ip): + result = self.scan_ip(ip) + if result: + printers.append(result) + + for ip in ips: + thread = threading.Thread(target=worker, args=(ip,)) + threads.append(thread) + thread.start() + + for thread in threads: + thread.join() + + return printers + + +if __name__ == "__main__": + scanner = PrinterScanner() + print(scanner.get_all_printers()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b86dc77 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +PyYAML +git+https://github.com/etingof/pysnmp.git@master#egg=pysnmp +pyasyncore;python_version>="3.12" + diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..91ba41f --- /dev/null +++ b/ui.py @@ -0,0 +1,126 @@ +import tkinter as tk +from tkinter import ttk +from tkinter.scrolledtext import ScrolledText +import threading +import ipaddress +from epson_print_conf import EpsonPrinter +from find_printers import PrinterScanner +from pprint import pformat + +class EpsonPrinterUI(tk.Tk): + def __init__(self): + super().__init__() + self.title("Epson Printer Configuration") + self.geometry("450x400") + + # configure the main window to be resizable + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + # main Frame + main_frame = ttk.Frame(self, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + main_frame.columnconfigure(0, weight=1) + main_frame.rowconfigure(3, weight=1) + + # printer model selection + model_frame = ttk.LabelFrame(main_frame, text="Printer Model", padding="10") + model_frame.grid(row=0, column=0, pady=10, sticky=(tk.W, tk.E)) + model_frame.columnconfigure(1, weight=1) + + self.model_var = tk.StringVar() + ttk.Label(model_frame, text="Select Printer Model:").grid(row=0, column=0, sticky=tk.W, padx=5) + self.model_dropdown = ttk.Combobox(model_frame, textvariable=self.model_var) + self.model_dropdown['values'] = sorted(list(EpsonPrinter.PRINTER_CONFIG.keys())) + self.model_dropdown.grid(row=0, column=1, pady=5, padx=5, sticky=(tk.W, tk.E)) + + # IP address entry + ip_frame = ttk.LabelFrame(main_frame, text="Printer IP Address", padding="10") + ip_frame.grid(row=1, column=0, pady=10, sticky=(tk.W, tk.E)) + ip_frame.columnconfigure(1, weight=1) + + self.ip_var = tk.StringVar() + ttk.Label(ip_frame, text="Enter Printer IP Address:").grid(row=0, column=0, sticky=tk.W, padx=5) + self.ip_entry = ttk.Entry(ip_frame, textvariable=self.ip_var) + self.ip_entry.grid(row=0, column=1, pady=5, padx=5, sticky=(tk.W, tk.E)) + + # buttons + button_frame = ttk.Frame(main_frame, padding="10") + button_frame.grid(row=2, column=0, pady=10, sticky=(tk.W, tk.E)) + button_frame.columnconfigure((0, 1, 2), weight=1) + + self.detect_button = ttk.Button(button_frame, text="Detect Printers", command=self.start_detect_printers) + self.detect_button.grid(row=0, column=0, padx=5, pady=5, sticky=(tk.W, tk.E)) + + self.status_button = ttk.Button(button_frame, text="Print Status", command=self.print_status) + self.status_button.grid(row=0, column=1, padx=5, pady=5, sticky=(tk.W, tk.E)) + + self.reset_button = ttk.Button(button_frame, text="Reset Waste Ink Levels", command=self.reset_waste_ink) + self.reset_button.grid(row=0, column=2, padx=5, pady=5, sticky=(tk.W, tk.E)) + + # status display + status_frame = ttk.LabelFrame(main_frame, text="Status", padding="10") + status_frame.grid(row=3, column=0, pady=10, sticky=(tk.W, tk.E, tk.N, tk.S)) + status_frame.columnconfigure(0, weight=1) + status_frame.rowconfigure(0, weight=1) + + self.status_text = ScrolledText(status_frame, height=10, width=50, wrap=tk.WORD) + self.status_text.grid(row=0, column=0, pady=5, padx=5, sticky=(tk.W, tk.E, tk.N, tk.S)) + + def print_status(self): + model = self.model_var.get() + ip_address = self.ip_var.get() + if not model or not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, "[ERROR] Please select a printer model and enter a valid IP address.\n") + return + printer = EpsonPrinter(model=model, hostname=ip_address) + + try: + self.status_text.insert(tk.END, f"[INFO] {pformat(printer.stats())}\n") + except Exception as e: + self.status_text.insert(tk.END, f"[ERROR] {e}\n") + + def reset_waste_ink(self): + model = self.model_var.get() + ip_address = self.ip_var.get() + if not model or not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, "[ERROR] Please select a printer model and enter a valid IP address.\n") + return + printer = EpsonPrinter(model=model, hostname=ip_address) + try: + printer.reset_waste_ink_levels() + self.status_text.insert(tk.END, "[INFO] Waste ink levels have been reset.\n") + except Exception as e: + self.status_text.insert(tk.END, f"[ERROR] {e}\n") + + def start_detect_printers(self): + self.status_text.insert(tk.END, "[INFO] Detecting printers... (this might take a while)\n") + self.detect_button.config(state=tk.DISABLED) # disable button while processing + + # run printer detection in new thread, as it can take a while + threading.Thread(target=self.detect_printers).start() + + def detect_printers(self): + printer_scanner=PrinterScanner() + try: + printers = printer_scanner.get_all_printers() + if len(printers) > 0: + for printer in printers: + self.status_text.insert(tk.END, f"[INFO] {printer['name']} found at {printer['ip']} (hostname: {printer['hostname']})\n") + else: + self.status_text.insert(tk.END, "[WARN] No printers found.\n") + except Exception as e: + self.status_text.insert(tk.END, f"[ERROR] {e}\n") + finally: + self.detect_button.config(state=tk.NORMAL) # enable button after processing + + def _is_valid_ip(self,ip): + try: + ip = ipaddress.ip_address(ip) + return True + except ValueError: + return False + +if __name__ == "__main__": + app = EpsonPrinterUI() + app.mainloop()