diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 65d4397..69207f3 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -11,12 +11,14 @@ export default defineConfig({ "@renderer": resolve("src/renderer/src"), }, }, - plugins: [vue({ - template: { - compilerOptions: { - isCustomElement: tag => tag === "nfc-reader", + plugins: [ + vue({ + template: { + compilerOptions: { + isCustomElement: (tag) => tag === "nfc-reader", + }, }, - }, - })], + }), + ], }, }); diff --git a/resources/icon.png b/resources/icon.png index cf9e8b2..2367ff2 100644 Binary files a/resources/icon.png and b/resources/icon.png differ diff --git a/src/config/base.json b/src/config/base.json new file mode 100644 index 0000000..d4171fb --- /dev/null +++ b/src/config/base.json @@ -0,0 +1,4 @@ +{ + "defultServer": "http://localhost:3000", + "defaultPrinter": "192.168.1.254" +} diff --git a/src/main/LogService.ts b/src/main/LogService.ts new file mode 100644 index 0000000..6e899eb --- /dev/null +++ b/src/main/LogService.ts @@ -0,0 +1,74 @@ +import { app } from "electron"; +import * as fs from "fs"; +import * as path from "path"; + +export class LogService { + private static instance: LogService; + private logPath: string; + + private constructor() { + // Create logs directory in userData + const userDataPath = app.getPath("userData"); + const logsDir = path.join(userDataPath, "logs"); + + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + + // Monthly log file + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const monthString = `${year}-${month}`; // YYYY-MM + this.logPath = path.join(logsDir, `app_${monthString}.log`); + + this.info(`--- Log initialized at ${now.toISOString()} ---`); + console.log(`Log file: ${this.logPath}`); + } + /** + * Garantizar que sea singleton + * @returns + */ + public static getInstance(): LogService { + if (!LogService.instance) { + LogService.instance = new LogService(); + } + return LogService.instance; + } + + public info(message: string): void { + this.write("INFO", message); + } + + public error(message: string, error?: unknown): void { + const errorDetail = error ? ` | Details: ${JSON.stringify(error)}` : ""; + this.write("ERROR", `${message}${errorDetail}`); + } + + public logOperation( + operation: string, + data: unknown, + result?: unknown, + ): void { + const entry = { + operation, + data, + result, + timestamp: new Date().toISOString(), + }; + this.write("OP", JSON.stringify(entry)); + } + + private write(level: string, message: string): void { + const timestamp = new Date().toISOString(); + const logEntry = `[${timestamp}] [${level}] ${message}\n`; + + try { + fs.appendFileSync(this.logPath, logEntry, "utf8"); + } catch (err) { + console.error("Failed to write to log file:", err); + } + } +} + +export const logger = LogService.getInstance(); diff --git a/src/main/handlers.ts b/src/main/handlers.ts new file mode 100644 index 0000000..2e5aae2 --- /dev/null +++ b/src/main/handlers.ts @@ -0,0 +1,115 @@ +import { IpcMainInvokeEvent } from "electron"; +import { Result } from "../types/Result"; +import { CodeRequest, CodeResponse, PrinterRequest } from "../preload/index.d"; +import { createConnection } from "net"; +import { logger } from "./LogService"; + +/** + * LLamada al servidor para obtener la etiqueta + * @param event -- + * @param data + * @returns + */ +export async function labelReqHandler( + event: IpcMainInvokeEvent, + data: CodeRequest, +): Promise> { + logger.logOperation("labelReq", data); + console.log("nfc:labelReq", data); + try { + const endpoint = "/nfc/generate"; + const url = data.serverURL + endpoint; + console.log("fullUrl = ", url); + const response = await fetch(data.serverURL + endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + card_id: data.card_id, + override: data.override, + }), + }); + console.log("Response", response.status); + const body: CodeResponse = await response.json(); + console.log("Codigos:", body); + logger.info(`labelReq success: ${JSON.stringify(body)}`); + // body puede tener otro error dentro + return >{ + data: body, + }; + } catch (error: unknown) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const err = error as any; + logger.error("labelReq error", err); + console.error("IPC ERROR", err); + let errorStr = ""; + if (String(err.cause).includes("ECONNRESET")) { + errorStr = "Conexión perdida con el servidor"; + } else { + errorStr = "Error de red"; + } + return >{ + error: errorStr, + }; + } +} + +/** + * Llamada principa a la impresora + * TODO: + * - Mejora de control de errores + */ +export async function printReqHandler( + _event: IpcMainInvokeEvent, + data: PrinterRequest, +): Promise { + logger.logOperation("printReq", data); + return new Promise((res, rej) => { + console.log("nfc:printReq", data); + const port = 9100; + let finished = false; + + const socket = createConnection(port, data.printerURL); + socket.setTimeout(3 * 1000); + + socket.on("connect", (e) => { + console.log("Conectado!", e); + logger.info("Printer connected, sending label..."); + socket.write(data.label, "utf-8", () => { + socket.end(); + }); + }); + + socket.on("close", () => { + if (finished) return; + finished = true; + logger.info("Print job sent successfully"); + console.log("Print job sent and connection closed."); + res("ok"); + }); + + socket.on("timeout", (err) => { + if (finished) return; + finished = true; + logger.error("Print timeout", err); + console.error("Timeout", err); + socket.destroy(); + rej(new Error("timeout")); + }); + + socket.on("error", (err) => { + if (finished) return; + finished = true; + logger.error("Printer connection/transmission error", err); + let errorStr = ""; + if (err.message.includes("ETIMEDOUT")) { + errorStr = "Error de conexión con la impresora"; + rej(new Error(errorStr)); + } + console.error("Printer Error:", err.message); + socket.destroy(); + rej(err); + }); + }); +} diff --git a/src/main/index.ts b/src/main/index.ts index e748dda..22456c2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,7 +3,9 @@ import { join } from "path"; import { electronApp, optimizer, is } from "@electron-toolkit/utils"; import icon from "../../resources/icon.png?asset"; import { NfcService } from "../services/NfcService"; -import net, { createConnection } from "net"; +import { labelReqHandler, printReqHandler } from "./handlers"; +import { logger } from "./LogService"; + function createWindow(): void { // Create the browser window. const mainWindow = new BrowserWindow({ @@ -18,12 +20,16 @@ function createWindow(): void { }, }); - new NfcService((event) => { + const nfcService = new NfcService((event) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send(`nfc:${event.type}`, event); } }); + ipcMain.handle("nfc:getReaderName", () => { + return nfcService.getReaderName(); + }); + mainWindow.on("ready-to-show", () => { mainWindow.show(); }); @@ -60,64 +66,13 @@ app.whenReady().then(() => { ipcMain.on("ping", () => console.log("pong")); // HANDLE es bidireccionar ON es unidireccional - ipcMain.handle("nfc:labelReq", async (_event, data) => { - console.log("nfc:labelReq", data); - try { - const endpoint = "/nfc/generate"; - const url = data.serverURL + endpoint; - console.log("fullUrl = ", url); - const response = await fetch(data.serverURL + endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - card_id: data.card_id, - override: data.override, - }), - }); - console.log("Response", response.status); - const body = await response.json(); - console.log("Codigos:", body); - return body; - } catch (error) { - console.error(error); - return false; - } - return data; - }); + /** + * LLamada principal + * Cli - card_id -> Server - etiqueta/activacion -> Cli + */ + ipcMain.handle("nfc:labelReq", labelReqHandler); - ipcMain.handle("nfc:printReq", async (_event, data) => { - console.log("nfc:printReq", data); - const client = new net.Socket(); - const port = 9100; - const host = data.printerURL + port; - - const socket = createConnection(port, data.printerURL); - socket.setTimeout(10 * 1000); - socket.on("connect", (e) => { - console.log("Conectado!", e); - socket.write(data.label); - socket.end(); - }); - /* - client.connect(data.printerURL + port, () => { - console.log("Connected to ZSim Printer"); - const res = client.write(data.label, (res) => { - console.log("resultado de la impresion", res); - }); - - console.log("Resultado", res); - }); -*/ - socket.on("close", () => - console.log("Print job sent and connection closed."), - ); - socket.on("error", (err) => - console.error("Printer Error:", err.message), - ); - return data; - }); + ipcMain.handle("nfc:printReq", printReqHandler); ipcMain.handle("ping:url", async (_event, url: string) => { try { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 10b064b..cb111f9 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -1,4 +1,5 @@ import { ElectronAPI } from "@electron-toolkit/preload"; +import { Result } from "../types/Result.js"; export type CodeResponse = { error?: string; @@ -13,6 +14,7 @@ export type CodeResponse = { export type CodeRequest = { card_id: string; // UUIDv7 override?: boolean; + serverURL: string; }; export type PrinterRequest = { @@ -23,16 +25,14 @@ export type PrinterRequest = { export type PrinterResponse = unknown; export interface NfcAPI { + getReaderName: () => Promise; + onReader: (callback: (event: { name: string }) => void) => void; onTag: (callback: (event: { uid: string }) => void) => void; onRemoved: (callback: (event: { uid: string }) => void) => void; onError: (callback: (event: { message: string }) => void) => void; ping: (url: string) => Promise; removeAllListeners: () => void; - labelReq: (args: { - serverURL: string; - card_id: string; - override?: boolean; - }) => Promise; + labelReq: (args: CodeRequest) => Promise>; printReq: (args: { printerURL: string; label: string; diff --git a/src/preload/index.ts b/src/preload/index.ts index 256ff96..a772f79 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -5,6 +5,12 @@ import { CodeRequest, PrinterRequest } from "./index.d.js"; // Custom APIs for renderer const api = { nfc: { + getReaderName: (): Promise => { + return ipcRenderer.invoke("nfc:getReaderName"); + }, + onReader: (callback: (event: { name: string }) => void): void => { + ipcRenderer.on("nfc:reader", (_event, value) => callback(value)); + }, onTag: (callback: (event: { uid: string }) => void): void => { ipcRenderer.on("nfc:tag", (_event, value) => callback(value)); }, @@ -24,6 +30,7 @@ const api = { return ipcRenderer.invoke("ping:url", url); }, removeAllListeners: (): void => { + ipcRenderer.removeAllListeners("nfc:reader"); ipcRenderer.removeAllListeners("nfc:tag"); ipcRenderer.removeAllListeners("nfc:removed"); ipcRenderer.removeAllListeners("nfc:error"); diff --git a/src/renderer/index.html b/src/renderer/index.html index e333747..145794d 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -2,7 +2,7 @@ - Electron + Lector NFC import { type CodeResponse } from "src/preload/index.d.js"; +import { tryCatch } from "../../../types/Result.js"; import { ref, onMounted, onUnmounted } from "vue"; /** * waiting = esperando a leer una tarjeta (igual es mejor ready) @@ -10,10 +11,18 @@ import { ref, onMounted, onUnmounted } from "vue"; const readerState = ref<"waiting" | "reading" | "success" | "error">("waiting"); const uid = ref(null); const errorMsg = ref(null); -const labelOutput = ref(""); const labelOutputStructured = ref(null); +const readerName = ref("OFFLINE"); +const errors = ref([]); + +onMounted(async () => { + // Get initial reader name + readerName.value = await window.api.nfc.getReaderName(); + + window.api.nfc.onReader((event) => { + readerName.value = event.name; + }); -onMounted(() => { window.api.nfc.onTag((event) => { uid.value = event.uid; readerState.value = "success"; @@ -64,43 +73,62 @@ const mockReadCard = (): void => { const readCard = async (): Promise => { - console.log("test readcard"); + // Reset de los errores anteriores + errors.value = []; + if (readerState.value !== "waiting") return; readerState.value = "reading"; const serverURL = localStorage.getItem("serverUrl") || "http://localhost:3000"; - const printerURL = - localStorage.getItem("printerUrl") || "192.168.1.254"; + const printerURL = localStorage.getItem("printerUrl") || "192.168.1.254"; // TODO: De momento está hardcodeado porque no se puede leer la tarjeta const card_id = "019cdd39-fc08-7417-b16d-a78794a24c01"; const override = false; - try { - const res: CodeResponse = await window.api.nfc.labelReq({ - serverURL, - card_id, - override, - }); - console.log("Res IPC", res); - labelOutput.value = res.data.label; - labelOutputStructured.value = res; - console.log("Imprimiendo", res); + const codePromise = window.api.nfc.labelReq({ + serverURL, + card_id, + override, + }); - const impRes = await window.api.nfc.printReq({ - printerURL, - label: res.data.label, - }); + const res = await tryCatch(codePromise); + console.log("RES IPC:", res); - console.log("Impreso", impRes); + // Error de la llamada -> sin conexion al servidor + if (res.error != undefined) { + errors.value = [...errors.value, String(res.error)]; + delayWaiting(); + return; + } + // Error que viene del servidor + if (res.data?.error != undefined) { + errors.value = [...errors.value, String(res.data.error)]; + delayWaiting(); + return; + } + const labelData = res.data.data.data; + labelOutputStructured.value = res.data.data; + console.log("label data", labelData); - readerState.value = "waiting"; - } catch (e) { - console.error(e); - readerState.value = "error"; + const impPromise = window.api.nfc.printReq({ + printerURL, + label: labelData.label, + }); + + const resPrint = await tryCatch(impPromise); + console.log("print", resPrint); + if (resPrint.error != undefined) { + let errorStr = ""; + if (resPrint.error.message.includes("timeout")) { + errorStr = "Error de conexión con la impresora"; + } + errors.value = [...errors.value, errorStr]; delayWaiting(); } + + delayWaiting(); }; const delayWaiting = (): void => { @@ -125,8 +153,8 @@ const triggerError = (): void => {
- MODULE - NFC_RD_01 + LECTOR + {{ readerName }}
@@ -134,7 +162,7 @@ const triggerError = (): void => {
-

TAP CARD TO INITIALIZE

+

ACERCA UN NFC PARA EMPEZAR

{
+ +
+ + Error: {{ error }} + +
+ + + + {{ JSON.stringify(labelOutputStructured.error) }} + + Código {{ labelOutputStructured.data.code }} Etiqueta {{ labelOutputStructured.data.label }} diff --git a/src/services/NfcService.ts b/src/services/NfcService.ts index f0a18a8..c574bcc 100644 --- a/src/services/NfcService.ts +++ b/src/services/NfcService.ts @@ -1,13 +1,16 @@ import PCSC, { Tag, Reader } from "@tockawa/nfc-pcsc"; +import { logger } from "../main/LogService"; export type NfcEvent = | { type: "tag"; uid: string } | { type: "removed"; uid: string } + | { type: "reader"; name: string } | { type: "error"; message: string }; export class NfcService { private nfc: PCSC; private onEvent?: (event: NfcEvent) => void; + private currentReaderName: string = "OFFLINE"; constructor(onEvent?: (event: NfcEvent) => void) { this.nfc = new PCSC.default(); @@ -15,12 +18,22 @@ export class NfcService { this.init(); } + public getReaderName(): string { + return this.currentReaderName; + } + private init(): void { this.nfc.on("reader", (reader: Reader) => { console.log(`Lector detectado: ${reader.name}`); + logger.info(`NFC Reader detected: ${reader.name}`); + this.currentReaderName = reader.name; + if (this.onEvent) { + this.onEvent({ type: "reader", name: reader.name }); + } reader.on("card", async (card: Tag) => { console.log(`Tarjeta detectada! UID: ${card.uid}`); + logger.info(`NFC Tag detected: ${card.uid}`); if (this.onEvent) { this.onEvent({ type: "tag", uid: card.uid }); } @@ -28,6 +41,7 @@ export class NfcService { reader.on("card.off", async (card: Tag) => { console.log(`Tarjeta retirada: ${card.uid}`); + logger.info(`NFC Tag removed: ${card.uid}`); if (this.onEvent) { this.onEvent({ type: "removed", uid: card.uid }); } @@ -45,6 +59,11 @@ export class NfcService { reader.on("end", () => { console.log(`Lector desconectado: ${reader.name}`); + logger.info(`NFC Reader disconnected: ${reader.name}`); + this.currentReaderName = "OFFLINE"; + if (this.onEvent) { + this.onEvent({ type: "reader", name: "OFFLINE" }); + } }); }); diff --git a/src/types/Result.ts b/src/types/Result.ts new file mode 100644 index 0000000..0e09667 --- /dev/null +++ b/src/types/Result.ts @@ -0,0 +1,27 @@ +export type Success = { + error?: undefined | null; + data: D; +}; + +export type Failure = { + data?: undefined | null; + error: E; +}; + +/** + * Result + */ +export type Result = Failure | Success; + +export async function tryCatch(func: Promise): Promise> { + try { + const res = await func; + return { + data: res, + }; + } catch (e: unknown) { + return { + error: e as Error, + }; + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json index 4deb49e..398b008 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,6 +1,8 @@ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", "include": [ + "src/config/**/*", + "src/types/**/*", "electron.vite.config.*", "src/main/**/*", "src/preload/**/*",