diff --git a/python/Bag3.py b/python/Bag3.py index c59498d..74ea4e3 100644 --- a/python/Bag3.py +++ b/python/Bag3.py @@ -2,9 +2,15 @@ """ Bag3 v3.2 – FPSMS job orders by plan date (this file is the maintained version). -Uses the public API GET /py/job-orders (no login required). +Uses the public API GET /py/job-orders and POST /py/job-order-print-submit (no login required). UI tuned for aged users: larger font, Traditional Chinese labels, prev/next date. +Database print counts (py_job_order_print_submit): + Each finished print run calls submit_job_order_print_submit() with jobOrderId, qty, + and printChannel (DATAFLEX | LABEL | LASER). The server appends one row per call; + GET /py/job-orders returns cumulative bagPrintedQty / labelPrintedQty / laserPrintedQty + per job order. Re-printing the same job later adds another row (SUM increases). + Bag2 is kept as a separate legacy v2.x line; do not assume Bag2 matches Bag3. Run: python Bag3.py @@ -193,22 +199,28 @@ 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 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 -# After ~JA/~RO/~JR recovery, let firmware finish before sending label ZPL (avoids stuck E1005) -DATAFLEX_POST_RECOVERY_DELAY_SEC = 1.5 -# Zebra ~RO only (used when FPSMS_DATAFLEX_NO_JR is set) +# Before each print job: light reset only (~JA + ~RO). Short delay so first bag starts quickly. +DATAFLEX_PREPRINT_BYTES = b"~JA\r\n~RO1\r\n~RO2\r\n" +DATAFLEX_POST_PREPRINT_DELAY_SEC = 0.35 +# 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) DATAFLEX_RESET_BYTES = b"~RO1\r\n~RO2\r\n" # Full host recovery: ~JA clear buffers, ~RO counters, ~JR soft reset (clears latched errors without power cycle) DATAFLEX_FULL_RECOVERY_BYTES = b"~JA\r\n~RO1\r\n~RO2\r\n~JR\r\n" -def _dataflex_recovery_payload() -> bytes: - """~JA+~RO+~JR by default; set env FPSMS_DATAFLEX_NO_JR=1 to skip ~JR if firmware rejects it.""" +def _dataflex_full_recovery_payload() -> bytes: + """~JA+~RO+~JR for manual「打袋重設」; set env FPSMS_DATAFLEX_NO_JR=1 to skip ~JR.""" if os.environ.get("FPSMS_DATAFLEX_NO_JR", "").strip().lower() in ("1", "true", "yes"): return b"~JA\r\n" + DATAFLEX_RESET_BYTES return DATAFLEX_FULL_RECOVERY_BYTES @@ -233,7 +245,7 @@ def generate_zpl_dataflex( Row 1 (from zero): QR code, then item name (rotated 90°). Row 2: Batch/lot (left), item code (right). Label and QR use lotNo from API when present, else batch_no (Bxxxxx). - Host recovery (~JA/~RO/~JR) is sent separately via [send_dataflex_job_counter_reset] before labels. + Light preprint (~JA/~RO) is sent before labels; full ~JR recovery is only for「打袋重設」. """ desc = _zpl_escape((item_name or "—").strip()) code = _zpl_escape((item_code or "—").strip()) @@ -264,12 +276,33 @@ def generate_zpl_dataflex( ^XZ""" -def send_dataflex_job_counter_reset(ip: str, port: int) -> None: +def send_dataflex_preprint_reset(ip: str, port: int) -> 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. """ - Prepare DataFlex for a new job without power cycling: ~JA (clear host buffer / cancel pending), - ~RO1/~RO2 (counters), ~JR (soft reset — clears latched E1005 / “over qty” on many Zebra-class units). + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except OSError: + pass + sock.settimeout(DATAFLEX_SEND_TIMEOUT) + try: + sock.connect((ip, port)) + sock.sendall(DATAFLEX_PREPRINT_BYTES) + time.sleep(DATAFLEX_POST_PREPRINT_DELAY_SEC) + try: + sock.shutdown(socket.SHUT_WR) + except OSError: + pass + finally: + sock.close() + - Brief pause after send so ~JR can finish before label data. Set FPSMS_DATAFLEX_NO_JR=1 to omit ~JR. +def send_dataflex_job_counter_reset(ip: str, port: int) -> 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. """ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -279,8 +312,8 @@ def send_dataflex_job_counter_reset(ip: str, port: int) -> None: sock.settimeout(DATAFLEX_SEND_TIMEOUT) try: sock.connect((ip, port)) - sock.sendall(_dataflex_recovery_payload()) - time.sleep(DATAFLEX_POST_RECOVERY_DELAY_SEC) + sock.sendall(_dataflex_full_recovery_payload()) + time.sleep(DATAFLEX_POST_FULL_RECOVERY_DELAY_SEC) try: sock.shutdown(socket.SHUT_WR) except OSError: @@ -297,9 +330,8 @@ def send_dataflex_reset_and_labels( delay_sec: float, ) -> None: """ - One TCP connection: send ~RO reset, then `copies` identical ZPL labels with delay_sec between - copies (not after the last). Avoids rapid connect/disconnect per bag, which can cause skipped - prints on some DataFlex/Zebra TCP hosts. + One TCP connection: light preprint (~JA + ~RO), short pause, then `copies` identical ZPL labels + with delay_sec between copies (not after the last). Avoids rapid connect/disconnect per bag. """ if copies < 1: return @@ -312,8 +344,8 @@ def send_dataflex_reset_and_labels( sock.settimeout(DATAFLEX_SEND_TIMEOUT) try: sock.connect((ip, port)) - sock.sendall(_dataflex_recovery_payload()) - time.sleep(DATAFLEX_POST_RECOVERY_DELAY_SEC) + sock.sendall(DATAFLEX_PREPRINT_BYTES) + time.sleep(DATAFLEX_POST_PREPRINT_DELAY_SEC) for i in range(copies): sock.sendall(raw_zpl) if i < copies - 1: @@ -958,6 +990,14 @@ def run_laser_row_send_thread( f"已發送,但伺服器記錄失敗:{err}", ), ) + elif base_url: + root.after( + 0, + lambda: messagebox.showwarning( + "激光機", + "已發送,但無工單 id,無法寫入伺服器記錄。", + ), + ) root.after( 0, lambda: set_status_message("已發送", is_error=False), @@ -1032,6 +1072,14 @@ def run_dataflex_fixed_qty_thread( f"已送出 {n} 張,但伺服器記錄失敗:{err}", ), ) + else: + root.after( + 0, + lambda: messagebox.showwarning( + "打袋機", + f"已送出列印 {n} 張,但無工單 id,無法寫入伺服器記錄。", + ), + ) except ConnectionRefusedError: root.after( 0, @@ -1198,14 +1246,34 @@ def submit_job_order_print_submit( qty: int, print_channel: str = "LABEL", ) -> None: - """POST /py/job-order-print-submit — one row per submit for DB wastage/stock tracking.""" + """ + Record printed quantity in the FPSMS database via PyController. + + POST ``/api/py/job-order-print-submit`` (path under base_url) — **public endpoint, no login** + or API key required. Each successful call appends one row to ``py_job_order_print_submit``; + totals per job order and channel are aggregated server-side. + + Raises ``requests.RequestException`` if all retry attempts fail. + """ url = f"{base_url.rstrip('/')}/py/job-order-print-submit" - resp = requests.post( - url, - json={"jobOrderId": job_order_id, "qty": qty, "printChannel": print_channel}, - timeout=30, - ) - resp.raise_for_status() + payload = { + "jobOrderId": int(job_order_id), + "qty": int(qty), + "printChannel": print_channel, + } + last_err: Optional[Exception] = None + for attempt in range(PRINT_SUBMIT_MAX_ATTEMPTS): + try: + resp = requests.post(url, json=payload, timeout=30) + resp.raise_for_status() + return + except requests.RequestException as ex: + last_err = ex + if attempt < PRINT_SUBMIT_MAX_ATTEMPTS - 1: + time.sleep(PRINT_SUBMIT_RETRY_DELAY_SEC) + if last_err is not None: + raise last_err + raise RuntimeError("submit_job_order_print_submit: unexpected empty error") def set_row_highlight(row_frame: tk.Frame, selected: bool) -> None: @@ -2031,7 +2099,7 @@ def main() -> None: 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_job_counter_reset(ip, port) + send_dataflex_preprint_reset(ip, port) while not stop_ev.is_set(): send_zpl_to_dataflex(ip, port, zpl) printed += 1