155 lines
6.2 KiB
Python
155 lines
6.2 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk
|
|
import threading
|
|
import requests
|
|
import usb.core
|
|
import usb.util
|
|
import usb.backend.libusb1
|
|
import os
|
|
import time
|
|
|
|
VENDOR_ID = 0xFFFF
|
|
PRODUCT_ID = 0x0035
|
|
|
|
HID_MAP = {
|
|
4: 'a', 5: 'b', 6: 'c', 7: 'd', 8: 'e', 9: 'f', 10: 'g', 11: 'h', 12: 'i',
|
|
13: 'j', 14: 'k', 15: 'l', 16: 'm', 17: 'n', 18: 'o', 19: 'p', 20: 'q',
|
|
21: 'r', 22: 's', 23: 't', 24: 'u', 25: 'v', 26: 'w', 27: 'x', 28: 'y', 29: 'z',
|
|
30: '1', 31: '2', 32: '3', 33: '4', 34: '5', 35: '6', 36: '7', 37: '8', 38: '9', 39: '0',
|
|
44: ' ', 45: '-', 46: '=', 55: '.', 56: '/'
|
|
}
|
|
|
|
class POSBridgeApp:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("POS Hardware Bridge")
|
|
self.root.geometry("500x320")
|
|
self.running = True
|
|
|
|
# UI Setup
|
|
ttk.Label(root, text="Target POS Endpoint:").pack(pady=(15, 2))
|
|
self.url_var = tk.StringVar(value="https://scanner.sekidesu.xyz/scan")
|
|
self.url_entry = ttk.Entry(root, textvariable=self.url_var, width=60)
|
|
self.url_entry.pack(pady=5)
|
|
|
|
self.status_var = tk.StringVar(value="Status: Booting...")
|
|
self.status_label = ttk.Label(root, textvariable=self.status_var, font=("Segoe UI", 10, "bold"))
|
|
self.status_label.pack(pady=10)
|
|
|
|
ttk.Label(root, text="Activity Log:").pack()
|
|
self.log_listbox = tk.Listbox(root, width=70, height=8, bg="#1e1e1e", fg="#00ff00", font=("Consolas", 9))
|
|
self.log_listbox.pack(pady=5, padx=10)
|
|
|
|
# Bind the close button to kill threads cleanly
|
|
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
|
|
|
|
# Fire up the USB listener in a background thread
|
|
self.usb_thread = threading.Thread(target=self.usb_listen_loop, daemon=True)
|
|
self.usb_thread.start()
|
|
|
|
def log(self, message):
|
|
# Tkinter requires GUI updates to happen on the main thread
|
|
self.root.after(0, self._append_log, message)
|
|
|
|
def _append_log(self, message):
|
|
self.log_listbox.insert(0, time.strftime("[%H:%M:%S] ") + message)
|
|
if self.log_listbox.size() > 15:
|
|
self.log_listbox.delete(15)
|
|
|
|
def update_status(self, text, color="black"):
|
|
self.root.after(0, self._set_status, text, color)
|
|
|
|
def _set_status(self, text, color):
|
|
self.status_var.set(f"Status: {text}")
|
|
self.status_label.config(foreground=color)
|
|
|
|
def on_close(self):
|
|
self.running = False
|
|
self.root.destroy()
|
|
|
|
def send_to_pos(self, barcode):
|
|
url = self.url_var.get()
|
|
self.log(f"Captured: {barcode}. Sending...")
|
|
try:
|
|
resp = requests.get(url, params={'content': barcode}, timeout=3)
|
|
self.log(f"Success: POS returned {resp.status_code}")
|
|
except requests.RequestException as e:
|
|
self.log(f"HTTP Error: Backend unreachable")
|
|
|
|
def usb_listen_loop(self):
|
|
import sys
|
|
# PyInstaller extracts files to a temp _MEIPASS folder at runtime
|
|
if getattr(sys, 'frozen', False):
|
|
base_path = sys._MEIPASS
|
|
else:
|
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
dll_path = os.path.join(base_path, "libusb-1.0.dll")
|
|
|
|
if not os.path.exists(dll_path):
|
|
self.update_status(f"CRITICAL: DLL missing at {dll_path}", "red")
|
|
return
|
|
|
|
backend = usb.backend.libusb1.get_backend(find_library=lambda x: dll_path)
|
|
|
|
while self.running:
|
|
# Reconnect loop
|
|
dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID, backend=backend)
|
|
|
|
if dev is None:
|
|
self.update_status("Scanner unplugged. Waiting...", "red")
|
|
time.sleep(2)
|
|
continue
|
|
|
|
try:
|
|
dev.set_configuration()
|
|
cfg = dev.get_active_configuration()
|
|
intf = cfg[(0,0)]
|
|
endpoint = usb.util.find_descriptor(
|
|
intf,
|
|
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
|
|
)
|
|
|
|
self.update_status("Scanner Locked & Ready", "green")
|
|
current_barcode = ""
|
|
|
|
# Active reading loop
|
|
while self.running:
|
|
try:
|
|
data = dev.read(endpoint.bEndpointAddress, endpoint.wMaxPacketSize, timeout=1000)
|
|
|
|
keycode = data[2]
|
|
modifier = data[0]
|
|
is_shift = modifier == 2 or modifier == 32
|
|
|
|
if keycode == 0:
|
|
continue
|
|
|
|
if keycode == 40: # Enter key signifies end of scan
|
|
if current_barcode:
|
|
# Spawn a micro-thread for the HTTP request so we don't block the next scan
|
|
threading.Thread(target=self.send_to_pos, args=(current_barcode,), daemon=True).start()
|
|
current_barcode = ""
|
|
elif keycode in HID_MAP:
|
|
char = HID_MAP[keycode]
|
|
if is_shift and char.isalpha():
|
|
char = char.upper()
|
|
current_barcode += char
|
|
|
|
except usb.core.USBError as e:
|
|
# 10060/110 are normal timeouts when no barcode is being actively scanned
|
|
if e.args[0] in (10060, 110):
|
|
continue
|
|
else:
|
|
self.log(f"Hardware interrupt lost. Reconnecting...")
|
|
break # Breaks inner loop to trigger outer reconnect loop
|
|
|
|
except Exception as e:
|
|
self.log(f"USB Error: {e}")
|
|
time.sleep(2) # Prevent rapid crash loops
|
|
|
|
if __name__ == '__main__':
|
|
# You must run pip install requests if you haven't already
|
|
root = tk.Tk()
|
|
app = POSBridgeApp(root)
|
|
root.mainloop() |