import TcpSocket from "react-native-tcp-socket"; // const STX = "\x02"; // const ETX = "\x03"; type TcpResponse = { success: boolean; message: string; data: string[]; }; type ScannerEvent = { scannerId?: string; commandDescription?: string; prompt?: string; message?: string; status: "success" | "error" | "location" | "unknown" | "scan"; lines?: string[]; }; // const ERROR_MESSAGES = [ // "Invalid barcode", // "Already scanned", // "Not on stock", // "Article tolerance for consolidation not satisfied.", // ]; const ERROR_KEYWORDS = [ "invalid barcode", "already", "not on stock", "article tolerance", "unloaded", "delivered", "blocked", ]; // function parseErpResponse(buffer: Buffer) { // const text = buffer // .toString("utf8") // .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|#[0-9A-Za-z])/g, "") // .replace(/\x02/g, "") // .replace(/\x03/g, "") // .trim(); // const noHeader = text.replace(/^\d+@/, ""); // console.log(text); // if (!noHeader.includes("Scan:")) { // return { // raw: text, // type: "error", // message: noHeader.trim(), // lines: [noHeader.trim()], // }; // } // const [actionPart, scanPart = ""] = noHeader.split("Scan:"); // const action = actionPart.trim(); // const scanClean = scanPart.trim(); // const successMatch = scanClean.match(/^(.*?)\s+V$/); // if (successMatch) { // const prompt = successMatch[1].trim(); // return { // raw: text, // type: "success", // action, // prompt, // status: "V", // lines: [action, prompt, "V"], // }; // } // // // Handles: "Production lotInvalid barcode" // // const knownErrors = [ // // "Invalid barcode", // // "Invalid machine", // // "Not on stock", // // "Article tolerance for consolidation not satisfied", // // ].sort((a, b) => b.length - a.length); // // const foundError = knownErrors.find((err) => scanClean.includes(err)); // // if (foundError) { // // const prompt = scanClean.replace(foundError, "").trim(); // // return { // // raw: text, // // type: "error", // // action, // // prompt, // // message: foundError, // // lines: [action, prompt, foundError].filter(Boolean), // // }; // // } // // return { // // raw: text, // // type: "pending", // // action, // // prompt: scanClean, // // lines: [action, scanClean].filter(Boolean), // // }; // const unitMatch = scanClean.match(/^(Unit\s+\d+\/\d+)(.*)$/); // if (unitMatch) { // const prompt = unitMatch[1].trim(); // "Unit 1/4" // const remainder = unitMatch[2].trim(); // everything after // // SUCCESS // if (remainder === "V") { // return { // raw: text, // type: "success", // action, // prompt, // status: "V", // lines: [action, prompt, "V"], // }; // } // // Known ERP errors // const knownErrors = [ // "Invalid barcode", // "Invalid machine", // "Not on stock", // "Article tolerance for consolidation not satisfied", // ]; // const foundError = knownErrors.find((err) => // remainder.toLowerCase().includes(err.toLowerCase()), // ); // if (foundError) { // return { // raw: text, // type: "error", // action, // prompt, // message: foundError, // lines: [action, prompt, foundError], // }; // } // if (remainder) { // return { // raw: text, // type: "prompt", // action, // prompt, // message: remainder, // lines: [action, prompt, remainder], // }; // } // return { // raw: text, // type: "pending", // action, // prompt, // lines: [action, prompt], // }; // } // } const parseScannerText = (buffer: Buffer) => { const text = buffer.toString("utf8"); return ( text // remove cursor movement like ESC[122C, ESC[2;1H, ESC[8q .replace(/\x1B\[[0-9;]*[A-Za-z]/g, "\n") // remove other ANSI sequences like ESC#5 .replace(/\x1B#[0-9]/g, "\n") // normalize carriage returns .replace(/\r/g, "\n") // split into clean lines .split(/\n+/) // clean each line .map((line) => line.trim()) // remove blanks .filter(Boolean) ); }; const parseScannerEvent = (lines: string[]): ScannerEvent => { const scannerId = lines[0]; const messageLines = lines.slice(1); const message = messageLines.at(-1); const commandDescription = messageLines.find((x) => /^\d+\s+/.test(x)); const prompt = messageLines.find((x) => /^Scan:/i.test(x)); let status: ScannerEvent["status"] = "unknown"; const msg = message?.toLowerCase() ?? ""; if (msg === "v") status = "success"; else if (msg && ERROR_KEYWORDS.some((keyword) => msg.includes(keyword))) status = "error"; else if (msg?.includes("scan")) status = "success"; // everything else will just be a location else if (commandDescription?.includes("Relocate")) status = "location"; // TODO: split command description and use the command id next to description for sorting. return { scannerId, commandDescription, prompt, message, status, lines, }; }; /** * Sends a Zebra-style TCP message: * 98@{scanned} */ export async function sendTcpMessage( command: string, host: string, port: number, timeoutMs = 5000, ): Promise { return new Promise((resolve) => { const responses: any = []; const client = TcpSocket.createConnection({ host, port }, () => { //console.log("Sending TCP (visible):", `${command}`); client.write(command); }); const timeout = setTimeout(() => { client.destroy(); resolve({ success: false, message: "TCP timeout", data: responses, }); }, timeoutMs); client.on("data", (data) => { //console.log("TCP received:", text); const parsed = parseScannerText(data); //console.log("scanned:", parsed); //responses.push(parsed); const cleaned = parseScannerEvent(parsed); //console.log(responses); clearTimeout(timeout); resolve({ success: true, message: "TCP Response", data: cleaned as any, }); }); client.on("error", (err) => { clearTimeout(timeout); client.destroy(); resolve({ success: false, message: err.message, data: ["Error", "Please try again"], }); }); client.on("close", () => { clearTimeout(timeout); resolve({ success: true, message: "TCP complete", data: ["Error", "Please try again"], }); }); }); }