소스 검색

no message

master
Fai Luk 14 시간 전
부모
커밋
2018e987cc
2개의 변경된 파일628개의 추가작업 그리고 178개의 파일을 삭제
  1. +625
    -165
      python/Bag3.py
  2. +3
    -13
      python/installAndExe.txt

+ 625
- 165
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))


+ 3
- 13
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]"
py -m PyInstaller --noconfirm --clean Bag3.spec

불러오는 중...
취소
저장