From 2018e987cce133c7a7fd61af67d9977f09567a8c Mon Sep 17 00:00:00 2001 From: Fai Luk Date: Fri, 17 Apr 2026 02:32:54 +0800 Subject: [PATCH] no message --- python/Bag3.py | 790 +++++++++++++++++++++++++++++++-------- python/installAndExe.txt | 16 +- 2 files changed, 628 insertions(+), 178 deletions(-) diff --git a/python/Bag3.py b/python/Bag3.py index 74ea4e3..63b5d83 100644 --- a/python/Bag3.py +++ b/python/Bag3.py @@ -31,6 +31,18 @@ from typing import Callable, Optional, Tuple import requests +# UI "列印機" check uses short TCP probes; DataFlex may refuse connections briefly during E1005 recovery. +_DATAFLEX_RECOVERY_GRACE_UNTIL: float = 0.0 + + +def touch_dataflex_recovery_grace(seconds: float = 22.0) -> None: + """While host reset runs, avoid flashing printer status to red.""" + global _DATAFLEX_RECOVERY_GRACE_UNTIL + u = time.time() + max(0.0, seconds) + if u > _DATAFLEX_RECOVERY_GRACE_UNTIL: + _DATAFLEX_RECOVERY_GRACE_UNTIL = u + + try: import serial except ImportError: @@ -116,16 +128,25 @@ def try_printer_connection(printer_name: str, sett: dict) -> bool: """Try to connect to the selected printer (TCP IP:port or COM). Returns True if OK.""" if printer_name == "打袋機 DataFlex": ip = (sett.get("dabag_ip") or "").strip() - port_str = (sett.get("dabag_port") or "9100").strip() + port_str = (sett.get("dabag_port") or "3008").strip() if not ip: return False try: port = int(port_str) - s = socket.create_connection((ip, port), timeout=PRINTER_SOCKET_TIMEOUT) - s.close() - return True - except (socket.error, ValueError, OSError): + except ValueError: return False + # Retry once: firmware often busy for ~1s after E1005 / blank label. + timeout = max(PRINTER_SOCKET_TIMEOUT, 6.0) + for attempt in range(2): + try: + s = socket.create_connection((ip, port), timeout=timeout) + s.close() + return True + except (socket.error, OSError): + if attempt == 0: + time.sleep(0.35) + continue + return False if printer_name == "激光機": ip = (sett.get("laser_ip") or "").strip() port_str = (sett.get("laser_port") or "45678").strip() @@ -198,19 +219,131 @@ FG_STATUS_ERROR = "#B22222" # red text BG_STATUS_OK = "#90EE90" # light green when connected FG_STATUS_OK = "#006400" # green text RETRY_MS = 30 * 1000 # 30 seconds reconnect -REFRESH_MS = 60 * 1000 # 60 seconds refresh when connected # POST /py/job-order-print-submit: retries when server is briefly unavailable -PRINT_SUBMIT_MAX_ATTEMPTS = 3 -PRINT_SUBMIT_RETRY_DELAY_SEC = 1.0 +PRINT_SUBMIT_MAX_ATTEMPTS = 5 +PRINT_SUBMIT_RETRY_DELAY_SEC = 1.5 PRINTER_CHECK_MS = 60 * 1000 # 1 minute when printer OK PRINTER_RETRY_MS = 30 * 1000 # 30 seconds when printer failed PRINTER_SOCKET_TIMEOUT = 3 DATAFLEX_SEND_TIMEOUT = 10 # seconds when sending ZPL to DataFlex -# Gap between bag labels on DataFlex (one TCP session per batch; tune if bags are skipped) -DATAFLEX_INTER_LABEL_DELAY_SEC = 2.5 -# Before each print job: light reset only (~JA + ~RO). Short delay so first bag starts quickly. + + +def _dataflex_float_env(name: str, default: float) -> float: + raw = (os.environ.get(name) or "").strip() + if not raw: + return default + try: + return float(raw) + except ValueError: + return default + + +def _dataflex_bool_env(name: str, default: bool) -> bool: + raw = (os.environ.get(name) or "").strip().lower() + if not raw: + return default + return raw in ("1", "true", "yes", "on") + + +def _dataflex_int_env(name: str, default: int) -> int: + raw = (os.environ.get(name) or "").strip() + if not raw: + return default + try: + return int(raw) + except ValueError: + return default + + +# Job list auto-refresh interval (ms). 0 = off (list rebuild can hide DataFlex「停止列印」). +# Re-enable: FPSMS_JOB_LIST_REFRESH_MS=60000 +JOB_LIST_AUTO_REFRESH_MS = max(0, _dataflex_int_env("FPSMS_JOB_LIST_REFRESH_MS", 0)) +# Defer full list rebuild while printing; retry after this interval until idle. FPSMS_JOB_LIST_DEFER_WHILE_PRINTING_MS +JOB_LIST_DEFER_WHILE_PRINTING_MS = max( + 500, + _dataflex_int_env("FPSMS_JOB_LIST_DEFER_WHILE_PRINTING_MS", 1500), +) + + +# Gap between bag labels (after each job has fully left the client). Tune if bags are blank/skipped. +# Override: FPSMS_DATAFLEX_INTER_LABEL_DELAY_SEC (e.g. 3.5 if ~3–5% blanks per 100). +DATAFLEX_INTER_LABEL_DELAY_SEC = _dataflex_float_env( + "FPSMS_DATAFLEX_INTER_LABEL_DELAY_SEC", 0.3 +) +# Brief pause after each ZPL send so firmware can commit before we FIN the socket (reduces dropped/blank jobs). +# Override: FPSMS_DATAFLEX_POST_LABEL_SETTLE_SEC +DATAFLEX_POST_LABEL_SETTLE_SEC = _dataflex_float_env( + "FPSMS_DATAFLEX_POST_LABEL_SETTLE_SEC", 0.08 +) +# Before each print job: light reset only (~JA + ~RO). Must finish before first ^XA or first label can be lost. +# Override: FPSMS_DATAFLEX_POST_PREPRINT_DELAY_SEC DATAFLEX_PREPRINT_BYTES = b"~JA\r\n~RO1\r\n~RO2\r\n" -DATAFLEX_POST_PREPRINT_DELAY_SEC = 0.35 +DATAFLEX_POST_PREPRINT_DELAY_SEC = _dataflex_float_env( + "FPSMS_DATAFLEX_POST_PREPRINT_DELAY_SEC", 0.55 +) +# Whether each new print job starts with full reset (~JR) so DataFlex batch counter returns to 0. +# Set FPSMS_DATAFLEX_FULL_RESET_EACH_JOB=0 to keep only light preprint reset. +DATAFLEX_FULL_RESET_EACH_JOB = _dataflex_bool_env( + "FPSMS_DATAFLEX_FULL_RESET_EACH_JOB", False +) +# Extra-safe mode: run light preprint reset (~JA/~RO) before EVERY bag. +# Slower, but reduces E1005 on unstable firmware. +DATAFLEX_PREPRINT_EACH_LABEL = _dataflex_bool_env( + "FPSMS_DATAFLEX_PREPRINT_EACH_LABEL", False +) +DATAFLEX_VERIFY_STATUS_AFTER_SEND = _dataflex_bool_env( + "FPSMS_DATAFLEX_VERIFY_STATUS_AFTER_SEND", False +) +# Hard-disable automatic reset/counter commands during printing. +# When False, normal print path sends ZPL only (no ~JA/~RO/~JR). +DATAFLEX_AUTO_RESET_ENABLED = _dataflex_bool_env( + "FPSMS_DATAFLEX_AUTO_RESET_ENABLED", False +) +# After a failed TCP send, always run host recovery + retry (recommended when E1005 stops the run). +DATAFLEX_RECOVER_ON_SEND_ERROR = _dataflex_bool_env( + "FPSMS_DATAFLEX_RECOVER_ON_SEND_ERROR", True +) +DATAFLEX_STATUS_QUERY_TIMEOUT_SEC = _dataflex_float_env( + "FPSMS_DATAFLEX_STATUS_QUERY_TIMEOUT_SEC", 0.8 +) +DATAFLEX_RECOVERY_MAX_ATTEMPTS = max( + 1, + _dataflex_int_env("FPSMS_DATAFLEX_RECOVERY_MAX_ATTEMPTS", 2), +) +DATAFLEX_RECOVERY_WAIT_SEC = _dataflex_float_env( + "FPSMS_DATAFLEX_RECOVERY_WAIT_SEC", 0.8 +) +# Prevent cumulative thermal/mechanical fault in long runs (E1000 after ~40 bags on some units): +# pause briefly every N bags. +DATAFLEX_COOLDOWN_EVERY_LABELS = max( + 0, + _dataflex_int_env("FPSMS_DATAFLEX_COOLDOWN_EVERY_LABELS", 8), +) +DATAFLEX_COOLDOWN_SEC = _dataflex_float_env( + "FPSMS_DATAFLEX_COOLDOWN_SEC", 3.5 +) +# Extra long pause every M bags (head cool-down). 0 = off. FPSMS_DATAFLEX_THERMAL_REST_EVERY_LABELS +DATAFLEX_THERMAL_REST_EVERY_LABELS = max( + 0, + _dataflex_int_env("FPSMS_DATAFLEX_THERMAL_REST_EVERY_LABELS", 20), +) +DATAFLEX_THERMAL_REST_SEC = _dataflex_float_env( + "FPSMS_DATAFLEX_THERMAL_REST_SEC", 5.0 +) +# Light ~HS check every N bags. Default off — periodic checks + recovery caused long stalls with E1005 on some units. +DATAFLEX_VERIFY_EVERY_LABELS = max( + 0, + _dataflex_int_env("FPSMS_DATAFLEX_VERIFY_EVERY_LABELS", 0), +) +# Status bar progress while printing (main thread). 0 = off. +DATAFLEX_UI_PROGRESS_EVERY = max( + 0, + _dataflex_int_env("FPSMS_DATAFLEX_UI_PROGRESS_EVERY", 5), +) +# One TCP send: single ZPL with ^PQn (n identical bags). Some DataFlex units may fault (E1005); default off. +DATAFLEX_SINGLE_TCP_JOB = _dataflex_bool_env( + "FPSMS_DATAFLEX_SINGLE_TCP_JOB", False +) # Full recovery (~JR soft reset) — used by「打袋重設」only; longer delay for firmware DATAFLEX_POST_FULL_RECOVERY_DELAY_SEC = 1.2 # Zebra ~RO only (used when FPSMS_DATAFLEX_NO_JR is set for full recovery) @@ -231,6 +364,12 @@ def _zpl_escape(s: str) -> str: return s.replace("\\", "\\\\").replace("^", "\\^") +def _dataflex_zpl_bytes(zpl: str) -> bytes: + """UTF-8 ZPL with one trailing CRLF so the printer sees a clear job boundary.""" + s = (zpl or "").rstrip("\r\n") + return (s + "\r\n").encode("utf-8") + + def generate_zpl_dataflex( batch_no: str, item_code: str, @@ -276,11 +415,28 @@ def generate_zpl_dataflex( ^XZ""" -def send_dataflex_preprint_reset(ip: str, port: int) -> None: +def dataflex_zpl_set_print_quantity(zpl: str, copies: int) -> str: + """ + Replace the fixed ^PQ1 line from generate_zpl_dataflex() with ^PQn so one ZPL job prints + n identical bags over one TCP connection. + """ + if copies < 1: + copies = 1 + old = "^PQ1,0,1,N" + if old not in zpl: + raise RuntimeError( + "DataFlex ZPL 缺少預期的 ^PQ1 列(無法改為單次連線多張)。" + ) + return zpl.replace(old, f"^PQ{copies},0,1,N", 1) + + +def send_dataflex_preprint_reset(ip: str, port: int, *, force: bool = False) -> None: """ Fast prep before printing: ~JA + ~RO (no ~JR). Clears buffer and zeros batch counters so the first bag starts quickly. Use before fixed-qty batch and continuous mode. """ + if not force and not DATAFLEX_AUTO_RESET_ENABLED: + return sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -299,11 +455,13 @@ def send_dataflex_preprint_reset(ip: str, port: int) -> None: sock.close() -def send_dataflex_job_counter_reset(ip: str, port: int) -> None: +def send_dataflex_job_counter_reset(ip: str, port: int, *, force: bool = False) -> None: """ Full host recovery for「打袋重設」: ~JA, ~RO, and ~JR (soft reset) to clear latched E1005. Slower than [send_dataflex_preprint_reset]; do not use on every row click. """ + if not force and not DATAFLEX_AUTO_RESET_ENABLED: + return sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -322,6 +480,24 @@ def send_dataflex_job_counter_reset(ip: str, port: int) -> None: sock.close() +def send_dataflex_start_job_reset(ip: str, port: int, *, force: bool = False) -> None: + """ + Start-of-job reset sequence. + + Full reset first (default) ensures printer-side batch quantity returns to 0 for each job; + then light preprint reset prepares the first bag send. + + Use force=True for the start of each print job and when selecting a job row so batch + counter resets even if FPSMS_DATAFLEX_AUTO_RESET_ENABLED=0 (that flag mainly gates + extra per-label / recovery traffic). + """ + if not force and not DATAFLEX_AUTO_RESET_ENABLED: + return + if DATAFLEX_FULL_RESET_EACH_JOB: + send_dataflex_job_counter_reset(ip, port, force=force) + send_dataflex_preprint_reset(ip, port, force=force) + + def send_dataflex_reset_and_labels( ip: str, port: int, @@ -335,7 +511,7 @@ def send_dataflex_reset_and_labels( """ if copies < 1: return - raw_zpl = zpl.encode("utf-8") + raw_zpl = _dataflex_zpl_bytes(zpl) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -348,6 +524,7 @@ def send_dataflex_reset_and_labels( time.sleep(DATAFLEX_POST_PREPRINT_DELAY_SEC) for i in range(copies): sock.sendall(raw_zpl) + time.sleep(DATAFLEX_POST_LABEL_SETTLE_SEC) if i < copies - 1: time.sleep(delay_sec) try: @@ -700,7 +877,8 @@ def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None: sock.settimeout(DATAFLEX_SEND_TIMEOUT) try: sock.connect((ip, port)) - sock.sendall(zpl.encode("utf-8")) + sock.sendall(_dataflex_zpl_bytes(zpl)) + time.sleep(DATAFLEX_POST_LABEL_SETTLE_SEC) try: sock.shutdown(socket.SHUT_WR) except OSError: @@ -709,6 +887,115 @@ def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None: sock.close() +def query_dataflex_host_status(ip: str, port: int) -> str: + """ + Query DataFlex/Zebra host status (~HS). Returns decoded status text, or empty string + when device does not return host status. + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except OSError: + pass + sock.settimeout(max(0.2, DATAFLEX_STATUS_QUERY_TIMEOUT_SEC)) + try: + sock.connect((ip, port)) + sock.sendall(b"~HS\r\n") + chunks: list[bytes] = [] + while True: + try: + data = sock.recv(4096) + except socket.timeout: + break + if not data: + break + chunks.append(data) + if sum(len(c) for c in chunks) >= 16384: + break + return b"".join(chunks).decode("utf-8", errors="ignore") + finally: + sock.close() + + +def _dataflex_status_has_e1005(status_text: str) -> bool: + s = (status_text or "").lower() + return "e1005" in s or "1005" in s + + +def _dataflex_status_problem_code(status_text: str) -> Optional[str]: + """If host status (~HS) suggests a fault, return a short code like E1000; else None.""" + s = (status_text or "").lower() + for code in ("e1000", "e1005", "e1004", "e1003", "e1002", "e1001"): + if code in s: + return code.upper() + return None + + +def assert_dataflex_host_ok(ip: str, port: int) -> None: + """ + Query ~HS once. If printer reports a known fault token, stop the job early. + Empty/short replies are ignored (some firmware is quiet). + """ + st = query_dataflex_host_status(ip, port) + if not (st or "").strip(): + return + prob = _dataflex_status_problem_code(st) + if prob is not None: + raise RuntimeError( + f"打袋機狀態異常 {prob}(~HS)。請看機台畫面處理後再印。" + ) + + +def recover_dataflex_if_host_fault(ip: str, port: int) -> None: + """ + If ~HS reports E1000/E1005/etc., clear host state once and continue — do not abort the whole run. + Keeps work short so the print thread does not look "frozen". + """ + st = query_dataflex_host_status(ip, port) + if not (st or "").strip(): + return + if _dataflex_status_problem_code(st) is None: + return + touch_dataflex_recovery_grace(14.0) + send_dataflex_job_counter_reset(ip, port, force=True) + send_dataflex_preprint_reset(ip, port, force=True) + time.sleep(max(0.35, DATAFLEX_RECOVERY_WAIT_SEC)) + + +def send_dataflex_label_with_recovery(ip: str, port: int, zpl: str) -> None: + """ + Send one bag label with one automatic recovery attempt. + + If first send fails (including firmware-latched states such as E1005), + perform full recovery (~JA/~RO/~JR), then light preprint reset (~JA/~RO), + and retry once. + """ + last_err: Optional[Exception] = None + for attempt in range(DATAFLEX_RECOVERY_MAX_ATTEMPTS): + try: + if DATAFLEX_AUTO_RESET_ENABLED and DATAFLEX_PREPRINT_EACH_LABEL: + send_dataflex_preprint_reset(ip, port) + send_zpl_to_dataflex(ip, port, zpl) + if DATAFLEX_VERIFY_STATUS_AFTER_SEND: + status_text = query_dataflex_host_status(ip, port) + if _dataflex_status_has_e1005(status_text): + raise RuntimeError("DataFlex E1005 detected from host status.") + return + except (ConnectionRefusedError, socket.timeout, OSError, RuntimeError) as ex: + last_err = ex + if attempt >= DATAFLEX_RECOVERY_MAX_ATTEMPTS - 1: + break + if DATAFLEX_AUTO_RESET_ENABLED or DATAFLEX_RECOVER_ON_SEND_ERROR: + touch_dataflex_recovery_grace(14.0) + send_dataflex_job_counter_reset(ip, port, force=True) + send_dataflex_preprint_reset(ip, port, force=True) + time.sleep(max(0.35, DATAFLEX_RECOVERY_WAIT_SEC)) + + if last_err is not None: + raise last_err + raise RuntimeError("DataFlex label send failed.") + + def send_zpl_to_label_printer(target: str, zpl: str) -> None: """ Send ZPL to 標簽機. @@ -1045,18 +1332,68 @@ def run_dataflex_fixed_qty_thread( ) return dataflex_busy_ref[0] = True + printed = 0 + used_single_tcp = False try: - send_dataflex_reset_and_labels( - ip, - port, - zpl, - n, - DATAFLEX_INTER_LABEL_DELAY_SEC, - ) + send_dataflex_start_job_reset(ip, port, force=True) + if DATAFLEX_SINGLE_TCP_JOB and n >= 1: + # One TCP connection, one ZPL, ^PQn — printer firmware prints n identical bags. + used_single_tcp = True + zpl_one = dataflex_zpl_set_print_quantity(zpl, n) + root.after( + 0, + lambda tn=n: set_status_message( + f"打袋單次發送中… {tn} 張(^PQ{tn})", + is_error=False, + ), + ) + send_dataflex_label_with_recovery(ip, port, zpl_one) + if DATAFLEX_VERIFY_EVERY_LABELS > 0: + recover_dataflex_if_host_fault(ip, port) + printed = n + else: + # One TCP job per bag. Slower but avoids E1005 on some units when ^PQ is large. + for i in range(n): + send_dataflex_label_with_recovery(ip, port, zpl) + printed += 1 + if DATAFLEX_UI_PROGRESS_EVERY > 0 and ( + printed == 1 or printed % DATAFLEX_UI_PROGRESS_EVERY == 0 + ): + p, t = printed, n + root.after( + 0, + lambda p=p, t=t: set_status_message( + f"打袋列印中… {p}/{t}", + is_error=False, + ), + ) + if ( + DATAFLEX_VERIFY_EVERY_LABELS > 0 + and printed % DATAFLEX_VERIFY_EVERY_LABELS == 0 + ): + recover_dataflex_if_host_fault(ip, port) + if ( + DATAFLEX_COOLDOWN_EVERY_LABELS > 0 + and printed % DATAFLEX_COOLDOWN_EVERY_LABELS == 0 + and i < n - 1 + ): + time.sleep(max(0.0, DATAFLEX_COOLDOWN_SEC)) + if ( + DATAFLEX_THERMAL_REST_EVERY_LABELS > 0 + and printed % DATAFLEX_THERMAL_REST_EVERY_LABELS == 0 + and i < n - 1 + ): + time.sleep(max(0.0, DATAFLEX_THERMAL_REST_SEC)) + if i < n - 1: + time.sleep(DATAFLEX_INTER_LABEL_DELAY_SEC) root.after( 0, - lambda: set_status_message( - f"已送出列印:批次 {label_text} x {n} 張", + lambda u=used_single_tcp: set_status_message( + ( + f"已送出列印(單次 TCP):批次 {label_text} x {n} 張" + if u + else f"已送出列印:批次 {label_text} x {n} 張" + ), is_error=False, ), ) @@ -1069,7 +1406,7 @@ def run_dataflex_fixed_qty_thread( 0, lambda err=str(ex): messagebox.showwarning( "打袋機", - f"已送出 {n} 張,但伺服器記錄失敗:{err}", + f"列印可能已完成,但伺服器記錄失敗(可再試):{err}", ), ) else: @@ -1084,7 +1421,7 @@ def run_dataflex_fixed_qty_thread( root.after( 0, lambda: set_status_message( - f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。", + f"無法連線至 {ip}:{port},已送出 {printed}/{n} 張。", is_error=True, ), ) @@ -1092,14 +1429,33 @@ def run_dataflex_fixed_qty_thread( root.after( 0, lambda: set_status_message( - f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。", + f"連線逾時 ({ip}:{port}),已送出 {printed}/{n} 張。", is_error=True, ), ) except OSError as err: root.after( 0, - lambda e=err: set_status_message(f"列印失敗:{e}", is_error=True), + lambda e=err: set_status_message( + f"列印失敗:{e}(已送出 {printed}/{n} 張)", + is_error=True, + ), + ) + except RuntimeError as err: + root.after( + 0, + lambda e=err: set_status_message( + f"打袋機錯誤:{e}(已送出 {printed}/{n} 張)", + is_error=True, + ), + ) + except Exception as err: + root.after( + 0, + lambda e=err: set_status_message( + f"打袋機例外:{e}(已送出 {printed}/{n} 張)", + is_error=True, + ), ) finally: with dataflex_lock: @@ -1622,6 +1978,8 @@ def main() -> None: # DataFlex: shared lock so fixed-qty and continuous jobs do not overlap (independent of laser/label) dataflex_lock = threading.Lock() dataflex_busy_ref: list = [False] + # Suppress transient DataFlex "disconnected" UI while we intentionally reset/print. + dataflex_status_grace_until_ref: list[float] = [0.0] # 標籤機: own lock so label jobs do not overlap; does not block DataFlex or laser label_lock = threading.Lock() label_busy_ref: list = [False] @@ -1641,6 +1999,11 @@ def main() -> None: except tk.TclError: pass + def hold_dataflex_status_ok(seconds: float) -> None: + until = time.time() + max(0.0, seconds) + if until > dataflex_status_grace_until_ref[0]: + dataflex_status_grace_until_ref[0] = until + # Top: left [前一天] [date] [後一天] | right [printer dropdown] top = tk.Frame(root, padx=12, pady=12, bg=BG_TOP) top.pack(fill=tk.X) @@ -1703,10 +2066,11 @@ def main() -> None: port = int(port_str) except ValueError: port = 3008 + hold_dataflex_status_ok(12.0) def worker() -> None: try: - send_dataflex_job_counter_reset(ip, port) + send_dataflex_job_counter_reset(ip, port, force=True) root.after( 0, lambda: messagebox.showinfo( @@ -1771,6 +2135,15 @@ def main() -> None: if printer_after_ref[0] is not None: root.after_cancel(printer_after_ref[0]) printer_after_ref[0] = None + if printer_var.get() == "打袋機 DataFlex": + if ( + dataflex_busy_ref[0] + or time.time() < dataflex_status_grace_until_ref[0] + or time.time() < _DATAFLEX_RECOVERY_GRACE_UNTIL + ): + set_printer_status_ok() + printer_after_ref[0] = root.after(5000, check_printer) + return ok = try_printer_connection(printer_var.get(), settings) if ok: set_printer_status_ok() @@ -2059,142 +2432,212 @@ def main() -> None: if not ip: messagebox.showerror("打袋機", "請在設定中填寫打袋機 DataFlex 的 IP。") else: - bag_ans = ask_bag_count(root) - if bag_ans is not None: - n, continuous = bag_ans - item_code = j.get("itemCode") or "—" - item_name = j.get("itemName") or "—" - item_id = j.get("itemId") - stock_in_line_id = j.get("stockInLineId") - lot_no = j.get("lotNo") - zpl = generate_zpl_dataflex( - b, - item_code, - item_name, - item_id=item_id, - stock_in_line_id=stock_in_line_id, - lot_no=lot_no, - ) - label_text = (lot_no or b).strip() - if continuous: - stop_ev = threading.Event() - stop_win = open_dataflex_stop_window( - root, stop_ev, dataflex_stop_win_ref + hold_dataflex_status_ok(12.0) + + def _after_row_select_reset() -> None: + bag_ans = ask_bag_count(root) + if bag_ans is not None: + n, continuous = bag_ans + hold_dataflex_status_ok(12.0) + item_code = j.get("itemCode") or "—" + item_name = j.get("itemName") or "—" + item_id = j.get("itemId") + stock_in_line_id = j.get("stockInLineId") + lot_no = j.get("lotNo") + zpl = generate_zpl_dataflex( + b, + item_code, + item_name, + item_id=item_id, + stock_in_line_id=stock_in_line_id, + lot_no=lot_no, ) - - def dflex_worker() -> None: - with dataflex_lock: - if dataflex_busy_ref[0]: + label_text = (lot_no or b).strip() + if continuous: + stop_ev = threading.Event() + stop_win = open_dataflex_stop_window( + root, stop_ev, dataflex_stop_win_ref + ) + + def dflex_worker() -> None: + with dataflex_lock: + if dataflex_busy_ref[0]: + root.after( + 0, + lambda: messagebox.showwarning( + "打袋機", + "請等待目前列印完成或先停止連續列印。", + ), + ) + return + dataflex_busy_ref[0] = True + printed = 0 + error_shown = False + try: + # One TCP job per bag (not one endless stream). Persistent socket + # caused E1005 over-qty on some DataFlex units after a few labels. + send_dataflex_start_job_reset(ip, port, force=True) + while not stop_ev.is_set(): + send_dataflex_label_with_recovery(ip, port, zpl) + printed += 1 + if DATAFLEX_UI_PROGRESS_EVERY > 0 and ( + printed == 1 + or printed % DATAFLEX_UI_PROGRESS_EVERY == 0 + ): + p = printed + root.after( + 0, + lambda p=p: set_status_message( + f"連續打袋列印中… 已印 {p} 張", + is_error=False, + ), + ) + if ( + DATAFLEX_VERIFY_EVERY_LABELS > 0 + and printed % DATAFLEX_VERIFY_EVERY_LABELS == 0 + ): + recover_dataflex_if_host_fault(ip, port) + if ( + DATAFLEX_COOLDOWN_EVERY_LABELS > 0 + and printed % DATAFLEX_COOLDOWN_EVERY_LABELS == 0 + ): + _sleep_interruptible( + stop_ev, + max(0.0, DATAFLEX_COOLDOWN_SEC), + ) + if ( + DATAFLEX_THERMAL_REST_EVERY_LABELS > 0 + and printed % DATAFLEX_THERMAL_REST_EVERY_LABELS == 0 + ): + _sleep_interruptible( + stop_ev, + max(0.0, DATAFLEX_THERMAL_REST_SEC), + ) + _sleep_interruptible( + stop_ev, + DATAFLEX_INTER_LABEL_DELAY_SEC, + ) + except ConnectionRefusedError: + error_shown = True root.after( 0, - lambda: messagebox.showwarning( - "打袋機", - "請等待目前列印完成或先停止連續列印。", + lambda: set_status_message( + f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。", + is_error=True, ), ) - return - dataflex_busy_ref[0] = True - printed = 0 - error_shown = False - try: - # One TCP job per bag (not one endless stream). Persistent socket - # caused E1005 over-qty on some DataFlex units after a few labels. - send_dataflex_preprint_reset(ip, port) - while not stop_ev.is_set(): - send_zpl_to_dataflex(ip, port, zpl) - printed += 1 - _sleep_interruptible( - stop_ev, - DATAFLEX_INTER_LABEL_DELAY_SEC, + except socket.timeout: + error_shown = True + root.after( + 0, + lambda: set_status_message( + f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。", + is_error=True, + ), ) - except ConnectionRefusedError: - error_shown = True - root.after( - 0, - lambda: set_status_message( - f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。", - is_error=True, - ), - ) - except socket.timeout: - error_shown = True - root.after( - 0, - lambda: set_status_message( - f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。", - is_error=True, - ), - ) - except OSError as err: - error_shown = True - root.after( - 0, - lambda e=err: set_status_message( - f"列印失敗:{e}", - is_error=True, - ), - ) - finally: - with dataflex_lock: - dataflex_busy_ref[0] = False - - def _done() -> None: - dataflex_stop_win_ref[0] = None - try: - if os.name == "nt": - stop_win.attributes("-topmost", False) - except tk.TclError: - pass - try: - stop_win.destroy() - except tk.TclError: - pass - if printed > 0: - set_status_message( - f"連續列印結束:批次 {label_text},已印 {printed} 張", - is_error=False, - ) - jo_id = j.get("id") - if jo_id is not None: - try: - submit_job_order_print_submit( - base_url_ref[0], - int(jo_id), - printed, - "DATAFLEX", - ) - load_job_orders(from_user_date_change=False) - except requests.RequestException as ex: - messagebox.showwarning( - "打袋機", - f"已印 {printed} 張,但伺服器記錄失敗:{ex}", - ) - elif not error_shown: - set_status_message( - "連續列印未印出或已取消", + except OSError as err: + error_shown = True + root.after( + 0, + lambda e=err: set_status_message( + f"列印失敗:{e}", is_error=True, - ) - - root.after(0, _done) - - threading.Thread(target=dflex_worker, daemon=True).start() - else: - run_dataflex_fixed_qty_thread( - root=root, - dataflex_lock=dataflex_lock, - dataflex_busy_ref=dataflex_busy_ref, - ip=ip, - port=port, - n=n, - zpl=zpl, - label_text=label_text, - jo_id=j.get("id"), - base_url=base_url_ref[0], - set_status_message=set_status_message, - on_recorded=lambda: load_job_orders( - from_user_date_change=False + ), + ) + except RuntimeError as err: + error_shown = True + root.after( + 0, + lambda e=err: set_status_message( + f"打袋機錯誤:{e}", + is_error=True, + ), + ) + except Exception as err: + error_shown = True + root.after( + 0, + lambda e=err: set_status_message( + f"打袋機例外:{e}", + is_error=True, + ), + ) + finally: + with dataflex_lock: + dataflex_busy_ref[0] = False + + def _done() -> None: + dataflex_stop_win_ref[0] = None + try: + if os.name == "nt": + stop_win.attributes("-topmost", False) + except tk.TclError: + pass + try: + stop_win.destroy() + except tk.TclError: + pass + if printed > 0: + set_status_message( + f"連續列印結束:批次 {label_text},已印 {printed} 張", + is_error=False, + ) + jo_id = j.get("id") + if jo_id is not None: + try: + submit_job_order_print_submit( + base_url_ref[0], + int(jo_id), + printed, + "DATAFLEX", + ) + load_job_orders(from_user_date_change=False) + except requests.RequestException as ex: + messagebox.showwarning( + "打袋機", + f"列印可能已完成,但伺服器記錄失敗(可再試):{ex}", + ) + elif not error_shown: + set_status_message( + "連續列印未印出或已取消", + is_error=True, + ) + + root.after(0, _done) + + threading.Thread(target=dflex_worker, daemon=True).start() + else: + run_dataflex_fixed_qty_thread( + root=root, + dataflex_lock=dataflex_lock, + dataflex_busy_ref=dataflex_busy_ref, + ip=ip, + port=port, + n=n, + zpl=zpl, + label_text=label_text, + jo_id=j.get("id"), + base_url=base_url_ref[0], + set_status_message=set_status_message, + on_recorded=lambda: load_job_orders( + from_user_date_change=False + ), + ) + + def _row_select_reset_worker() -> None: + try: + send_dataflex_start_job_reset(ip, port, force=True) + except OSError as ex: + root.after( + 0, + lambda e=str(ex): messagebox.showwarning( + "打袋機", + f"點選工單時重設批次計數失敗(仍可比對數量):{e}", ), ) + root.after(0, _after_row_select_reset) + + threading.Thread(target=_row_select_reset_worker, daemon=True).start() elif printer_var.get() == "標簽機": com = (settings.get("label_com") or "").strip() if not com: @@ -2325,16 +2768,33 @@ def main() -> None: last_plan_start_ref[0] = plan_start data_changed = not _data_equal(old_data, data) if data_changed or from_user_date_change: - # Rebuild list: clear and rebuild from current data (last_data_ref already updated) - for w in inner.winfo_children(): - w.destroy() - preserve = not from_user_date_change - needle = search_var.get().strip() - shown = _filter_job_orders_by_search(data, needle) if needle else data - _build_list_from_data(shown, plan_start, preserve_selection=preserve) + printing_busy = ( + dataflex_busy_ref[0] + or label_busy_ref[0] + or laser_send_busy_ref[0] + ) + # Do not destroy/rebuild all rows while printing — that removes click bindings and + # can hide DataFlex「停止列印」. Retry until idle (one deferred pass at a time). + if printing_busy and not from_user_date_change: + after_id_ref[0] = root.after( + JOB_LIST_DEFER_WHILE_PRINTING_MS, + lambda: load_job_orders(from_user_date_change=False), + ) + else: + # Rebuild list: clear and rebuild from current data (last_data_ref already updated) + for w in inner.winfo_children(): + w.destroy() + preserve = not from_user_date_change + needle = search_var.get().strip() + shown = _filter_job_orders_by_search(data, needle) if needle else data + _build_list_from_data(shown, plan_start, preserve_selection=preserve) if from_user_date_change: canvas.yview_moveto(0) - after_id_ref[0] = root.after(REFRESH_MS, lambda: load_job_orders(from_user_date_change=False)) + if JOB_LIST_AUTO_REFRESH_MS > 0: + after_id_ref[0] = root.after( + JOB_LIST_AUTO_REFRESH_MS, + lambda: load_job_orders(from_user_date_change=False), + ) # Load default (today) on start; then start printer connection check root.after(100, lambda: load_job_orders(from_user_date_change=True)) diff --git a/python/installAndExe.txt b/python/installAndExe.txt index 8a9184c..0de431b 100644 --- a/python/installAndExe.txt +++ b/python/installAndExe.txt @@ -1,15 +1,5 @@ -py -m pip install pyinstaller py -m pip install --upgrade pyinstaller -py -m PyInstaller --onefile --windowed --name "Bag1" Bag1.py +py -m pip install --upgrade pywin32 +py -m pip install --upgrade Pillow "qrcode[pil]" -py -m PyInstaller --onefile --windowed --name "Bag3" Bag3.py - -python -m pip install pyinstaller -python -m pip install --upgrade pyinstaller -python -m PyInstaller --onefile --windowed --name "Bag1" Bag1.py - - -pip install Pillow "qrcode[pil]" - - -py -m pip install Pillow "qrcode[pil]" \ No newline at end of file +py -m PyInstaller --noconfirm --clean Bag3.spec \ No newline at end of file