|
|
|
@@ -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)) |
|
|
|
|