From 1d6b1087b918b1c8b480001875c794a4dd07dc1d Mon Sep 17 00:00:00 2001 From: "DESKTOP-064TTA1\\Fai LUK" Date: Fri, 27 Mar 2026 18:04:49 +0800 Subject: [PATCH] no message --- .../fpsms/modules/common/SettingNames.java | 3 + .../service/LaserBag2AutoSendService.kt | 21 +++ .../service/PlasticBagPrinterService.kt | 161 +++++++++++++++--- .../web/model/LaserBag2AutoSendReport.kt | 2 + .../web/model/LaserBag2SendRequest.kt | 8 + .../web/model/LaserBag2SendResponse.kt | 4 + .../web/model/LaserBag2SettingsResponse.kt | 2 + .../web/model/LaserLastReceiveSuccessDto.kt | 22 +++ .../01_laser_print_last_receive_success.sql | 8 + 9 files changed, 206 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserLastReceiveSuccessDto.kt create mode 100644 src/main/resources/db/changelog/changes/20260327_laser_last_receive_success/01_laser_print_last_receive_success.sql diff --git a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java index 23dea74..316b865 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java +++ b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java @@ -99,4 +99,7 @@ public abstract class SettingNames { /** Comma-separated BOM item codes shown on /laserPrint job list (e.g. PP1175); blank = no filter (all packaging JOs) */ public static final String LASER_PRINT_ITEM_CODES = "LASER_PRINT.itemCodes"; + /** JSON: last laser TCP send where printer returned receive (job order no., lot, itemId/stockInLineId, etc.) */ + public static final String LASER_PRINT_LAST_RECEIVE_SUCCESS = "LASER_PRINT.lastReceiveSuccess"; + } diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt index 002bd9e..c884e9d 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt @@ -32,6 +32,17 @@ class LaserBag2AutoSendService( sendsPerJob: Int = DEFAULT_SENDS_PER_JOB, delayBetweenSendsMs: Long = DEFAULT_DELAY_BETWEEN_SENDS_MS, ): LaserBag2AutoSendReport { + val (reachable, laserIp, laserPort) = plasticBagPrinterService.probeLaserBag2Tcp() + if (!reachable) { + logger.warn("Connection failed to the laser print: {} / {}", laserIp, laserPort) + return LaserBag2AutoSendReport( + planStart = planStart, + jobOrdersFound = 0, + jobOrdersProcessed = 0, + results = emptyList(), + ) + } + val orders = plasticBagPrinterService.listLaserPrintJobOrders(planStart) val toProcess = if (limitPerRun > 0) orders.take(limitPerRun) else orders val results = mutableListOf() @@ -47,6 +58,8 @@ class LaserBag2AutoSendService( for (jo in toProcess) { var lastMsg = "" var overallOk = true + var lastPrinterAck: String? = null + var lastReceiveAck = false for (attempt in 1..sendsPerJob) { val resp = plasticBagPrinterService.sendLaserBag2Job( LaserBag2SendRequest( @@ -54,9 +67,15 @@ class LaserBag2AutoSendService( stockInLineId = jo.stockInLineId, itemCode = jo.itemCode, itemName = jo.itemName, + jobOrderId = jo.id, + jobOrderNo = jo.code, + lotNo = jo.lotNo, + source = "AUTO", ), ) lastMsg = resp.message + lastPrinterAck = resp.printerAck + lastReceiveAck = resp.receiveAcknowledged if (!resp.success) { overallOk = false logger.warn("Laser send failed jobOrderId={} attempt={}: {}", jo.id, attempt, resp.message) @@ -79,6 +98,8 @@ class LaserBag2AutoSendService( itemCode = jo.itemCode, success = overallOk, message = lastMsg, + printerAck = lastPrinterAck, + receiveAcknowledged = lastReceiveAck, ), ) } diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt index dcfd186..29d3f64 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt @@ -7,6 +7,7 @@ import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendRequest import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse +import com.ffii.fpsms.modules.jobOrder.web.model.LaserLastReceiveSuccessDto import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest import com.ffii.fpsms.modules.jobOrder.web.model.NgpclPushResponse @@ -47,12 +48,23 @@ import java.net.ConnectException import java.net.SocketTimeoutException import org.springframework.core.io.ClassPathResource import org.slf4j.LoggerFactory +import com.fasterxml.jackson.databind.ObjectMapper import java.time.Duration +import java.time.Instant import java.time.LocalDate // Data class to store bitmap bytes + width (for XML) data class BitmapResult(val bytes: ByteArray, val width: Int) +/** One Bag2-style laser TCP attempt (internal to [PlasticBagPrinterService]). */ +private data class LaserBag2TcpResult( + val success: Boolean, + val message: String, + val payload: String, + val printerAck: String?, + val receiveAcknowledged: Boolean, +) + @Service class PlasticBagPrinterService( val jobOrderRepository: JobOrderRepository, @@ -61,6 +73,7 @@ class PlasticBagPrinterService( private val settingsService: SettingsService, private val pyJobOrderPrintSubmitService: PyJobOrderPrintSubmitService, private val environment: Environment, + private val objectMapper: ObjectMapper, ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -86,7 +99,43 @@ class PlasticBagPrinterService( val itemCodes = settingsService.findByName(SettingNames.LASER_PRINT_ITEM_CODES) .map { it.value?.trim() ?: "" } .orElse(DEFAULT_LASER_ITEM_CODES) - return LaserBag2SettingsResponse(host = host, port = port, itemCodes = itemCodes) + return LaserBag2SettingsResponse( + host = host, + port = port, + itemCodes = itemCodes, + lastReceiveSuccess = readLaserLastReceiveSuccessFromSettings(), + ) + } + + private fun readLaserLastReceiveSuccessFromSettings(): LaserLastReceiveSuccessDto? { + val raw = settingsService.findByName(SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS) + .map { it.value } + .orElse(null) + ?.trim() + .orEmpty() + if (raw.isBlank() || raw == "{}") return null + return try { + val dto = objectMapper.readValue(raw, LaserLastReceiveSuccessDto::class.java) + if (dto.sentAt.isNullOrBlank()) null else dto + } catch (e: Exception) { + logger.warn("Could not parse LASER_PRINT.lastReceiveSuccess: {}", e.message) + null + } + } + + private fun persistLaserLastReceiveSuccess(request: LaserBag2SendRequest, printerAck: String?) { + val dto = LaserLastReceiveSuccessDto( + jobOrderId = request.jobOrderId, + jobOrderNo = request.jobOrderNo?.trim()?.takeIf { it.isNotEmpty() }, + lotNo = request.lotNo?.trim()?.takeIf { it.isNotEmpty() }, + itemId = request.itemId, + stockInLineId = request.stockInLineId, + printerAck = printerAck?.trim()?.takeIf { it.isNotEmpty() }, + sentAt = Instant.now().toString(), + source = request.source?.trim()?.takeIf { it.isNotEmpty() } ?: "MANUAL", + ) + val json = objectMapper.writeValueAsString(dto) + settingsService.update(SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS, json) } /** @@ -173,22 +222,39 @@ class PlasticBagPrinterService( itemCode = request.itemCode, itemName = request.itemName, ) - if (first.first) { - return LaserBag2SendResponse(success = true, message = first.second, payloadSent = first.third) + val response = if (first.success) { + LaserBag2SendResponse( + success = true, + message = first.message, + payloadSent = first.payload, + printerAck = first.printerAck, + receiveAcknowledged = first.receiveAcknowledged, + ) + } else { + val second = sendLaserBag2TcpOnce( + ip = ip, + port = port, + itemId = request.itemId, + stockInLineId = request.stockInLineId, + itemCode = request.itemCode, + itemName = request.itemName, + ) + LaserBag2SendResponse( + success = second.success, + message = second.message, + payloadSent = second.payload, + printerAck = second.printerAck, + receiveAcknowledged = second.receiveAcknowledged, + ) } - val second = sendLaserBag2TcpOnce( - ip = ip, - port = port, - itemId = request.itemId, - stockInLineId = request.stockInLineId, - itemCode = request.itemCode, - itemName = request.itemName, - ) - return LaserBag2SendResponse( - success = second.first, - message = second.second, - payloadSent = second.third, - ) + if (response.success && response.receiveAcknowledged) { + try { + persistLaserLastReceiveSuccess(request, response.printerAck) + } catch (e: Exception) { + logger.warn("Could not persist laser last receive success: {}", e.message) + } + } + return response } private fun resolveLaserBag2Host(): String { @@ -209,6 +275,17 @@ class PlasticBagPrinterService( return v.toIntOrNull() ?: DEFAULT_LASER_BAG2_PORT } + /** + * TCP connect probe to configured [LASER_PRINT.host] / [LASER_PRINT.port] (same endpoint as [sendLaserBag2Job] / auto-send). + * @return Triple(ok, host, port) + */ + fun probeLaserBag2Tcp(): Triple { + val host = resolveLaserBag2Host() + val port = resolveLaserBag2Port() + val (ok, _) = checkTcpPrinter(host, port, "Laser") + return Triple(ok, host, port) + } + private fun sendLaserBag2TcpOnce( ip: String, port: Int, @@ -216,7 +293,7 @@ class PlasticBagPrinterService( stockInLineId: Long?, itemCode: String?, itemName: String?, - ): Triple { + ): LaserBag2TcpResult { val codeStr = (itemCode ?: "").trim().replace(";", ",") val nameStr = (itemName ?: "").trim().replace(";", ",") val payload = if (itemId != null && stockInLineId != null) { @@ -233,27 +310,61 @@ class PlasticBagPrinterService( val out = socket.getOutputStream() out.write(bytes) out.flush() + // Half-close the write side so the peer sees a clean end-of-send before we read the ack. + // Abrupt full socket close right after read often makes EZCAD log "Remote TCP client disconnected". + try { + socket.shutdownOutput() + } catch (_: Exception) { + } + var ackRaw: String? = null + var receiveAck = false socket.soTimeout = 500 try { val buf = ByteArray(4096) val n = socket.getInputStream().read(buf) if (n > 0) { - val ack = String(buf, 0, n, StandardCharsets.UTF_8).trim().lowercase() - if (ack.contains("receive") && !ack.contains("invalid")) { - return Triple(true, "已送出激光機:$payload(已確認)", payload) + ackRaw = String(buf, 0, n, StandardCharsets.UTF_8).trim() + val ackLower = ackRaw.lowercase() + if (ackLower.contains("receive") && !ackLower.contains("invalid")) { + receiveAck = true + logger.info( + "Laser TCP ack (receive): {}:{} sent={} ack={}", + ip, + port, + payload, + ackRaw, + ) + } else if (ackRaw.isNotEmpty()) { + logger.info( + "Laser TCP response: {}:{} sent={} ack={}", + ip, + port, + payload, + ackRaw, + ) } } } catch (_: SocketTimeoutException) { - // Same as Python: ignore read timeout, treat as sent + // Same as Python Bag3: ignore read timeout, payload was still sent + } + val msg = if (receiveAck) { + "已送出激光機:$payload(已確認)" + } else { + "已送出激光機:$payload" } - return Triple(true, "已送出激光機:$payload", payload) + return LaserBag2TcpResult(true, msg, payload, ackRaw, receiveAck) } catch (e: ConnectException) { - return Triple(false, "無法連線至 $ip:$port,請確認激光機已開機且 IP 正確。", payload) + return LaserBag2TcpResult(false, "無法連線至 $ip:$port,請確認激光機已開機且 IP 正確。", payload, null, false) } catch (e: SocketTimeoutException) { - return Triple(false, "連線逾時 ($ip:$port),請檢查網路與連接埠。", payload) + return LaserBag2TcpResult(false, "連線逾時 ($ip:$port),請檢查網路與連接埠。", payload, null, false) } catch (e: Exception) { - return Triple(false, "激光機送出失敗:${e.message}", payload) + return LaserBag2TcpResult(false, "激光機送出失敗:${e.message}", payload, null, false) } finally { + try { + Thread.sleep(100) + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } try { socket?.close() } catch (_: Exception) { diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt index e27a108..a8e5fdc 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt @@ -15,4 +15,6 @@ data class LaserBag2JobSendResult( val itemCode: String?, val success: Boolean, val message: String, + val printerAck: String? = null, + val receiveAcknowledged: Boolean = false, ) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendRequest.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendRequest.kt index c135a19..5c0c81c 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendRequest.kt @@ -3,6 +3,9 @@ package com.ffii.fpsms.modules.jobOrder.web.model /** * Body for Bag2.py-style laser TCP send: `json;itemCode;itemName;;` (UTF-8). * Optional [printerIp] / [printerPort] override system settings [LASER_PRINT.host] / [LASER_PRINT.port]. + * + * Optional job metadata is used to persist [com.ffii.fpsms.modules.common.SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS] + * when the printer returns a receive ack. */ data class LaserBag2SendRequest( val itemId: Long? = null, @@ -11,4 +14,9 @@ data class LaserBag2SendRequest( val itemName: String? = null, val printerIp: String? = null, val printerPort: Int? = null, + val jobOrderId: Long? = null, + val jobOrderNo: String? = null, + val lotNo: String? = null, + /** AUTO (auto-send) or MANUAL (/laserPrint); optional. */ + val source: String? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendResponse.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendResponse.kt index 07c42df..1c68bfe 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendResponse.kt @@ -4,4 +4,8 @@ data class LaserBag2SendResponse( val success: Boolean, val message: String, val payloadSent: String? = null, + /** Raw bytes from the laser TCP peer after our payload (often `receive;;`). */ + val printerAck: String? = null, + /** True when [printerAck] contained `receive` and not `invalid` (same rule as Bag3.py). */ + val receiveAcknowledged: Boolean = false, ) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SettingsResponse.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SettingsResponse.kt index 673e3a1..ec6e64b 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SettingsResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SettingsResponse.kt @@ -2,9 +2,11 @@ package com.ffii.fpsms.modules.jobOrder.web.model /** * @param itemCodes Comma-separated item codes for the laser job list filter (e.g. `PP1175` or `PP1175,AB123`). Empty string means no filter (show all packaging job orders). + * @param lastReceiveSuccess Last job where the laser printer returned a receive ack (from settings JSON); null if never recorded or empty. */ data class LaserBag2SettingsResponse( val host: String, val port: Int, val itemCodes: String, + val lastReceiveSuccess: LaserLastReceiveSuccessDto? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserLastReceiveSuccessDto.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserLastReceiveSuccessDto.kt new file mode 100644 index 0000000..b4676d8 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserLastReceiveSuccessDto.kt @@ -0,0 +1,22 @@ +package com.ffii.fpsms.modules.jobOrder.web.model + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +/** + * Persisted in [com.ffii.fpsms.modules.common.SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS] when the laser + * TCP peer returns a receive-style ack ([com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse.receiveAcknowledged]). + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class LaserLastReceiveSuccessDto( + val jobOrderId: Long? = null, + /** Job order code (工單號). */ + val jobOrderNo: String? = null, + val lotNo: String? = null, + val itemId: Long? = null, + val stockInLineId: Long? = null, + val printerAck: String? = null, + /** ISO-8601 instant string (server UTC). */ + val sentAt: String? = null, + /** e.g. AUTO (scheduler/auto-send) or MANUAL (/laserPrint row). */ + val source: String? = null, +) diff --git a/src/main/resources/db/changelog/changes/20260327_laser_last_receive_success/01_laser_print_last_receive_success.sql b/src/main/resources/db/changelog/changes/20260327_laser_last_receive_success/01_laser_print_last_receive_success.sql new file mode 100644 index 0000000..a85edc6 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260327_laser_last_receive_success/01_laser_print_last_receive_success.sql @@ -0,0 +1,8 @@ +--liquibase formatted sql +--changeset fpsms:20260327_laser_print_last_receive_success + +INSERT INTO `settings` (`name`, `value`, `category`, `type`) +SELECT 'LASER_PRINT.lastReceiveSuccess', '{}', 'LASER', 'string' +WHERE NOT EXISTS ( + SELECT 1 FROM `settings` s WHERE s.name = 'LASER_PRINT.lastReceiveSuccess' +);