From 775627f215b3147fb67cea180e19d78a0ac59784 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Mon, 16 Feb 2026 09:37:14 -0600 Subject: [PATCH] feat(scanner): tcp scanner connection based on env var no more db stuff --- lstV2/server/globalUtils/scannerConnect.ts | 187 ++++++++++++++++++ .../logistics/controller/commands/bookout.ts | 155 +++++++++++++++ .../commands/removeAsNonReusable.ts | 148 ++++---------- .../services/logistics/route/bookout.ts | 87 ++++++++ 4 files changed, 468 insertions(+), 109 deletions(-) create mode 100644 lstV2/server/globalUtils/scannerConnect.ts create mode 100644 lstV2/server/services/logistics/controller/commands/bookout.ts create mode 100644 lstV2/server/services/logistics/route/bookout.ts diff --git a/lstV2/server/globalUtils/scannerConnect.ts b/lstV2/server/globalUtils/scannerConnect.ts new file mode 100644 index 0000000..305fc36 --- /dev/null +++ b/lstV2/server/globalUtils/scannerConnect.ts @@ -0,0 +1,187 @@ +/** + * Using this to make a scanner connection to the server. + */ + +import net from "net"; + +interface QueuedCommand { + command: string; + resolve: (value: string) => void; + reject: (reason?: any) => void; + timeout: NodeJS.Timeout; +} + +const STX = "\x02"; +const ETX = "\x03"; + +// const prodIP = process.env.SERVER_IP as string; +// const prodPort = parseInt(process.env.SCANNER_PORT || "50000", 10); +// const scannerID = `${process.env.SCANNER_ID}@`; +//const scannerCommand = "AlplaPRODcmd00000042#000028547"; // top of the picksheet + +export class ScannerClient { + private socket = new net.Socket(); + private connected = false; + + private queue: QueuedCommand[] = []; + private processing = false; + + private incomingBuffer = ""; + + constructor( + private host: string, + private port: number, + private scannerId: string, + ) { + this.initialize(); + } + + private initialize() { + this.socket.connect(this.port, this.host, () => { + console.info("Connected to scanner"); + this.connected = true; + }); + + this.socket.on("data", (data) => this.handleData(data)); + + this.socket.on("close", () => { + console.log("Scanner connection closed"); + this.connected = false; + }); + + this.socket.on("error", (err) => { + console.error("Scanner error:", err); + }); + } + + // ✅ Public method you use + public scan(command: string): Promise { + if (!this.connected) { + return Promise.reject("Scanner not connected"); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.processing = false; + reject("Scanner timeout"); + this.processQueue(); + }, 5000); // 5s safety timeout + + this.queue.push({ + command, + resolve, + reject, + timeout, + }); + + this.processQueue(); + }); + } + + // ✅ Ensures strict FIFO processing + private processQueue() { + if (this.processing) return; + if (this.queue.length === 0) return; + + this.processing = true; + + const current = this.queue[0]; + const message = Buffer.from( + `${STX}${this.scannerId}${current.command}${ETX}`, + "ascii", + ); + + this.socket.write(message); + } + + // ✅ Handles full STX/ETX framed responses + private handleData(data: Buffer) { + console.log( + "ASCII:", + data + .toString("ascii") + .replace(/\x00/g, "") // remove null bytes + .replace(/\x1B\[[0-9;?]*[A-Za-z]/g, "") // remove ANSI escape codes + .trim(), + ); + + const current = this.queue.shift(); + if (current) { + clearTimeout(current.timeout); + current.resolve(data.toString("ascii")); + } + + this.processing = false; + this.processQueue(); + } +} + +export const scanner = new ScannerClient( + process.env.SERVER_IP!, + parseInt(process.env.SCANNER_PORT!, 10), + `${process.env.SCANNER_ID}@`, +); + +// export const connectToScanner = () => { +// if (!process.env.SERVER_IP || !process.env.SCANNER_PORT) { +// return { +// success: false, +// message: "Missing ServerIP or ServerPort", +// }; +// } + +// scanner.connect(prodPort, prodIP, () => { +// console.log("Connected to scanner"); +// connected = true; +// }); +// }; + +// export const scan = async (command: string) => { +// if (!connected) { +// return { +// success: false, +// message: "Scanner is not connected, please contact admin", +// }; +// } +// if (inScanCommand) { +// bufferCommands.push({ timeStamp: new Date(Date.now()), command: command }); +// } + +// // we are going to set to scanning +// inScanCommand = true; + +// const message = Buffer.from(`${STX}${scannerID}${command}${ETX}`, "ascii"); +// scanner.write(message); +// await new Promise((resolve) => setTimeout(resolve, 750)); + +// inScanCommand = false; + +// if (bufferCommands.length > 0) { +// await scan(bufferCommands[0].command); +// bufferCommands.shift(); +// } + +// return { +// success: true, +// message: "Scan completed", +// }; +// }; + +// scanner.on("data", async (data) => { +// console.log( +// "Response:", +// data +// .toString("ascii") +// .replace(/\x00/g, "") // remove null bytes +// .replace(/\x1B\[[0-9;?]*[A-Za-z]/g, "") // remove ANSI escape codes +// .trim(), +// ); +// }); + +// scanner.on("close", () => { +// console.log("Connection closed"); +// }); + +// scanner.on("error", (err) => { +// console.error("Scanner error:", err); +// }); diff --git a/lstV2/server/services/logistics/controller/commands/bookout.ts b/lstV2/server/services/logistics/controller/commands/bookout.ts new file mode 100644 index 0000000..5e5986c --- /dev/null +++ b/lstV2/server/services/logistics/controller/commands/bookout.ts @@ -0,0 +1,155 @@ +import axios from "axios"; +import net from "net"; +import { db } from "../../../../../database/dbclient.js"; +import { commandLog } from "../../../../../database/schema/commandLog.js"; +import { createSSCC } from "../../../../globalUtils/createSSCC.js"; +import { prodEndpointCreation } from "../../../../globalUtils/createUrl.js"; +import { scanner } from "../../../../globalUtils/scannerConnect.js"; +import { tryCatch } from "../../../../globalUtils/tryCatch.js"; +import { createLog } from "../../../logger/logger.js"; +import { query } from "../../../sqlServer/prodSqlServer.js"; +import { sqlQuerySelector } from "../../../sqlServer/utils/querySelector.utils.js"; + +type Data = { + runningNr: number; + reason: string; + user: string; +}; +export const bookOutPallet = async (data: Data) => { + const { runningNr, reason, user } = data; + + if (!reason || reason.length < 4) { + return { + success: false, + status: 400, + message: "The reason provided is to short", + data: [], + }; + } + + const queryCheck = sqlQuerySelector("inventoryInfo.query"); + + if (!queryCheck.success) { + return { + success: false, + status: 400, + message: queryCheck.message, + data: data, + }; + } + const { data: label, error: labelError } = (await tryCatch( + query( + queryCheck.query!.replace("[runningNr]", `${runningNr}`), + "labelQuery", + ), + )) as any; + + if (labelError) { + return { + success: false, + status: 400, + message: labelError.message, + data: labelError, + }; + } + + // check if we are in ppoo + if (label.data.length <= 0) { + return { + success: false, + status: 400, + message: `${runningNr} is not currently in ppoo, please move to ppoo before trying to book-out`, + data: [], + }; + } + + // check if the label is blocked for coa. + if ( + label.data[0].blockingReason && + !label.data[0].blockingReason?.includes("COA") + ) { + return { + success: false, + status: 400, + message: `${runningNr} is not currently blocked for coa, to get this pallet booked out please take the label to quality to be released then you can book-out.`, + data: [], + }; + } + + if (label.data[0].blockingReason) { + await scanner.scan("AlplaPRODcmd89"); + await scanner.scan(`${label.data[0].barcode}`); + } + + // create the url to post + const url = await prodEndpointCreation( + "/public/v1.1/Manufacturing/ProductionControlling/BookOut", + ); + const SSCC = await createSSCC(runningNr); + + const bookOutData = { + sscc: SSCC.slice(2), + scannerId: "666", + }; + + try { + const results = await axios.post(url, bookOutData, { + headers: { + "X-API-Key": process.env.TEC_API_KEY || "", + "Content-Type": "application/json", + }, + }); + + if (results.data.Errors) { + return { + success: false, + status: 400, + message: results.data.Errors.Error.Description, + }; + } + + // if (results.data.Result !== 0) { + // console.log("stopping here and closing to soon", results); + // return { + // success: false, + // status: 400, + // message: results.data.Message, + // }; + // } + + const { data: commandL, error: ce } = await tryCatch( + db.insert(commandLog).values({ + commandUsed: "book out", + bodySent: data, + reasonUsed: reason, + }), + ); + + return { + success: true, + message: `${runningNr} was booked out`, + status: results.status, + }; + } catch (error: any) { + console.log(bookOutData); + + return { + success: false, + status: 400, + message: error.response?.data, + data: error.response?.data, + }; + } + + // }); + + /** + * book out the label with + * url /public/v1.1/Manufacturing/ProductionControlling/BookOut + * { + * "sscc": "string", + * "scannerId": "string" + * } + */ + //---------------------------------------------------------------------------------------\\ +}; diff --git a/lstV2/server/services/logistics/controller/commands/removeAsNonReusable.ts b/lstV2/server/services/logistics/controller/commands/removeAsNonReusable.ts index 15a0be4..713eda6 100644 --- a/lstV2/server/services/logistics/controller/commands/removeAsNonReusable.ts +++ b/lstV2/server/services/logistics/controller/commands/removeAsNonReusable.ts @@ -1,120 +1,50 @@ -import axios from "axios"; -import { commandLog } from "../../../../../database/schema/commandLog.js"; -import { prodEndpointCreation } from "../../../../globalUtils/createUrl.js"; -import { tryCatch } from "../../../../globalUtils/tryCatch.js"; -import { lstAuth } from "../../../../index.js"; -import { createSSCC } from "../../../../globalUtils/createSSCC.js"; import { db } from "../../../../../database/dbclient.js"; -import net from "net"; +import { commandLog } from "../../../../../database/schema/commandLog.js"; +import { scanner } from "../../../../globalUtils/scannerConnect.js"; +import { tryCatch } from "../../../../globalUtils/tryCatch.js"; import { query } from "../../../sqlServer/prodSqlServer.js"; import { labelInfo } from "../../../sqlServer/querys/warehouse/labelInfo.js"; -import { settings } from "../../../../../database/schema/settings.js"; -import { eq } from "drizzle-orm"; -import { serverData } from "../../../../../database/schema/serverData.js"; + export const removeAsNonReusable = async (data: any) => { - // const removalUrl = await prodEndpointCreation( - // "/public/v1.0/Warehousing/RemoveAsNonReusableMaterial" - // ); + // get the label info + const { data: label, error: labelError } = (await tryCatch( + query(labelInfo.replaceAll("[runningNr]", data.runningNr), "Label Info"), + )) as any; - // const sscc = await createSSCC(data.runningNr); + if (label.data[0].stockStatus === "notOnStock") { + return { + success: false, + message: `The label: ${data.runningNr} is not currently in stock`, + data: [], + }; + } - // const { data: remove, error } = await tryCatch( - // axios.post( - // removalUrl, - // { scannerId: "500", sscc: sscc.slice(2) }, - // { - // headers: { Authorization: `Basic ${lstAuth}` }, - // } - // ) - // ); + if (label.data[0].blockingReason) { + return { + success: false, + status: 400, + message: `${data.runningNr} is currently blocked, to get this pallet removed please take the label to quality to be released then you can remove.`, + data: [], + }; + } - // use a scanner tcp connection to trigger this process - const STX = "\x02"; - const ETX = "\x03"; - const scanner = new net.Socket(); - let stage = 0; - // get the label info - const { data: label, error: labelError } = (await tryCatch( - query(labelInfo.replaceAll("[runningNr]", data.runningNr), "Label Info") - )) as any; + await scanner.scan("AlplaPRODcmd23"); + await scanner.scan(`${label.data[0].barcode}`); - if (label.data[0].stockStatus === "notOnStock") { - return { - success: false, - message: `The label: ${data.runningNr} is not currently in stock`, - data: [], - }; - } + let reason = data.reason || ""; + delete data.reason; - // get the server ip based on the token. - const setting = await db.select().from(settings); + const { data: commandL, error: ce } = await tryCatch( + db.insert(commandLog).values({ + commandUsed: "removeAsNonReusable", + bodySent: data, + reasonUsed: reason, + }), + ); - const plantInfo = await db.select().from(serverData); - const plantToken = setting.filter((n: any) => n.name === "plantToken"); - const scannerID = setting.filter((n: any) => n.name === "scannerID"); - const scannerPort = setting.filter((n: any) => n.name === "scannerPort"); - const plantData = plantInfo.filter( - (p: any) => p.plantToken === plantToken[0].value - ); - - scanner.connect( - parseInt(scannerPort[0].value), - plantData[0].idAddress!, - async () => { - // need to get the ip from the server data and scanner port - //console.log(`connected to scanner`); - scanner.write(`${STX}${scannerID[0].value}@AlplaPRODcmd23${ETX}`); - } - ); - scanner.on("data", (data) => { - const response = data.toString(); - //console.log("Received:", response.trimStart()); - if (stage === 0) { - stage = 1; - scanner.write( - `${STX}${scannerID[0].value}@${label.data[0].Barcode}${ETX}` - ); - } else if (stage === 1) { - scanner.end(); - } - }); - scanner.on("close", () => { - //console.log("Connection closed"); - scanner.destroy(); - }); - scanner.on("error", (err) => { - //console.error("Scanner error:", err); - scanner.destroy(); - return { - success: false, - message: `The label: ${data.runningNr} encountering an error while being removed, please try again`, - data: [], - }; - }); - - // if (error) { - // //console.log(error); - // return { - // success: false, - // message: `There was an error removing ${data.runningNr}`, - // data: [], - // }; - // } - - let reason = data.reason || ""; - delete data.reason; - - const { data: commandL, error: ce } = await tryCatch( - db.insert(commandLog).values({ - commandUsed: "removeAsNonReusable", - bodySent: data, - reasonUsed: reason, - }) - ); - - return { - success: true, - message: `The label: ${data.runningNr}, was removed`, - data: [], - }; + return { + success: true, + message: `The label: ${data.runningNr}, was removed`, + data: [], + }; }; diff --git a/lstV2/server/services/logistics/route/bookout.ts b/lstV2/server/services/logistics/route/bookout.ts new file mode 100644 index 0000000..0f65b42 --- /dev/null +++ b/lstV2/server/services/logistics/route/bookout.ts @@ -0,0 +1,87 @@ +import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; +import { verify } from "hono/jwt"; +import { apiHit } from "../../../globalUtils/apiHits.js"; +import { tryCatch } from "../../../globalUtils/tryCatch.js"; +//import { authMiddleware } from "../../auth/middleware/authMiddleware.js"; +import { bookOutPallet } from "../controller/commands/bookout.js"; + +const app = new OpenAPIHono(); + +const responseSchema = z.object({ + success: z.boolean().optional().openapi({ example: true }), + message: z.string().optional().openapi({ example: "user access" }), +}); + +app.openapi( + createRoute({ + tags: ["logistics"], + summary: "Consumes material based on its running number", + method: "post", + path: "/bookout", + //middleware: authMiddleware, + description: + "Provided a running number and lot number you can consume material.", + responses: { + 200: { + content: { "application/json": { schema: responseSchema } }, + description: "stopped", + }, + 400: { + content: { "application/json": { schema: responseSchema } }, + description: "Failed to stop", + }, + 401: { + content: { "application/json": { schema: responseSchema } }, + description: "Failed to stop", + }, + }, + }), + async (c) => { + const { data, error } = await tryCatch(c.req.json()); + + if (error) { + return c.json( + { + success: false, + message: "Missing data please try again", + error, + }, + 400, + ); + } + apiHit(c, { endpoint: "/bookout", lastBody: data }); + //const authHeader = c.req.header("Authorization"); + //const token = authHeader?.split("Bearer ")[1] || ""; + + //const payload = await verify(token, process.env.JWT_SECRET!); + try { + //return apiReturn(c, true, access?.message, access?.data, 200); + + //const pointData = { ...data, user: payload.user }; + + const bookout = await bookOutPallet(data); + + console.log("from booout:", bookout); + return c.json( + { + success: bookout?.success, + message: bookout?.message, + data: bookout.data, + }, + 200, + ); + } catch (error) { + console.log("from error:", error); + //return apiReturn(c, false, "Error in setting the user access", error, 400); + return c.json( + { + success: false, + message: "Missing data please try again", + error, + }, + 400, + ); + } + }, +); +export default app;