diff --git a/BACKEND/app/services/station_connection.ts b/BACKEND/app/services/station_connection.ts new file mode 100644 index 0000000..5c9da26 --- /dev/null +++ b/BACKEND/app/services/station_connection.ts @@ -0,0 +1,108 @@ +import net from 'node:net' +import { appendLog, cleanData } from '../ultils/helper.js' + +interface LineConfig { + id: number + port: number + ip: string + name: string + output: string + status: string +} + +export default class StationConnection { + public client: net.Socket + public config: LineConfig + + constructor(config: LineConfig) { + this.config = config + this.client = new net.Socket() + } + + connect(timeoutMs = 5000) { + return new Promise((resolve, reject) => { + const { ip, port, name } = this.config + let resolvedOrRejected = false + // Set timeout + this.client.setTimeout(timeoutMs) + console.log(`🔌 Connecting to station ${name} (${ip}:${port})...`) + this.client.connect(port, ip, () => { + if (resolvedOrRejected) return + resolvedOrRejected = true + + console.log(`[${Date.now()}] ✅ Connected to line ${name} (${ip}:${port})`) + setTimeout(() => { + this.config.status = 'connected' + resolve() + }, 1000) + }) + + this.client.on('data', (data) => { + let message = data.toString() + if (message.includes('--More--')) this.writeCommand(' ') + this.config.output += cleanData(message) + this.config.output = this.config.output.slice(-5000) + // appendLog(cleanData(message), this.config.id, this.config.name, this.config.ip, 0) + }) + + this.client.on('error', (err) => { + if (resolvedOrRejected) return + resolvedOrRejected = true + console.error(`❌ Error station ${name}:`, err.message) + this.config.output += '\r\n' + err.message + '\r\n' + resolve() + }) + + this.client.on('close', async () => { + console.log(`[${Date.now()}] 🔌 station ${name} disconnected`) + this.config.status = 'disconnected' + }) + + this.client.on('timeout', () => { + if (resolvedOrRejected) return + resolvedOrRejected = true + const message = '\r\nConnection timeout!!\r\n' + this.config.output += message + console.log(`⏳ Connection timeout station ${name}`) + this.client.destroy() + resolve() + // reject(new Error('Connection timeout')) + }) + }) + } + + private sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + async writeCommand(cmd: string | Buffer) { + if (this.client.destroyed) { + console.log(`⚠️ Cannot send, station ${this.config.name} is closed`) + return + } + this.client.write(cmd) + } + + async disconnect() { + try { + console.log('[DISCONNECT] station', this.config.name) + this.client.destroy() + this.config.status = 'disconnected' + console.log(`🔻 Closed connection to station ${this.config.name}`) + } catch (e) { + console.error('Error closing line:', e) + } + } + + public async reconnect(): Promise { + try { + this.disconnect() + this.client = new net.Socket() + await this.sleep(1000) + await this.connect() + return true + } catch (err: any) { + return false + } + } +} diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 0cbd239..5f36df3 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -15,6 +15,7 @@ import { appendLog, cleanData, sendMessageToMail, sleep } from '../app/ultils/he import SwitchController from '#services/switch_connection' import redis from '@adonisjs/redis/services/main' import axios from 'axios' +import StationConnection from '#services/station_connection' interface HandleOptions { command?: string @@ -37,6 +38,11 @@ interface HistoryItem { type LineAction = (line: LineConnection, options?: HandleOptions) => Promise | void +type StationAction = ( + line: StationConnection, + options?: HandleOptions +) => Promise | void + export default class SocketIoProvider { private static _io: CustomServer constructor(protected app: ApplicationService) {} @@ -78,6 +84,7 @@ export default class SocketIoProvider { export class WebSocketIo { intervalMap: { [key: string]: NodeJS.Timeout } = {} + stationMap: Map = new Map() // key = stationId lineMap: Map = new Map() // key = lineId userConnecting: Map = new Map() apcsControl: Map = new Map() @@ -629,7 +636,7 @@ export class WebSocketIo { private setTimeoutConnect = ( lineId: number, - lineConn: LineConnection | SwitchController, + lineConn: LineConnection | SwitchController | StationConnection, timeout = 28800000 // 8h = 8*60*60*1000 ) => { if (this.intervalMap[`${lineId}`]) { @@ -798,43 +805,10 @@ export class WebSocketIo { return } - // Kết nối tới station qua Telnet / Socket - const client = new net.Socket() - return new Promise((resolve, reject) => { - client.setTimeout(5000) - client.connect(station.port, station.ip, async () => { - console.log( - `Connected to station ${station.name} (${station.ip}) to clear line ${clearLine}` - ) - // Gửi lệnh clear line - client.write(`clear line ${clearLine}\r\n`) - await sleep(500) - client.write(`\r\n\r\n`) - }) - - client.on('data', (data) => { - const output = data.toString() - if (output.includes('Clear completed') || output.includes('OK')) { - console.log(`Line ${clearLine} cleared successfully.`) - client.destroy() - resolve() - } - }) - - client.on('error', (err) => { - console.error(`Error clearing line ${clearLine}:`, err) - resolve() - }) - - client.on('close', () => { - console.log(`Station connection closed (line ${clearLine})`) - resolve() - }) - client.on('timeout', () => { - console.log(`Station connection timeout (line ${clearLine})`) - client.destroy() - resolve() - }) + await this.handleStationOperation(stationId, async (stationCon) => { + stationCon.writeCommand(`\r\nclear line ${clearLine}\r\n`) + await sleep(500) + stationCon.writeCommand(`\r\n\r\n`) }) } @@ -888,57 +862,32 @@ export class WebSocketIo { return } - // Kết nối tới station qua Telnet / Socket - const client = new net.Socket() - let buffer = '' - - return new Promise((resolve, reject) => { - client.setTimeout(8000) - client.connect(station.port, station.ip, async () => { - console.log(`Connected to station ${station.name} (${station.ip})`) - client.write(`\r\n`) - await sleep(500) - client.write(`show line\r\n`) - await sleep(2000) - client.destroy() - resolve() - }) - - client.on('data', (data) => { - const text = data.toString() - buffer += cleanData(text) - }) - - client.on('error', (err) => { - console.error(`Error clearing line ${lineClear}:`, err) - resolve() - }) - - client.on('close', () => { - console.log(`Station connection closed (line ${lineClear})`) - const result = this.detectBaudFromShowLine(buffer) - const found = result.find((x) => x.clearLine === lineClear) - if (found) { - const line = this.lineMap.get(lineId) - if (line) { - line.config.baud = found.baud - this.lineMap.set(lineId, line) - io.emit('update_baud', { - stationId, - lineId, - data: found.baud, - }) - } - } - - resolve() - }) - client.on('timeout', () => { - console.log(`Station connection timeout (line ${lineClear})`) - client.destroy() - resolve() - }) + await this.handleStationOperation(stationId, async (stationCon) => { + stationCon.writeCommand(`\r\n`) + await sleep(500) + stationCon.writeCommand(`show line\r\n`) + await sleep(2000) }) + + const stationConn = this.stationMap.get(stationId) + + if (stationConn) { + const buffer = stationConn?.config?.output || '' + const result = this.detectBaudFromShowLine(buffer) + const found = result.find((x) => x.clearLine === lineClear) + if (found) { + const line = this.lineMap.get(lineId) + if (line) { + line.config.baud = found.baud + this.lineMap.set(lineId, line) + io.emit('update_baud', { + stationId, + lineId, + data: found.baud, + }) + } + } + } } private async setBaudByClearLine( @@ -1112,4 +1061,58 @@ export class WebSocketIo { html += `\n\n` return html } + + private async connectStation(station: Station) { + try { + const stationConn = new StationConnection({ + id: station.id, + port: station.port, + ip: station.ip, + name: station.name, + output: '', + status: '', + }) + this.stationMap.set(station.id, stationConn) + await stationConn.connect() + stationConn.writeCommand('\r\n') + this.setTimeoutConnect(station.id, stationConn) + } catch (error) { + console.log(error) + } + } + + /** + * Hàm xử lý chung cho mọi action (write command, runScript, v.v.) + */ + async handleStationOperation( + stationId: number, + action: StationAction, + options: HandleOptions = {} + ): Promise { + try { + const station = this.stationMap.get(stationId) + // console.log(line?.config) + if (station && station.config.status === 'connected') { + this.setTimeoutConnect(stationId, station) + // await sleep(500) + await action(station, options) + } else { + const stationData = await Station.findBy('id', stationId) + + if (stationData) { + await this.connectStation(stationData) + const stationReconnect = this.stationMap.get(stationId) + if (stationReconnect) { + this.setTimeoutConnect(stationId, stationReconnect) + await sleep(100) + await action(stationReconnect, options) + } + } else { + console.log('Station not found') + } + } + } catch (err: any) { + console.log('Station connect error:', err.message) + } + } } diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index fb7052b..98d68b4 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -523,7 +523,13 @@ function App() { }} > = 9 lines: chia làm 2 cột, mỗi cột chứa 1/2 số line, // mỗi cột hiển thị 2 item trên một "hàng" như ví dụ yêu cầu (() => { - const total = station.lines.length; - const half = Math.ceil(total / 2); + // const total = station.lines.length; + const half = 8; const leftLines = station.lines.slice(0, half); const rightLines = station.lines.slice(half); diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index 774ff85..b5b9bea 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -501,6 +501,7 @@ const BottomToolBar = ({ translate: "-19px 0", backgroundColor: "#e3e0e0", width: "55px", + zIndex: 10, }} variant="light" onClick={() => { @@ -631,7 +632,8 @@ const BottomToolBar = ({ selectedLines.forEach((line) => { socket?.emit("close_cli", { lineId: line?.id, - stationId: line.stationId || line.station_id, + stationId: + line.stationId || line.station_id, }); }); setSelectedLines([]); @@ -646,7 +648,8 @@ const BottomToolBar = ({ - Selected: {selectedLines.length} / {station.lines.length} + Selected: {selectedLines.length} /{" "} + {station.lines.length} - + = ({ style={{ paddingLeft: 0, paddingRight: 0, - width: "65px", + width: "55px", position: "relative", cursor: "pointer", textAlign: "center", @@ -668,7 +668,7 @@ export const DrawerAPCControl: React.FC = ({ style={{ paddingLeft: 0, paddingRight: 0, - width: "65px", + width: "55px", position: "relative", cursor: "pointer", textAlign: "center", diff --git a/FRONTEND/src/components/FormAddEdit.tsx b/FRONTEND/src/components/FormAddEdit.tsx index 4239133..cf9e667 100644 --- a/FRONTEND/src/components/FormAddEdit.tsx +++ b/FRONTEND/src/components/FormAddEdit.tsx @@ -269,6 +269,8 @@ const StationSetting = ({ if (response.data.status) { if (response.data.data) { const station = response.data.data[0]; + const lines = station?.lines; + const dataStationLines = dataStation?.lines; setStations((pre) => isEdit ? pre.map((el) => @@ -276,13 +278,13 @@ const StationSetting = ({ ? { ...el, ...station, - lines: dataStation?.lines?.map((el) => - lineUpdate?.find((value) => value?.id === el.id) + lines: lines?.map((el: TLine) => + dataStationLines?.find((value) => value?.id === el.id) ? { - ...el, - ...lineUpdate?.find( - (value) => value?.id === el.id + ...dataStationLines?.find( + (value: TLine) => value?.id === el.id ), + ...el, } : el ),