diff --git a/BACKEND/app/services/apc_connection.ts b/BACKEND/app/services/apc_connection.ts index ab59bc1..7ffc9b8 100644 --- a/BACKEND/app/services/apc_connection.ts +++ b/BACKEND/app/services/apc_connection.ts @@ -5,7 +5,7 @@ interface APCOptions { port?: number username: string password: string - onData?: (data: string) => void + onData?: (data: string, status: string) => void number?: number keep_connect?: boolean } @@ -26,7 +26,7 @@ class APCController { private buffer: string private output: string private promptCallbacks: PromptCallback[] - private onData: (data: string) => void + private onData: (data: string, status: string) => void private retryConnect: number constructor({ host, port = 23, username, password, onData, number }: APCOptions) { @@ -80,7 +80,7 @@ class APCController { this.buffer += data this.buffer = this.buffer.slice(-1000) - this.onData(this.buffer) + this.onData(this.buffer, this.status) if (this.promptCallbacks.length > 0) { const { prompt, callback } = this.promptCallbacks[0] @@ -95,14 +95,14 @@ class APCController { private _handleClose(): void { this.status = 'DISCONNECTED' this.output += '\r\n\r\n[DISCONNECTED] Socket closed' - this.onData(this.output) + this.onData(this.output, this.status) this._cleanup() } private async _handleTimeout(): Promise { this.status = 'TIMEOUT' this.output += '\r\n\r\n[TIMEOUT] Connection timed out' - this.onData(this.output) + this.onData(this.output, this.status) if (this.retryConnect <= 5) { await this.sleep(5000) @@ -114,7 +114,7 @@ class APCController { private _handleError(err: NodeJS.ErrnoException): void { this.output += `\r\n\r\n[ERROR] ${err.message}` - this.onData(this.output) + this.onData(this.output, this.status) if (err.code === 'ECONNRESET') { setTimeout(() => { console.log('[ECONNRESET] Trying reconnect apc:', this.apc_ip) diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index bfe41f9..8175eae 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -235,17 +235,14 @@ export default class LineConnection { }, script.timeout || 300000) const runStep = async (index: number) => { - console.log('Running step', index, Date.now()) if (index >= steps.length) { - clearTimeout(timeoutTimer) if (this.waitingScenario) { + this.waitingScenario = false setTimeout(() => { - this.waitingScenario = false runStep(index) }, 5000) return - } - console.log('End step', Date.now()) + } else clearTimeout(timeoutTimer) this.isRunningScript = false this.outputBuffer = '' appendLog( @@ -397,12 +394,14 @@ export default class LineConnection { username, password, number: this.config.lineNumber, - onData: (data: string) => { + onData: (data: string, status: string) => { this.config.output += data - this.socketIO.emit('line_output', { + this.socketIO.emit('apc_output', { stationId: this.config.stationId, lineId: this.config.id, - data: data, + apcNumber: apcName === 'apc_1' ? 1 : 2, + data, + status, }) appendLog( cleanData(data), diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 2241e6e..fb89aa5 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -7,6 +7,7 @@ import env from '#start/env' import { CustomServer, CustomSocket } from '../app/ultils/types.js' import Line from '#models/line' import Station from '#models/station' +import APCController from '#services/apc_connection' interface HandleOptions { command?: string @@ -62,6 +63,7 @@ export class WebSocketIo { lineMap: Map = new Map() // key = lineId lineConnecting: number[] = [] // key = lineId userConnecting: Map = new Map() + apcsControl: Map = new Map() constructor(protected app: ApplicationService) {} @@ -123,7 +125,7 @@ export class WebSocketIo { stationId, lineIds, async (line) => line.writeCommand(command), - { command, timeout: 180000 } + { command, timeout: 120000 } ) }) @@ -137,7 +139,7 @@ export class WebSocketIo { async (line) => line.runScript(scenario), { scenario, - timeout: scenario?.timeout ? Number(scenario.timeout) + 180000 : 300000, + timeout: scenario?.timeout ? Number(scenario.timeout) + 120000 : 300000, } ) }) @@ -201,7 +203,6 @@ export class WebSocketIo { // Get file stats const stats = fs.statSync(filePath) const fileSizeInBytes = stats.size - console.log('File size (bytes):', fileSizeInBytes) if (fileSizeInBytes / 1024 / 1024 > 0.5) { // File is larger than 0.5 MB const fileId = Date.now() // Mã định danh file @@ -242,9 +243,17 @@ export class WebSocketIo { stationId, lineIds, async (line) => line.apcControl(action), - { actionApc: action, timeout: 180000 } + { actionApc: action, timeout: 120000 } ) }) + + socket.on('connect_apc', async (data) => { + const { apcIp, station, apcName } = data + if (this.apcsControl.has(apcIp)) { + return + } + await this.connectApc(io, apcName, station) + }) }) socketServer.listen(SOCKET_IO_PORT, () => { @@ -286,7 +295,7 @@ export class WebSocketIo { } } - private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 180000) => { + private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 120000) => { if (this.intervalMap[`${lineId}`]) { clearInterval(this.intervalMap[`${lineId}`]) delete this.intervalMap[`${lineId}`] @@ -351,4 +360,38 @@ export class WebSocketIo { } } } + + private async connectApc(socket: any, apcName: string, station: Station) { + try { + const ip = (station as any)[`${apcName}_ip`] as string + const port = (station as any)[`${apcName}_port`] as number + const username = (station as any)[`${apcName}_username`] as string + const password = (station as any)[`${apcName}_password`] as string + + if (!ip || !port || !username || !password) + throw new Error(`Missing APC configuration for ${apcName}`) + + // Tạo APC Controller instance + const apc = new APCController({ + host: ip, + port, + username, + password, + onData: (data: string, status: string) => { + socket.emit('apc_output', { + stationId: station.id, + apcNumber: apcName === 'apc_1' ? 1 : 2, + data, + status, + }) + }, + }) + // Connect và login + await apc.connect() + await apc.login() + this.apcsControl.set(ip, apc) + } catch (error) { + console.log(error) + } + } } diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index 6b86be5..2014b44 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -14,8 +14,6 @@ import { Grid, ScrollArea, LoadingOverlay, - Button, - Box, } from "@mantine/core"; import type { IDataTakeOver, @@ -338,7 +336,7 @@ function App() { }; }); }, - [activeTab] + [] ); // const getLine = (lineId: number, stationId: number) => { diff --git a/FRONTEND/src/components/DragTabs.tsx b/FRONTEND/src/components/DragTabs.tsx index 81f3907..5c9c286 100644 --- a/FRONTEND/src/components/DragTabs.tsx +++ b/FRONTEND/src/components/DragTabs.tsx @@ -270,7 +270,25 @@ export default function DraggableTabs({ fz={"sm"} variant="filled" onClick={() => { + const station = tabs.find( + (el) => el.id.toString() === active + ); + if (!station) return; setOpenedAPC(true); + if (station?.apc_1_ip && station?.apc_1_port) { + socket?.emit("connect_apc", { + station: station, + apcIp: station?.apc_1_ip, + apcName: "apc_1", + }); + } + if (station?.apc_2_ip && station?.apc_2_port) { + socket?.emit("connect_apc", { + station: station, + apcIp: station?.apc_2_ip, + apcName: "apc_2", + }); + } }} > APC diff --git a/FRONTEND/src/components/DrawerControl.tsx b/FRONTEND/src/components/DrawerControl.tsx index 659f1d7..846b3e1 100644 --- a/FRONTEND/src/components/DrawerControl.tsx +++ b/FRONTEND/src/components/DrawerControl.tsx @@ -1,14 +1,15 @@ -import { Box, Button, Card, Checkbox, Drawer, Grid, Text } from "@mantine/core"; +import { Box, Button, Card, Drawer, Grid, Text } from "@mantine/core"; import { IconRepeat, IconSection } from "@tabler/icons-react"; import { useEffect, useState } from "react"; import classes from "./Component.module.css"; import type { APCProps, SwitchPortsProps, TStation } from "../untils/types"; import { useDebounce } from "../untils/helper"; import { SOCKET_EVENTS } from "../untils/constanst"; +import type { Socket } from "socket.io-client"; interface DrawerProps { stationAPI: TStation; - socket: any; + socket: Socket | null; open: boolean; onClose: () => void; openedSwitch?: () => void; @@ -37,7 +38,7 @@ export const DrawerAPCControl: React.FC = ({ const findLineByOutlet = (outlet: TSelectedOutlet) => { return stationAPI.lines.find( (line) => - line.outlet === outlet.number && line.apc_name === `apc_${outlet.apc}` + line.outlet === outlet.number && line.apcName === `apc_${outlet.apc}` ); }; @@ -54,7 +55,7 @@ export const DrawerAPCControl: React.FC = ({ if (!line) return el; return { ...el, - name: "Line " + line.line_number || el.name, + name: "Line " + line.lineNumber || el.name, }; }) ); @@ -168,15 +169,25 @@ export const DrawerAPCControl: React.FC = ({ }, [stationAPI, isConnected]); useEffect(() => { - socket?.on(SOCKET_EVENTS.APP_DATA.RECEIVED, (data: TStation[]) => { - const station = data?.find((el) => stationAPI.id === el.id); - if (!station) return; - setDataStation(station); - const apcs = - station?.apcs?.sort( - (a, b) => (a?.apc_number || 0) - (b?.apc_number || 0) - ) || []; + socket?.on("apc_output", (data) => { + if (data.stationId !== stationAPI.id) return; + let apcs: APCProps[] = []; + setDataStation((prev) => { + const apc1 = + data.apcNumber === 1 + ? { ...prev.apc1, output: data.data, status: data.status } + : prev.apc1; + const apc2 = + data.apcNumber === 2 + ? { ...prev.apc2, output: data.data, status: data.status } + : prev.apc2; + apcs = [apc1, apc2]; + return prev.id === data.stationId + ? { ...prev, apc1: apc1, apc2: apc2 } + : prev; + }); const outlets: TSelectedOutlet[] = []; + console.log("apcs", apcs); apcs.forEach(async (apc, i) => { const result: TSelectedOutlet[] = []; const lines = apc?.output?.split("\n") || []; @@ -185,29 +196,6 @@ export const DrawerAPCControl: React.FC = ({ }); setListOutlet(outlets); }); - - socket?.on( - SOCKET_EVENTS.DATA_APC_RECEIVED.DATA_APC_RECEIVED_TO_WEB, - (data: TStation[]) => { - const outlets: TSelectedOutlet[] = []; - data - ?.filter((el) => stationAPI.id === el.id) - ?.forEach((station) => { - station?.apcs - ?.sort((a, b) => (a?.apc_number || 0) - (b?.apc_number || 0)) - .forEach(async (apc, i) => { - const result: TSelectedOutlet[] = []; - const lines = apc?.output?.split("\n") || []; - await detectOutlet(apc, lines, result, i); - outlets.push(...result); - }); - }); - setListOutlet(outlets); - setDataStation( - data?.find((el) => stationAPI.id === el.id) || stationAPI - ); - } - ); }, [socket]); const toggleSelect = (outlet: TSelectedOutlet, number: number) => { @@ -270,663 +258,624 @@ export const DrawerAPCControl: React.FC = ({ position="bottom" > - {dataStation?.apcs && ( - -
- -
- - APC 1 - - {dataStation?.apcs[0]?.status && - RenderAPCStatus(dataStation?.apcs[0])} - {dataStation?.apcs[0]?.status === "DISCONNECTED" || - dataStation?.apcs[0]?.status === "TIMEOUT" ? ( - + ) : ( +
+ )} +
+
+ + + {listOutlet + .filter((el) => el.apc === 1) + .map((outlet, i) => ( + + el.name === outlet.name && el.apc === outlet.apc + )?.name + ? "1px solid #0018ff" + : "", + }} onClick={() => { - socket?.emit( - SOCKET_EVENTS.SEND_COMMAND_TO_APC - .SEND_COMMAND_TO_APC_FROM_WEB, - { - station_id: dataStation.id, - apc_number: 1, - command: "reconnect", - isAction: true, - outlet_number: 0, - } - ); - setListOutletSelected([]); // Clear selected outlets - setIsSubmit(true); - setTimeout(() => { - setIsSubmit(false); - }, 5000); + toggleSelect(outlet, i + 1); }} > - - - ) : ( -
- )} - - - - - {listOutlet - .filter((el) => el.apc === 1) - .map((outlet, i) => ( - - el.name === outlet.name && el.apc === outlet.apc - )?.name - ? "1px solid #0018ff" - : "", - }} - onClick={() => { - toggleSelect(outlet, i + 1); + color: outlet.status === "ON" ? "#40c057" : "#f03e3e", }} > - - {findLineByOutlet(outlet) - ? "Line " + findLineByOutlet(outlet)?.line_number - : outlet.name} - - - ))} - -
+ + ))} + +
+ + - - - -
- -
-
- )} - - {dataStation?.apcs && ( - -
- -
- - APC 2 - - {dataStation?.apcs[1]?.status && - RenderAPCStatus(dataStation?.apcs[1])} - {dataStation?.apcs[1]?.status === "DISCONNECTED" || - dataStation?.apcs[1]?.status === "TIMEOUT" ? ( - - ) : ( -
- )} -
-
- - - {listOutlet - .filter((el) => el.apc === 2) - .map((outlet, i) => ( - - el.name === outlet.name && el.apc === outlet.apc - )?.name - ? "1px solid #0018ff" - : "", - }} - onClick={() => { - toggleSelect(outlet, i + 1); - }} - > - - {findLineByOutlet(outlet) - ? "Line " + findLineByOutlet(outlet)?.line_number - : outlet.name} - - - ))} - -
{ + setIsSubmit(false); + }, 5000); }} > - - + + +
+
+
+
+ +
+ +
+ + APC 2 + + {dataStation?.apc2?.status && + RenderAPCStatus(dataStation?.apc2)} + {dataStation?.apc2?.status === "DISCONNECTED" || + dataStation?.apc2?.status === "TIMEOUT" ? ( + -
+
+ + + {listOutlet + .filter((el) => el.apc === 2) + .map((outlet, i) => ( + + el.name === outlet.name && el.apc === outlet.apc + )?.name + ? "1px solid #0018ff" + : "", + }} + onClick={() => { + toggleSelect(outlet, i + 1); + }} + > + + {findLineByOutlet(outlet) + ? "Line " + findLineByOutlet(outlet)?.lineNumber + : outlet.name} + + + ))} + +
+ + - + + -
-
-
-
- )} + : "Turn Off Selected" + } + // mt={'xs'} + miw={"80px"} + size="xs" + fz={"sm"} + variant="filled" + color="red" + onClick={() => { + if ( + listOutletSelected.filter((el) => el.apc === 2).length === + 0 || + listOutletSelected.filter((el) => el.apc === 2).length === + listOutlet.filter((el) => el.apc === 2).length + ) { + socket?.emit(SOCKET_EVENTS.APC_CONTROL.FROM_WEB_ALL_APC, { + apc: "apc_2", + station: stationAPI, + action: "2", + station_id: Number(stationAPI.id), + }); + } else { + listOutletSelected.forEach((el) => { + const line = findLineByOutlet(el); + if (!line) return; + socket?.emit(SOCKET_EVENTS.APC_CONTROL.FROM_WEB, { + line: line, + action: "2", + station_id: Number(stationAPI.id), + }); + }); + } + setListOutletSelected([]); + setIsSubmit(true); + setTimeout(() => { + setIsSubmit(false); + }, 5000); + }} + > + {listOutletSelected.filter((el) => el.apc === 2).length === + listOutlet.filter((el) => el.apc === 2).length || + listOutletSelected.filter((el) => el.apc === 2).length === 0 + ? "Turn Off All" + : "Turn Off Selected"} + + + + +