231 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
import { Server as SocketIOServer } from 'socket.io'
 | 
						|
import http from 'node:http'
 | 
						|
import LineConnection from '../app/services/line_connection.js'
 | 
						|
import { ApplicationService } from '@adonisjs/core/types'
 | 
						|
import env from '#start/env'
 | 
						|
import { CustomServer, CustomSocket } from '../app/ultils/types.js'
 | 
						|
import Line from '#models/line'
 | 
						|
import Station from '#models/station'
 | 
						|
 | 
						|
export default class SocketIoProvider {
 | 
						|
  private static _io: CustomServer
 | 
						|
  constructor(protected app: ApplicationService) {}
 | 
						|
 | 
						|
  /**
 | 
						|
   * Register bindings to the container
 | 
						|
   */
 | 
						|
  register() {}
 | 
						|
 | 
						|
  /**
 | 
						|
   * The container bindings have booted
 | 
						|
   */
 | 
						|
  async boot() {}
 | 
						|
 | 
						|
  /**
 | 
						|
   * The application has been booted
 | 
						|
   */
 | 
						|
  async start() {}
 | 
						|
 | 
						|
  /**
 | 
						|
   * The process has been started
 | 
						|
   */
 | 
						|
  async ready() {
 | 
						|
    if (process.argv[1].includes('server.js')) {
 | 
						|
      const webSocket = new WebSocketIo(this.app)
 | 
						|
      SocketIoProvider._io = await webSocket.boot()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Preparing to shutdown the app
 | 
						|
   */
 | 
						|
  async shutdown() {}
 | 
						|
 | 
						|
  public static get io() {
 | 
						|
    return this._io
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export class WebSocketIo {
 | 
						|
  intervalMap: { [key: string]: NodeJS.Timeout } = {}
 | 
						|
  stationMap: Map<number, Station> = new Map()
 | 
						|
  lineMap: Map<number, LineConnection> = new Map() // key = lineId
 | 
						|
  lineConnecting: number[] = [] // key = lineId
 | 
						|
 | 
						|
  constructor(protected app: ApplicationService) {}
 | 
						|
 | 
						|
  async boot() {
 | 
						|
    const SOCKET_IO_PORT = env.get('SOCKET_PORT') || 8989
 | 
						|
    const FRONTEND_URL = env.get('FRONTEND_URL') || 'http://localhost:5173'
 | 
						|
 | 
						|
    const socketServer = http.createServer()
 | 
						|
    const io = new SocketIOServer(socketServer, {
 | 
						|
      pingInterval: 25000, // 25s server gửi ping
 | 
						|
      pingTimeout: 20000, // chờ 20s không có pong thì disconnect
 | 
						|
      cors: {
 | 
						|
        origin: [FRONTEND_URL],
 | 
						|
        methods: ['GET', 'POST'],
 | 
						|
        credentials: true,
 | 
						|
      },
 | 
						|
    })
 | 
						|
 | 
						|
    io.on('connection', (socket: CustomSocket) => {
 | 
						|
      console.log('Socket connected:', socket.id)
 | 
						|
      socket.connectionTime = new Date()
 | 
						|
 | 
						|
      setTimeout(() => {
 | 
						|
        io.to(socket.id).emit(
 | 
						|
          'init',
 | 
						|
          Array.from(this.lineMap.values()).map((el) => el.config)
 | 
						|
        )
 | 
						|
      }, 200)
 | 
						|
 | 
						|
      socket.on('disconnect', () => {
 | 
						|
        console.log(`FE disconnected: ${socket.id}`)
 | 
						|
      })
 | 
						|
 | 
						|
      // FE gửi yêu cầu connect lines
 | 
						|
      socket.on('connect_lines', async (data) => {
 | 
						|
        const { stationData, linesData } = data
 | 
						|
        await this.connectLine(io, linesData, stationData)
 | 
						|
      })
 | 
						|
 | 
						|
      socket.on('write_command_line_from_web', async (data) => {
 | 
						|
        const { lineIds, stationId, command } = data
 | 
						|
        for (const lineId of lineIds) {
 | 
						|
          const line = this.lineMap.get(lineId)
 | 
						|
          if (line) {
 | 
						|
            this.lineConnecting.filter((el) => el !== lineId)
 | 
						|
            this.setTimeoutConnect(lineId, line)
 | 
						|
            line.writeCommand(command)
 | 
						|
          } else {
 | 
						|
            if (this.lineConnecting.includes(lineId)) continue
 | 
						|
            const linesData = await Line.findBy('id', lineId)
 | 
						|
            const stationData = await Station.findBy('id', stationId)
 | 
						|
            if (linesData && stationData) {
 | 
						|
              this.lineConnecting.push(lineId)
 | 
						|
              await this.connectLine(io, [linesData], stationData)
 | 
						|
              const lineReconnect = this.lineMap.get(lineId)
 | 
						|
              if (lineReconnect) {
 | 
						|
                this.lineConnecting.filter((el) => el !== lineId)
 | 
						|
                this.setTimeoutConnect(lineId, lineReconnect)
 | 
						|
                lineReconnect.writeCommand(command)
 | 
						|
              }
 | 
						|
            } else {
 | 
						|
              io.emit('line_disconnected', {
 | 
						|
                stationId,
 | 
						|
                lineId,
 | 
						|
                status: 'disconnected',
 | 
						|
              })
 | 
						|
              io.emit('line_error', { lineId, error: 'Line not connected\r\n' })
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
      })
 | 
						|
 | 
						|
      socket.on('run_scenario', async (data) => {
 | 
						|
        const lineId = data.id
 | 
						|
        const scenario = data.scenario
 | 
						|
        const line = this.lineMap.get(lineId)
 | 
						|
        if (line) {
 | 
						|
          this.setTimeoutConnect(
 | 
						|
            lineId,
 | 
						|
            line,
 | 
						|
            scenario?.timeout ? Number(scenario?.timeout) + 120000 : 300000
 | 
						|
          )
 | 
						|
          line.runScript(scenario)
 | 
						|
        } else {
 | 
						|
          const linesData = await Line.findBy('id', lineId)
 | 
						|
          const stationData = await Station.findBy('id', data.station_id)
 | 
						|
          if (linesData && stationData) {
 | 
						|
            await this.connectLine(io, [linesData], stationData)
 | 
						|
            const lineReconnect = this.lineMap.get(lineId)
 | 
						|
            if (lineReconnect) {
 | 
						|
              this.setTimeoutConnect(lineId, lineReconnect, 300000)
 | 
						|
              lineReconnect.runScript(scenario)
 | 
						|
            }
 | 
						|
          } else {
 | 
						|
            io.emit('line_disconnected', {
 | 
						|
              stationId: data.stationId,
 | 
						|
              lineId,
 | 
						|
              status: 'disconnected',
 | 
						|
            })
 | 
						|
            io.emit('line_error', { lineId, error: 'Line not connected\r\n' })
 | 
						|
          }
 | 
						|
        }
 | 
						|
      })
 | 
						|
 | 
						|
      // FE yêu cầu ngắt kết nối 1 station
 | 
						|
      socket.on('disconnect_station', (stationId) => {
 | 
						|
        this.disconnectStation(stationId)
 | 
						|
      })
 | 
						|
    })
 | 
						|
 | 
						|
    socketServer.listen(SOCKET_IO_PORT, () => {
 | 
						|
      console.log(`Socket server is running on port ${SOCKET_IO_PORT}`)
 | 
						|
    })
 | 
						|
 | 
						|
    return io
 | 
						|
  }
 | 
						|
 | 
						|
  private async connectLine(socket: any, lines: Line[], station: Station) {
 | 
						|
    try {
 | 
						|
      this.stationMap.set(station.id, station)
 | 
						|
      for (const line of lines) {
 | 
						|
        const lineConn = new LineConnection(
 | 
						|
          {
 | 
						|
            id: line.id,
 | 
						|
            port: line.port,
 | 
						|
            ip: station.ip,
 | 
						|
            lineNumber: line.lineNumber,
 | 
						|
            stationId: station.id,
 | 
						|
            apcName: line.apcName,
 | 
						|
            output: '',
 | 
						|
            status: '',
 | 
						|
          },
 | 
						|
          socket
 | 
						|
        )
 | 
						|
        await lineConn.connect()
 | 
						|
        lineConn.writeCommand('\r\n\r\n')
 | 
						|
        this.lineMap.set(line.id, lineConn)
 | 
						|
        this.setTimeoutConnect(line.id, lineConn)
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      console.log(error)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  private disconnectStation(stationId: number) {
 | 
						|
    const station = this.stationMap.get(stationId)
 | 
						|
    if (!station) return
 | 
						|
 | 
						|
    for (const line of station.lines) {
 | 
						|
      const conn = this.lineMap.get(line.id)
 | 
						|
      if (conn) {
 | 
						|
        conn.disconnect()
 | 
						|
        this.lineMap.delete(line.id)
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    this.stationMap.delete(stationId)
 | 
						|
    console.log(`🔻 Station ${station.name} disconnected`)
 | 
						|
  }
 | 
						|
 | 
						|
  private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 120000) => {
 | 
						|
    if (this.intervalMap[`${lineId}`]) {
 | 
						|
      clearInterval(this.intervalMap[`${lineId}`])
 | 
						|
      delete this.intervalMap[`${lineId}`]
 | 
						|
    }
 | 
						|
    const interval = setInterval(() => {
 | 
						|
      lineConn.disconnect()
 | 
						|
      this.lineMap.delete(lineId)
 | 
						|
      if (this.intervalMap[`${lineId}`]) {
 | 
						|
        clearInterval(this.intervalMap[`${lineId}`])
 | 
						|
        delete this.intervalMap[`${lineId}`]
 | 
						|
      }
 | 
						|
    }, timeout)
 | 
						|
 | 
						|
    this.intervalMap[`${lineId}`] = interval
 | 
						|
  }
 | 
						|
}
 |