@@ -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.LaserBag2SendRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse
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.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.PrintRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest
import com.ffii.fpsms.modules.jobOrder.web.model.NgpclPushResponse
import com.ffii.fpsms.modules.jobOrder.web.model.NgpclPushResponse
@@ -47,12 +48,23 @@ import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.SocketTimeoutException
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.ClassPathResource
import org.slf4j.LoggerFactory
import org.slf4j.LoggerFactory
import com.fasterxml.jackson.databind.ObjectMapper
import java.time.Duration
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDate
// Data class to store bitmap bytes + width (for XML)
// Data class to store bitmap bytes + width (for XML)
data class BitmapResult(val bytes: ByteArray, val width: Int)
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
@Service
class PlasticBagPrinterService(
class PlasticBagPrinterService(
val jobOrderRepository: JobOrderRepository,
val jobOrderRepository: JobOrderRepository,
@@ -61,6 +73,7 @@ class PlasticBagPrinterService(
private val settingsService: SettingsService,
private val settingsService: SettingsService,
private val pyJobOrderPrintSubmitService: PyJobOrderPrintSubmitService,
private val pyJobOrderPrintSubmitService: PyJobOrderPrintSubmitService,
private val environment: Environment,
private val environment: Environment,
private val objectMapper: ObjectMapper,
) {
) {
private val logger = LoggerFactory.getLogger(javaClass)
private val logger = LoggerFactory.getLogger(javaClass)
@@ -86,7 +99,43 @@ class PlasticBagPrinterService(
val itemCodes = settingsService.findByName(SettingNames.LASER_PRINT_ITEM_CODES)
val itemCodes = settingsService.findByName(SettingNames.LASER_PRINT_ITEM_CODES)
.map { it.value?.trim() ?: "" }
.map { it.value?.trim() ?: "" }
.orElse(DEFAULT_LASER_ITEM_CODES)
.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,
itemCode = request.itemCode,
itemName = request.itemName,
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 {
private fun resolveLaserBag2Host(): String {
@@ -209,6 +275,17 @@ class PlasticBagPrinterService(
return v.toIntOrNull() ?: DEFAULT_LASER_BAG2_PORT
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<Boolean, String, Int> {
val host = resolveLaserBag2Host()
val port = resolveLaserBag2Port()
val (ok, _) = checkTcpPrinter(host, port, "Laser")
return Triple(ok, host, port)
}
private fun sendLaserBag2TcpOnce(
private fun sendLaserBag2TcpOnce(
ip: String,
ip: String,
port: Int,
port: Int,
@@ -216,7 +293,7 @@ class PlasticBagPrinterService(
stockInLineId: Long?,
stockInLineId: Long?,
itemCode: String?,
itemCode: String?,
itemName: String?,
itemName: String?,
): Triple<Boolean, String, String> {
): LaserBag2TcpResult {
val codeStr = (itemCode ?: "").trim().replace(";", ",")
val codeStr = (itemCode ?: "").trim().replace(";", ",")
val nameStr = (itemName ?: "").trim().replace(";", ",")
val nameStr = (itemName ?: "").trim().replace(";", ",")
val payload = if (itemId != null && stockInLineId != null) {
val payload = if (itemId != null && stockInLineId != null) {
@@ -233,27 +310,61 @@ class PlasticBagPrinterService(
val out = socket.getOutputStream()
val out = socket.getOutputStream()
out.write(bytes)
out.write(bytes)
out.flush()
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
socket.soTimeout = 500
try {
try {
val buf = ByteArray(4096)
val buf = ByteArray(4096)
val n = socket.getInputStream().read(buf)
val n = socket.getInputStream().read(buf)
if (n > 0) {
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) {
} 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) {
} catch (e: ConnectException) {
return Triple(false, "無法連線至 $ip:$port,請確認激光機已開機且 IP 正確。", payload)
return LaserBag2TcpResult (false, "無法連線至 $ip:$port,請確認激光機已開機且 IP 正確。", payload, null, false )
} catch (e: SocketTimeoutException) {
} catch (e: SocketTimeoutException) {
return Triple(false, "連線逾時 ($ip:$port),請檢查網路與連接埠。", payload)
return LaserBag2TcpResult (false, "連線逾時 ($ip:$port),請檢查網路與連接埠。", payload, null, false )
} catch (e: Exception) {
} catch (e: Exception) {
return Triple(false, "激光機送出失敗:${e.message}", payload)
return LaserBag2TcpResult (false, "激光機送出失敗:${e.message}", payload, null, false )
} finally {
} finally {
try {
Thread.sleep(100)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
try {
try {
socket?.close()
socket?.close()
} catch (_: Exception) {
} catch (_: Exception) {