From 40fcaba4b074858df2ee696e2c2f6110a5dd99ae Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Sat, 10 Jan 2026 23:29:25 +0800 Subject: [PATCH] adding printer testing --- .../service/PlasticBagPrinterService.kt | 221 ++++++++++++++++++ .../web/PlasticBagPrinterController.kt | 90 +++++++ .../jobOrder/web/model/PlasticPrintRequest.kt | 22 ++ 3 files changed, 333 insertions(+) create mode 100644 src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt 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 new file mode 100644 index 0000000..8c6a4d8 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt @@ -0,0 +1,221 @@ +package com.ffii.fpsms.modules.jobOrder.service + +import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository +import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest +import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest +import org.springframework.stereotype.Service + +import java.awt.Color +import java.awt.Font +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.imageio.ImageIO +import com.google.zxing.BarcodeFormat +import com.google.zxing.qrcode.QRCodeWriter +import java.net.Socket +import java.net.InetSocketAddress +import java.io.PrintWriter +import java.io.DataOutputStream +import java.nio.charset.Charset + +@Service +open class PlasticBagPrinterService( + val jobOrderRepository: JobOrderRepository, +) { + fun generatePrintJobBundle(itemCode: String, lotNo: String, expiryDate: String, productName: String): ByteArray { + val baos = ByteArrayOutputStream() + ZipOutputStream(baos).use { zos -> + + // Use unique names based on the Lot Number + val nameFile = "${lotNo}_product.bmp" + val expFile = "${lotNo}_expiry.bmp" + val qrFile = "${lotNo}_qr.bmp" + + // 1. Generate Bitmaps + addToZip(zos, nameFile, createMonochromeBitmap(productName, 5365, 704)) + addToZip(zos, expFile, createMonochromeBitmap(expiryDate, 4203, 1173)) + addToZip(zos, qrFile, createQrCodeBitmap("$itemCode|$lotNo|$expiryDate", 1000)) + + // 2. Generate the .image file with dynamic references + val imageXml = """ + + + + PRODUCT_NAME + 0250 + $nameFile + + + EXPIRY_DATE + 5002500 + $expFile + + + QR_CODE + 7503750 + $qrFile + + + + """.trimIndent() + + addToZip(zos, "$lotNo.image", imageXml.toByteArray()) + + // 3. Generate the .job file pointing to the new .image [cite: 2] + val jobXml = "$lotNo.image" + addToZip(zos, "$lotNo.job", jobXml.toByteArray()) + } + return baos.toByteArray() + } + + private fun createMonochromeBitmap(text: String, width: Int, height: Int): ByteArray { + val image = BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY) // Essential for printers + val g = image.createGraphics() + g.color = Color.WHITE + g.fillRect(0, 0, width, height) + g.color = Color.BLACK + g.font = Font("Arial", Font.BOLD, 400) + g.drawString(text, 50, height - 200) + g.dispose() + + val baos = ByteArrayOutputStream() + ImageIO.write(image, "bmp", baos) + return baos.toByteArray() + } + + private fun createQrCodeBitmap(content: String, size: Int): ByteArray { + val bitMatrix = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, size, size) + val image = BufferedImage(size, size, BufferedImage.TYPE_BYTE_BINARY) + for (x in 0 until size) { + for (y in 0 until size) { + image.setRGB(x, y, if (bitMatrix.get(x, y)) Color.BLACK.rgb else Color.WHITE.rgb) + } + } + val baos = ByteArrayOutputStream() + ImageIO.write(image, "bmp", baos) + return baos.toByteArray() + } + + private fun addToZip(zos: ZipOutputStream, fileName: String, content: ByteArray) { + zos.putNextEntry(ZipEntry(fileName)) + zos.write(content) + zos.closeEntry() + } + + fun sendDataFlexJob(request: PrintRequest) { + try { + // 1. Establish Socket Connection + val socket = Socket(request.printerIp, request.printerPort) + socket.soTimeout = 5000 // 5 seconds timeout + + val writer = PrintWriter(socket.getOutputStream(), true) + + // 2. Format the command for DataFlex 6330 + // Note: This format depends on your specific label design in CLARiSOFT. + // Many DataFlex printers use the "Job Select" or "Set Variable" protocol. + + val command = """ + ^SVAR1|${request.itemCode} + ^SVAR2|${request.itemName} + ^SVAR3|${request.lotNo} + ^SVAR4|${request.expiryDate} + ^PRNT1 + """.trimIndent() + + // 3. Send and Close + writer.print(command) + writer.flush() + + socket.close() + println("Successfully sent command to DataFlex at ${request.printerIp}") + + } catch (e: Exception) { + throw RuntimeException("Failed to communicate with DataFlex printer: ${e.message}") + } + } + + fun sendLaserMark(request: LaserRequest) { + Socket().use { socket -> + socket.connect(InetSocketAddress(request.printerIp, request.printerPort.toInt()), 3000) + val writer = PrintWriter(socket.getOutputStream(), true) + + // Standard HANS/General Laser Command Format: + // [STX]Command|Variable1|Variable2[ETX] + // Note: Exact protocol depends on your HANS controller software (usually JCZ or similar) + + val command = "STRSET|${request.templateId}|LOT=${request.lotNo}|EXP=${request.expiryDate}\n" + writer.print(command) + writer.flush() + + // Optional: Trigger the marking immediately + // writer.print("STRMARK\n") + // writer.flush() + } + } + + /* + fun previewLaser(request: LaserRequest) { + Socket().use { socket -> + socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 2000) + val writer = PrintWriter(socket.getOutputStream(), true) + + // Typical HANS command for Red Light Preview + writer.println("JOBLOAD|${request.templateId}") + writer.println("REDLIGHT|1") // 1 to turn on, 0 to turn off + writer.flush() + } + } */ + + fun sendLaserPreview(request: LaserRequest) { + Socket().use { socket -> + socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 3000) + val writer = PrintWriter(socket.getOutputStream(), true) + + // HANS Protocol for Red Light + // Often requires loading the job first so it knows the boundary + val command = """ + JOBLOAD|${request.templateId} + REDLIGHT|1 + """.trimIndent() + + writer.println(command) + writer.flush() + } + } + + fun sendTscPrintJob(request: PrintRequest) { + Socket().use { socket -> + try { + socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 3000) + val out = DataOutputStream(socket.getOutputStream()) + + // Construct TSPL commands + // Note: Coordinates (x,y) are in dots. + // For 203 DPI: 8 dots = 1mm. + val tspl = StringBuilder() + .append("SIZE 100 mm, 50 mm\n") // Adjust to your label size + .append("GAP 3 mm, 0 mm\n") + .append("DIRECTION 1\n") + .append("CLS\n") // Clear buffer + // Text commands: TEXT x, y, "font", rotation, x-multi, y-multi, "content" + .append("TEXT 50,50,\"ROMAN.TTF\",0,1,1,\"ITEM: ${request.itemCode}\"\n") + .append("TEXT 50,100,\"ROMAN.TTF\",0,1,1,\"NAME: ${request.itemName}\"\n") + .append("TEXT 50,150,\"ROMAN.TTF\",0,1,1,\"LOT: ${request.lotNo}\"\n") + .append("TEXT 50,200,\"ROMAN.TTF\",0,1,1,\"EXP: ${request.expiryDate}\"\n") + .append("PRINT 1,1\n") + .toString() + + // TSC printers usually expect encoding in Windows-1252 or Thai (TIS-620) + val bytes = tspl.toByteArray(Charset.forName("TIS-620")) + out.write(bytes) + out.flush() + + } catch (e: Exception) { + throw RuntimeException("TSC Printer Error: ${e.message}") + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt new file mode 100644 index 0000000..29cb031 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt @@ -0,0 +1,90 @@ +package com.ffii.fpsms.modules.jobOrder.web + +import com.ffii.fpsms.modules.jobOrder.service.PlasticBagPrinterService +import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest +import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpHeaders +import org.springframework.web.bind.annotation.* +import java.time.LocalDate +import org.springframework.http.ResponseEntity + +@RestController +@RequestMapping("/plastic") +class PlasticBagPrinterController( + private val plasticBagPrinterService: PlasticBagPrinterService, +) { + + /** + * Test API to generate and download the printer job files as a ZIP. + * ONPACK2030 + */ + @GetMapping("/get-printer6") + fun testPrintJob( + @RequestParam itemCode: String, + @RequestParam lotNo: String, + @RequestParam expiryDate: String, + @RequestParam productName: String, + response: HttpServletResponse + ) { + // Generate the ZIP bundle via the service + val zipBytes = plasticBagPrinterService.generatePrintJobBundle( + itemCode, + lotNo, + expiryDate, + productName + ) + + // Set headers for file download + response.contentType = "application/zip" + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"Job_$lotNo.zip\"") + response.setContentLength(zipBytes.size) + + // Write to response stream + response.outputStream.write(zipBytes) + response.outputStream.flush() + } + + @PostMapping("/print-dataflex") + fun printDataFlex(@RequestBody request: PrintRequest): ResponseEntity { + return try { + plasticBagPrinterService.sendDataFlexJob(request) + ResponseEntity.ok("Print command sent successfully") + } catch (e: Exception) { + ResponseEntity.status(500).body("Error: ${e.message}") + } + } + + @PostMapping("/print-laser") + fun printLaser(@RequestBody request: LaserRequest): ResponseEntity { + return try { + // Call the laser marking service + plasticBagPrinterService.sendLaserMark(request) + ResponseEntity.ok("Laser marking command sent successfully to ${request.printerIp}") + } catch (e: Exception) { + // Log the error and return 500 status + ResponseEntity.status(500).body("Laser Error: ${e.message}") + } + } + + @PostMapping("/preview-laser") + fun previewLaser(@RequestBody request: LaserRequest): ResponseEntity { + return try { + plasticBagPrinterService.sendLaserPreview(request) + ResponseEntity.ok("Red light activated") + } catch (e: Exception) { + ResponseEntity.status(500).body("Preview Error: ${e.message}") + } + } + + @PostMapping("/print-tsc") + fun printTsc(@RequestBody request: PrintRequest): ResponseEntity { + return try { + plasticBagPrinterService.sendTscPrintJob(request) + ResponseEntity.ok("TSC Print command sent to ${request.printerIp}") + } catch (e: Exception) { + ResponseEntity.status(500).body("Error: ${e.message}") + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt new file mode 100644 index 0000000..9aa593c --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt @@ -0,0 +1,22 @@ +package com.ffii.fpsms.modules.jobOrder.web.model + +data class PrintRequest( + val itemCode: String, + val itemName: String, + val lotNo: String, + val expiryDate: String, + val printerIp: String, + val printerPort: Int +) + +data class LaserRequest( + val templateId: String, // The name of the file on the laser controller + val itemCode: String, + val itemName: String, + val lotNo: String, + val expiryDate: String, + val power: String, // Laser intensity (0-100) + val speed: String, // Marking speed + val printerIp: String, + val printerPort: Int +) \ No newline at end of file