Update
This commit is contained in:
parent
01485bf1d9
commit
3b55644bc1
|
|
@ -0,0 +1,267 @@
|
|||
import net, { Socket } from 'node:net'
|
||||
|
||||
interface APCOptions {
|
||||
host: string
|
||||
port?: number
|
||||
username: string
|
||||
password: string
|
||||
onData?: (data: string) => void
|
||||
number?: number
|
||||
keep_connect?: boolean
|
||||
}
|
||||
|
||||
interface PromptCallback {
|
||||
prompt: string
|
||||
callback: (data: string) => void
|
||||
}
|
||||
|
||||
class APCController {
|
||||
private apc_number?: number
|
||||
private apc_ip: string
|
||||
private apc_port: number
|
||||
private apc_username: string
|
||||
private apc_password: string
|
||||
private status: 'CONNECTED' | 'DISCONNECTED' | 'TIMEOUT'
|
||||
private socket: Socket
|
||||
private buffer: string
|
||||
private output: string
|
||||
private promptCallbacks: PromptCallback[]
|
||||
private onData: (data: string) => void
|
||||
private retryConnect: number
|
||||
|
||||
constructor({ host, port = 23, username, password, onData, number }: APCOptions) {
|
||||
this.apc_number = number
|
||||
this.apc_ip = host
|
||||
this.apc_port = port
|
||||
this.apc_username = username
|
||||
this.apc_password = password
|
||||
this.status = 'DISCONNECTED'
|
||||
this.socket = new net.Socket()
|
||||
this.buffer = ''
|
||||
this.output = '... Starting ...\n'
|
||||
this.promptCallbacks = []
|
||||
this.onData = onData || (() => {})
|
||||
this.retryConnect = 0
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket.connect(this.apc_port, this.apc_ip, () => {
|
||||
this.status = 'CONNECTED'
|
||||
this.socket.setEncoding('utf8')
|
||||
this.socket.on('data', (data) => this._handleData(data.toString()))
|
||||
this.socket.on('close', () => this._handleClose())
|
||||
this.socket.on('timeout', () => this._handleTimeout())
|
||||
this.socket.on('error', (err) => this._handleError(err))
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
if (this.socket && !this.socket.destroyed) {
|
||||
this.socket.removeAllListeners()
|
||||
this.socket.destroy()
|
||||
this.socket.unref()
|
||||
}
|
||||
}
|
||||
|
||||
private _handleData(data: string): void {
|
||||
this.output += data
|
||||
this.output = this.output.slice(-10000)
|
||||
this.buffer += data
|
||||
this.buffer = this.buffer.slice(-1000)
|
||||
|
||||
this.onData(this.buffer)
|
||||
|
||||
if (this.promptCallbacks.length > 0) {
|
||||
const { prompt, callback } = this.promptCallbacks[0]
|
||||
if (this.buffer.includes(prompt)) {
|
||||
const cb = this.promptCallbacks.shift()
|
||||
if (cb) cb.callback(this.buffer)
|
||||
this.buffer = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClose(): void {
|
||||
this.status = 'DISCONNECTED'
|
||||
this.output += '\r\n\r\n[DISCONNECTED] Socket closed'
|
||||
this.onData(this.output)
|
||||
this._cleanup()
|
||||
}
|
||||
|
||||
private async _handleTimeout(): Promise<void> {
|
||||
this.status = 'TIMEOUT'
|
||||
this.output += '\r\n\r\n[TIMEOUT] Connection timed out'
|
||||
this.onData(this.output)
|
||||
|
||||
if (this.retryConnect <= 5) {
|
||||
await this.sleep(5000)
|
||||
console.log('Retry connect times', this.retryConnect)
|
||||
this.retryConnect += 1
|
||||
await this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private _handleError(err: NodeJS.ErrnoException): void {
|
||||
this.output += `\r\n\r\n[ERROR] ${err.message}`
|
||||
this.onData(this.output)
|
||||
if (err.code === 'ECONNRESET') {
|
||||
setTimeout(() => {
|
||||
console.log('[ECONNRESET] Trying reconnect apc:', this.apc_ip)
|
||||
this.reconnect()
|
||||
}, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
private _waitFor(prompt: string, timeout = 5000): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this._handleTimeout()
|
||||
reject(new Error(`Timeout waiting for: ${prompt}`))
|
||||
}, timeout)
|
||||
|
||||
this.promptCallbacks.push({
|
||||
prompt,
|
||||
callback: (data) => {
|
||||
clearTimeout(timer)
|
||||
resolve(data)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private _send(command: string): void {
|
||||
if (this.socket && !this.socket.destroyed && this.socket.readyState) {
|
||||
this.socket.write(this._convertSpecialKey(command) + '\r\n')
|
||||
}
|
||||
}
|
||||
|
||||
private _cleanup(): void {
|
||||
this.promptCallbacks = []
|
||||
|
||||
this.socket.removeAllListeners()
|
||||
this.socket.unref()
|
||||
this.socket.destroy()
|
||||
}
|
||||
|
||||
private _convertSpecialKey(key: string): string {
|
||||
switch (key) {
|
||||
case 'ENTER':
|
||||
return ''
|
||||
case 'ESC':
|
||||
return '\x1B'
|
||||
case 'CTRL-L':
|
||||
return '\x0C'
|
||||
case 'SPACE':
|
||||
return ' '
|
||||
case 'D':
|
||||
return '\x44'
|
||||
default:
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
public async login(): Promise<void> {
|
||||
await this.sleep(500)
|
||||
this._send(this.apc_username)
|
||||
await this.sleep(500)
|
||||
this._send(this.apc_password)
|
||||
await this.sleep(1000)
|
||||
this._send('1')
|
||||
await this.sleep(5000)
|
||||
this._send('ENTER')
|
||||
}
|
||||
|
||||
public async returnToMainMenu(maxAttempts = 5): Promise<void> {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
this._send('\x1B')
|
||||
const menuText = await this._waitFor('Main Menu', 5000)
|
||||
|
||||
if (menuText.includes('Control Console') || menuText.includes('Device Manager')) {
|
||||
return
|
||||
}
|
||||
}
|
||||
throw new Error('Unable to return to main menu after ESC attempts')
|
||||
}
|
||||
|
||||
public async navigateToOutlet(outletNumber: number): Promise<void> {
|
||||
await this.returnToMainMenu()
|
||||
this._send('1')
|
||||
await this.sleep(500)
|
||||
this._send(outletNumber.toString())
|
||||
await this.sleep(500)
|
||||
this._send('1')
|
||||
}
|
||||
|
||||
public async turnOnOutlet(outletNumber: number): Promise<void> {
|
||||
await this.navigateToOutlet(outletNumber)
|
||||
this._send('1')
|
||||
await this.sleep(500)
|
||||
this._send('YES')
|
||||
await this.sleep(500)
|
||||
this._send('')
|
||||
await this.sleep(500)
|
||||
this._send('\x1B')
|
||||
await this.sleep(500)
|
||||
this._send('\x1B')
|
||||
await this.sleep(2000)
|
||||
this._send('')
|
||||
}
|
||||
|
||||
public async turnOffOutlet(outletNumber: number): Promise<void> {
|
||||
await this.navigateToOutlet(outletNumber)
|
||||
this._send('2')
|
||||
await this.sleep(500)
|
||||
this._send('YES')
|
||||
await this.sleep(500)
|
||||
this._send('')
|
||||
await this.sleep(500)
|
||||
this._send('\x1B')
|
||||
await this.sleep(500)
|
||||
this._send('\x1B')
|
||||
await this.sleep(2000)
|
||||
this._send('')
|
||||
}
|
||||
|
||||
public async restartOutlet(outletNumber: number): Promise<void> {
|
||||
await this.navigateToOutlet(outletNumber)
|
||||
this._send('3')
|
||||
await this.sleep(500)
|
||||
this._send('YES')
|
||||
await this.sleep(500)
|
||||
this._send('')
|
||||
await this.sleep(500)
|
||||
this._send('\x1B')
|
||||
await this.sleep(500)
|
||||
this._send('\x1B')
|
||||
await this.sleep(2000)
|
||||
this._send('')
|
||||
}
|
||||
|
||||
public async reconnect(): Promise<boolean> {
|
||||
try {
|
||||
this.disconnect()
|
||||
await this.sleep(1000)
|
||||
console.log('RECONNECT APC:', this.apc_number, 'IP:', this.apc_ip)
|
||||
this.socket = new net.Socket()
|
||||
await this.connect()
|
||||
await this.login()
|
||||
return true
|
||||
} catch (err) {
|
||||
this._handleError(err as NodeJS.ErrnoException)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default APCController
|
||||
|
|
@ -10,6 +10,8 @@ import {
|
|||
sleep,
|
||||
} from '../ultils/helper.js'
|
||||
import Scenario from '#models/scenario'
|
||||
import Station from '#models/station'
|
||||
import APCController from './apc_connection.js'
|
||||
|
||||
interface LineConfig {
|
||||
id: number
|
||||
|
|
@ -48,7 +50,6 @@ export default class LineConnection {
|
|||
private outputBuffer: string
|
||||
private isRunningScript: boolean
|
||||
private connecting: boolean
|
||||
private isSendPlatform: boolean
|
||||
|
||||
constructor(config: LineConfig, socketIO: any) {
|
||||
this.config = config
|
||||
|
|
@ -57,7 +58,6 @@ export default class LineConnection {
|
|||
this.outputBuffer = ''
|
||||
this.isRunningScript = false
|
||||
this.connecting = false
|
||||
this.isSendPlatform = false
|
||||
}
|
||||
|
||||
connect(timeoutMs = 5000) {
|
||||
|
|
@ -89,6 +89,8 @@ export default class LineConnection {
|
|||
this.client.on('data', (data) => {
|
||||
if (this.connecting) return
|
||||
let message = data.toString()
|
||||
|
||||
if (message.includes('--More--')) this.writeCommand(' ')
|
||||
if (this.isRunningScript) this.outputBuffer += message
|
||||
// let output = cleanData(message)
|
||||
// console.log(`📨 [${this.config.port}] ${message}`)
|
||||
|
|
@ -168,9 +170,7 @@ export default class LineConnection {
|
|||
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
||||
return
|
||||
}
|
||||
if (cmd.includes('show platform') || cmd.includes('sh platform')) {
|
||||
this.isSendPlatform = true
|
||||
} else this.isSendPlatform = false
|
||||
|
||||
this.client.write(`${cmd}`)
|
||||
}
|
||||
|
||||
|
|
@ -366,4 +366,87 @@ export default class LineConnection {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async apcControl(action: 'on' | 'off' | 'restart') {
|
||||
try {
|
||||
const station = await Station.find(this.config.stationId)
|
||||
if (!station) throw new Error('Station not found')
|
||||
|
||||
const apcName = this.config.apcName || 'apc_1'
|
||||
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,
|
||||
number: this.config.lineNumber,
|
||||
onData: (data: string) => {
|
||||
this.config.output += data
|
||||
this.socketIO.emit('line_output', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
data: data,
|
||||
})
|
||||
appendLog(
|
||||
cleanData(data),
|
||||
this.config.stationId,
|
||||
this.config.lineNumber,
|
||||
this.config.port
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// Connect và login
|
||||
await apc.connect()
|
||||
await apc.login()
|
||||
|
||||
// Thực thi hành động
|
||||
this.socketIO.emit('apc_status', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
action,
|
||||
status: 'running',
|
||||
})
|
||||
|
||||
switch (action) {
|
||||
case 'on':
|
||||
await apc.turnOnOutlet(this.config.outlet)
|
||||
break
|
||||
case 'off':
|
||||
await apc.turnOffOutlet(this.config.outlet)
|
||||
break
|
||||
case 'restart':
|
||||
await apc.restartOutlet(this.config.outlet)
|
||||
break
|
||||
}
|
||||
|
||||
// Hoàn thành
|
||||
this.socketIO.emit('apc_status', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
action,
|
||||
status: 'done',
|
||||
})
|
||||
|
||||
apc.disconnect()
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
console.error('APC Control error:', msg)
|
||||
this.socketIO.emit('apc_status', {
|
||||
stationId: this.config.stationId,
|
||||
lineId: this.config.id,
|
||||
action,
|
||||
status: 'error',
|
||||
message: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,317 @@
|
|||
import net from 'node:net'
|
||||
|
||||
type PromptCallback = {
|
||||
prompt: string
|
||||
callback: (data: string) => void
|
||||
}
|
||||
|
||||
type PortInfo = {
|
||||
name: string
|
||||
status: string
|
||||
poe: string
|
||||
}
|
||||
|
||||
interface SwitchControllerOptions {
|
||||
host: string
|
||||
port?: number
|
||||
username: string
|
||||
password: string
|
||||
onData?: (data?: any) => void
|
||||
keep_connect?: boolean
|
||||
}
|
||||
|
||||
export default class SwitchController {
|
||||
private host: string
|
||||
private port: number
|
||||
private username: string
|
||||
private password: string
|
||||
private keep_connect: boolean
|
||||
private onData: (data?: any) => void
|
||||
private socket: net.Socket
|
||||
private status: 'CONNECTED' | 'DISCONNECTED'
|
||||
private buffer: string
|
||||
private output: string
|
||||
private promptCallbacks: PromptCallback[]
|
||||
public ports: PortInfo[]
|
||||
public portGroups: PortInfo[][]
|
||||
private isEnable: boolean
|
||||
private checkingPorts: boolean
|
||||
|
||||
constructor({
|
||||
host,
|
||||
port = 23,
|
||||
username,
|
||||
password,
|
||||
onData,
|
||||
keep_connect = false,
|
||||
}: SwitchControllerOptions) {
|
||||
this.host = host
|
||||
this.port = port
|
||||
this.username = username
|
||||
this.password = password
|
||||
this.keep_connect = keep_connect
|
||||
this.onData = onData || (() => {})
|
||||
this.socket = new net.Socket()
|
||||
this.status = 'DISCONNECTED'
|
||||
this.buffer = ''
|
||||
this.output = ''
|
||||
this.promptCallbacks = []
|
||||
this.ports = []
|
||||
this.portGroups = []
|
||||
this.isEnable = false
|
||||
this.checkingPorts = false
|
||||
}
|
||||
|
||||
private sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
private _handleData(data: string) {
|
||||
this.buffer += data
|
||||
|
||||
if (this.promptCallbacks.length > 0) {
|
||||
const { prompt, callback } = this.promptCallbacks[0]
|
||||
if (this.buffer.includes(prompt)) {
|
||||
this.promptCallbacks.shift()?.callback(this.buffer)
|
||||
this.buffer = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClose() {
|
||||
this.status = 'DISCONNECTED'
|
||||
this.isEnable = false
|
||||
this.onData(`[DISCONNECTED]`)
|
||||
}
|
||||
|
||||
private _handleError(err: Error & { code?: string }) {
|
||||
this.onData(`[ERROR] ${err.message}`)
|
||||
}
|
||||
|
||||
private _waitFor(prompt: string, timeout = 5000): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Timeout waiting for: ${prompt}`))
|
||||
}, timeout)
|
||||
|
||||
this.promptCallbacks.push({
|
||||
prompt,
|
||||
callback: (data: string) => {
|
||||
clearTimeout(timer)
|
||||
resolve(data)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private _send(command: string) {
|
||||
if (this.socket && !this.socket.destroyed && this.status === 'CONNECTED') {
|
||||
this.socket.write(command + '\r\n')
|
||||
}
|
||||
}
|
||||
|
||||
public connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.socket.connect(this.port, this.host, () => {
|
||||
this.status = 'CONNECTED'
|
||||
this.isEnable = false
|
||||
this.socket.setEncoding('utf8')
|
||||
this.checkStatusAllPorts()
|
||||
this.socket.on('data', (data) => this._handleData(data.toString()))
|
||||
resolve()
|
||||
})
|
||||
this.socket.on('close', () => this._handleClose())
|
||||
this.socket.on('error', (err) => this._handleError(err))
|
||||
} catch (error: any) {
|
||||
console.log('[ERROR CONNECT SWITCH]:', error.message)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this.socket.end()
|
||||
this.status = 'DISCONNECTED'
|
||||
this.isEnable = false
|
||||
}
|
||||
|
||||
public checkStatusAllPorts() {
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await this.getPorts()
|
||||
if (this.promptCallbacks.length === 0) this.buffer = ''
|
||||
} catch (err: any) {
|
||||
console.error('Error checking port status:', err)
|
||||
this.onData(`[ERROR] ${err.message}`)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
public async enterEnableMode() {
|
||||
if (this.isEnable) return
|
||||
await this.sleep(1000)
|
||||
this._send('')
|
||||
await this.sleep(1000)
|
||||
this._send('')
|
||||
await this.sleep(1000)
|
||||
|
||||
this._send('enable')
|
||||
await this.sleep(1000)
|
||||
this._send(this.password)
|
||||
this.isEnable = true
|
||||
}
|
||||
|
||||
public async login() {
|
||||
await this.enterEnableMode()
|
||||
this._send('terminal length 0')
|
||||
}
|
||||
|
||||
public async checkPortStatus(port: string) {
|
||||
this._send(`show interface ${port}`)
|
||||
const result = await this._waitFor('#')
|
||||
return result
|
||||
}
|
||||
|
||||
public async checkPoEStatus(port: string) {
|
||||
this._send(`show power inline ${port}`)
|
||||
const result = await this._waitFor('#')
|
||||
return result
|
||||
}
|
||||
|
||||
public async turnPortOff(port: string) {
|
||||
await this.enterEnableMode()
|
||||
this._send(`configure terminal`)
|
||||
await this._waitFor('(config)#')
|
||||
this._send(`interface ${port}`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`shutdown`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`end`)
|
||||
}
|
||||
|
||||
public async turnPortOn(port: string) {
|
||||
await this.enterEnableMode()
|
||||
this._send(`configure terminal`)
|
||||
await this._waitFor('(config)#')
|
||||
this._send(`interface ${port}`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`no shutdown`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`end`)
|
||||
}
|
||||
|
||||
public async restartPort(port: string) {
|
||||
await this.enterEnableMode()
|
||||
this._send(`configure terminal`)
|
||||
await this._waitFor('(config)#')
|
||||
this._send(`interface ${port}`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`shutdown`)
|
||||
await this._waitFor('(config-if)#')
|
||||
await this.sleep(2000)
|
||||
this._send(`no shutdown`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`end`)
|
||||
}
|
||||
|
||||
public async disablePoE(port: string) {
|
||||
await this.enterEnableMode()
|
||||
this._send(`configure terminal`)
|
||||
await this._waitFor('(config)#')
|
||||
this._send(`interface ${port}`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`power inline never`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`end`)
|
||||
}
|
||||
|
||||
public async enablePoE(port: string) {
|
||||
await this.enterEnableMode()
|
||||
this._send(`configure terminal`)
|
||||
await this._waitFor('(config)#')
|
||||
this._send(`interface ${port}`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`power inline auto`)
|
||||
await this._waitFor('(config-if)#')
|
||||
this._send(`end`)
|
||||
}
|
||||
|
||||
public async restartPoE(port: string) {
|
||||
await this.disablePoE(port)
|
||||
await this.sleep(3000)
|
||||
await this.enablePoE(port)
|
||||
}
|
||||
|
||||
public async getPorts(): Promise<boolean> {
|
||||
this.checkingPorts = true
|
||||
this._send('show interface status')
|
||||
await this.sleep(2000)
|
||||
const statusOutput = this.buffer
|
||||
this.buffer = ''
|
||||
|
||||
const lines = statusOutput.split('\n')
|
||||
const ports = this.ports?.length > 0 ? [...this.ports] : []
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^(\S+)\s+(connected|notconnect|disabled|inactive)/i)
|
||||
if (match) {
|
||||
const name = match[1]
|
||||
const rawStatus = match[2].toLowerCase()
|
||||
const status = rawStatus === 'connected' ? 'ON' : 'OFF'
|
||||
const port = ports.find((p) => p.name === name)
|
||||
if (port) {
|
||||
port.status = status
|
||||
} else ports.push({ name, status, poe: 'UNKNOWN' })
|
||||
}
|
||||
}
|
||||
|
||||
// PoE check
|
||||
this._send('show power inline')
|
||||
await this.sleep(2000)
|
||||
const poeOutput = this.buffer
|
||||
this.buffer = ''
|
||||
|
||||
const poeLines = poeOutput.split('\n')
|
||||
for (const line of poeLines) {
|
||||
const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
|
||||
if (match) {
|
||||
const name = match[1]
|
||||
const poeStatus = match[2].toLowerCase() === 'on' ? 'ON' : 'OFF'
|
||||
const port = ports.find((p) => p.name === name)
|
||||
if (port) port.poe = poeStatus
|
||||
}
|
||||
}
|
||||
|
||||
const grouped: Record<string, PortInfo[]> = {}
|
||||
for (const port of ports) {
|
||||
const prefixMatch = port.name.match(/^([A-Za-z]+)/)
|
||||
const prefix = prefixMatch ? prefixMatch[1] : 'Unknown'
|
||||
if (!grouped[prefix]) grouped[prefix] = []
|
||||
const existing = grouped[prefix].find((el) => el.name === port.name)
|
||||
if (!existing) grouped[prefix].push(port)
|
||||
}
|
||||
|
||||
const groupedArray = Object.values(grouped)
|
||||
this.ports = ports
|
||||
this.portGroups = groupedArray
|
||||
this.onData()
|
||||
this.checkingPorts = false
|
||||
return true
|
||||
}
|
||||
|
||||
public async reconnect(): Promise<boolean> {
|
||||
try {
|
||||
this.disconnect()
|
||||
this.socket = new net.Socket()
|
||||
await this.sleep(1000)
|
||||
await this.connect()
|
||||
await this.login()
|
||||
await this.getPorts()
|
||||
return true
|
||||
} catch (err: any) {
|
||||
this._handleError(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,15 @@ import { CustomServer, CustomSocket } from '../app/ultils/types.js'
|
|||
import Line from '#models/line'
|
||||
import Station from '#models/station'
|
||||
|
||||
interface HandleOptions {
|
||||
command?: string
|
||||
actionApc?: string
|
||||
scenario?: any
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
type LineAction = (line: LineConnection, options?: HandleOptions) => Promise<void | unknown> | void
|
||||
|
||||
export default class SocketIoProvider {
|
||||
private static _io: CustomServer
|
||||
constructor(protected app: ApplicationService) {}
|
||||
|
|
@ -108,67 +117,29 @@ export class WebSocketIo {
|
|||
|
||||
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 && line.config.status === 'connected') {
|
||||
this.lineConnecting = 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)
|
||||
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
|
||||
const lineReconnect = this.lineMap.get(lineId)
|
||||
if (lineReconnect) {
|
||||
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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.handleLineOperation(
|
||||
io,
|
||||
stationId,
|
||||
lineIds,
|
||||
async (line) => line.writeCommand(command),
|
||||
{ command, timeout: 180000 }
|
||||
)
|
||||
})
|
||||
|
||||
socket.on('run_scenario', async (data) => {
|
||||
const lineId = data.id
|
||||
const scenario = data.scenario
|
||||
const line = this.lineMap.get(lineId)
|
||||
if (line && line.config.status === 'connected') {
|
||||
this.setTimeoutConnect(
|
||||
lineId,
|
||||
line,
|
||||
scenario?.timeout ? Number(scenario?.timeout) + 180000 : 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' })
|
||||
await this.handleLineOperation(
|
||||
io,
|
||||
data.stationId,
|
||||
[lineId],
|
||||
async (line) => line.runScript(scenario),
|
||||
{
|
||||
scenario,
|
||||
timeout: scenario?.timeout ? Number(scenario.timeout) + 180000 : 300000,
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
socket.on('open_cli', async (data) => {
|
||||
|
|
@ -261,6 +232,18 @@ export class WebSocketIo {
|
|||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('control_apc', async (data) => {
|
||||
const { lineIds, stationId, action } = data
|
||||
|
||||
await this.handleLineOperation(
|
||||
io,
|
||||
stationId,
|
||||
lineIds,
|
||||
async (line) => line.apcControl(action),
|
||||
{ actionApc: action, timeout: 180000 }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
socketServer.listen(SOCKET_IO_PORT, () => {
|
||||
|
|
@ -318,4 +301,53 @@ export class WebSocketIo {
|
|||
|
||||
this.intervalMap[`${lineId}`] = interval
|
||||
}
|
||||
|
||||
/**
|
||||
* Hàm xử lý chung cho mọi action (writeCommand, runScript, v.v.)
|
||||
*/
|
||||
async handleLineOperation(
|
||||
io: CustomServer,
|
||||
stationId: number,
|
||||
lineIds: number[],
|
||||
action: LineAction,
|
||||
options: HandleOptions = {}
|
||||
): Promise<void> {
|
||||
for (const lineId of lineIds) {
|
||||
try {
|
||||
const line = this.lineMap.get(lineId)
|
||||
|
||||
if (line && line.config.status === 'connected') {
|
||||
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
|
||||
this.setTimeoutConnect(lineId, line, options.timeout)
|
||||
await action(line, options)
|
||||
} 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)
|
||||
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
|
||||
|
||||
const lineReconnect = this.lineMap.get(lineId)
|
||||
if (lineReconnect) {
|
||||
this.setTimeoutConnect(lineId, lineReconnect, options.timeout)
|
||||
await action(lineReconnect, options)
|
||||
}
|
||||
} else {
|
||||
io.emit('line_disconnected', {
|
||||
stationId,
|
||||
lineId,
|
||||
status: 'disconnected',
|
||||
})
|
||||
io.emit('line_error', { lineId, error: 'Line not connected\r\n', stationId })
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
io.emit('line_error', { lineId, error: `[ERROR] ${err.message}\r\n`, stationId })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ import axios from "axios";
|
|||
import CardLine from "./components/CardLine";
|
||||
import { SocketProvider, useSocket } from "./context/SocketContext";
|
||||
import {
|
||||
ButtonConnect,
|
||||
// ButtonConnect,
|
||||
ButtonControlApc,
|
||||
ButtonCopy,
|
||||
ButtonDPELP,
|
||||
ButtonScenario,
|
||||
|
|
@ -122,11 +123,19 @@ function App() {
|
|||
if (!socket || !stations?.length) return;
|
||||
|
||||
socket.on("line_connected", (data) =>
|
||||
updateValueLineStation(data?.id, { status: data.status }, data?.stationId)
|
||||
updateValueLineStation(
|
||||
data?.lineId,
|
||||
{ status: data.status },
|
||||
data?.stationId
|
||||
)
|
||||
);
|
||||
|
||||
socket.on("line_disconnected", (data) =>
|
||||
updateValueLineStation(data?.id, { status: data.status }, data?.stationId)
|
||||
updateValueLineStation(
|
||||
data?.lineId,
|
||||
{ status: data.status },
|
||||
data?.stationId
|
||||
)
|
||||
);
|
||||
|
||||
socket?.on("line_output", (data) => {
|
||||
|
|
@ -347,7 +356,7 @@ function App() {
|
|||
data.userOpenCLI = user?.userName;
|
||||
socket?.emit("open_cli", {
|
||||
lineId: line.id,
|
||||
stationId: line.station_id,
|
||||
stationId: line.stationId || line.station_id,
|
||||
userEmail: user?.email,
|
||||
userName: user?.userName,
|
||||
});
|
||||
|
|
@ -438,7 +447,13 @@ function App() {
|
|||
setSelectedLines={setSelectedLines}
|
||||
station={station}
|
||||
/>
|
||||
<ButtonConnect
|
||||
{/* <ButtonConnect
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
station={station}
|
||||
socket={socket}
|
||||
/> */}
|
||||
<ButtonControlApc
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
station={station}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import type { Socket } from "socket.io-client";
|
||||
import type { IScenario, TextFSM, TLine, TStation } from "../untils/types";
|
||||
import { Button } from "@mantine/core";
|
||||
import { Button, Menu, rem, Text } from "@mantine/core";
|
||||
import classes from "./Component.module.css";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {
|
||||
IconPlayerPlay,
|
||||
IconPlugConnectedX,
|
||||
IconRotate,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
export const ButtonDPELP = ({
|
||||
socket,
|
||||
|
|
@ -307,3 +313,133 @@ export const ButtonConnect = ({
|
|||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonControlApc = ({
|
||||
selectedLines,
|
||||
setSelectedLines,
|
||||
station,
|
||||
socket,
|
||||
}: {
|
||||
setSelectedLines: (value: React.SetStateAction<TLine[]>) => void;
|
||||
selectedLines: TLine[];
|
||||
station: TStation;
|
||||
socket: Socket | null;
|
||||
}) => {
|
||||
return (
|
||||
<Menu
|
||||
closeOnItemClick={false}
|
||||
position="left-start"
|
||||
transitionProps={{
|
||||
transition: "rotate-left",
|
||||
duration: 150,
|
||||
}}
|
||||
>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
color="green"
|
||||
disabled={selectedLines.length === 0}
|
||||
variant="outline"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {}}
|
||||
>
|
||||
APC
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{["APC 1", "APC 2"].map((item, i) => (
|
||||
<Menu.Item
|
||||
key={i}
|
||||
onClick={() => {
|
||||
if (
|
||||
(i === 0 && !station.apc_1_ip) ||
|
||||
(i === 1 && !station.apc_2_ip)
|
||||
) {
|
||||
notifications.show({
|
||||
title: "Warning",
|
||||
message: "Please set APC IP address in station settings!",
|
||||
color: "orange",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
disabled={
|
||||
(i === 0 && !station.apc_1_ip) || (i === 1 && !station.apc_2_ip)
|
||||
}
|
||||
withArrow
|
||||
offset={20}
|
||||
position="left-start"
|
||||
transitionProps={{
|
||||
transition: "rotate-left",
|
||||
duration: 150,
|
||||
}}
|
||||
>
|
||||
<Menu.Target>
|
||||
<div>{item}</div>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item>
|
||||
<div
|
||||
className={classes.itemMenuPower}
|
||||
onClick={() => {
|
||||
setSelectedLines([]);
|
||||
socket?.emit("control_apc", {
|
||||
lineIds: selectedLines.map((el) => el.id),
|
||||
stationId: station.id,
|
||||
action: "on",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconPlayerPlay
|
||||
color="green"
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
/>
|
||||
<Text size="sm">Turn on All</Text>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<div
|
||||
className={classes.itemMenuPower}
|
||||
onClick={() => {
|
||||
setSelectedLines([]);
|
||||
socket?.emit("control_apc", {
|
||||
lineIds: selectedLines.map((el) => el.id),
|
||||
stationId: station.id,
|
||||
action: "off",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconPlugConnectedX
|
||||
color="red"
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
/>
|
||||
<Text size="sm">Turn off All</Text>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<div
|
||||
className={classes.itemMenuPower}
|
||||
onClick={() => {
|
||||
setSelectedLines([]);
|
||||
socket?.emit("control_apc", {
|
||||
lineIds: selectedLines.map((el) => el.id),
|
||||
stationId: station.id,
|
||||
action: "restart",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconRotate
|
||||
color="orange"
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
/>
|
||||
<Text size="sm">Restart All</Text>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -75,3 +75,12 @@
|
|||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.itemMenuPower {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
gap: 4px;
|
||||
width: 100px;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,12 +91,12 @@ const StationSetting = ({
|
|||
|
||||
const dataLine = dataStation.lines.map((value) => ({
|
||||
id: value.id,
|
||||
lineNumber: value.line_number || 0,
|
||||
lineNumber: value.lineNumber || value.line_number || 0,
|
||||
port: value.port,
|
||||
lineClear: value.line_clear || 0,
|
||||
apc_name: value.apc_name,
|
||||
lineClear: value.lineClear || value.line_clear || 0,
|
||||
apc_name: value.apcName || value.apc_name,
|
||||
outlet: value.outlet,
|
||||
station_id: value.station_id,
|
||||
station_id: value.stationId || value.station_id,
|
||||
}));
|
||||
setLines(dataLine);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
Grid,
|
||||
Group,
|
||||
Modal,
|
||||
ScrollArea,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import type {
|
||||
|
|
@ -19,6 +20,7 @@ import type { Socket } from "socket.io-client";
|
|||
import classes from "./Component.module.css";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
||||
import { ButtonDPELP } from "./ButtonAction";
|
||||
|
||||
const ModalTerminal = ({
|
||||
opened,
|
||||
|
|
@ -103,35 +105,57 @@ const ModalTerminal = ({
|
|||
size={"80%"}
|
||||
style={{ position: "absolute", left: 0 }}
|
||||
title={
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text size="md" mr={10}>
|
||||
Line number: <strong>{line?.line_number || ""}</strong>
|
||||
</Text>
|
||||
<Text size="md" mr={10}>
|
||||
- <strong>{line?.port || ""}</strong>
|
||||
</Text>
|
||||
{line?.status === "connected" && (
|
||||
<IconCircleCheckFilled color="green" />
|
||||
)}
|
||||
<div
|
||||
<Flex align={"center"} justify={"space-between"} w={"100%"}>
|
||||
<Box
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginLeft: "16px",
|
||||
fontSize: "12px",
|
||||
color: "red",
|
||||
display: "flex",
|
||||
// justifyContent: "center",
|
||||
width: "400px",
|
||||
}}
|
||||
>
|
||||
{line?.userOpenCLI
|
||||
? line?.userOpenCLI + " is using"
|
||||
: "Terminal is used"}
|
||||
</div>
|
||||
</Box>
|
||||
<Text size="md" mr={10}>
|
||||
Line number: <strong>{line?.line_number || ""}</strong>
|
||||
</Text>
|
||||
<Text size="md" mr={10}>
|
||||
- <strong>{line?.port || ""}</strong>
|
||||
</Text>
|
||||
{line?.status === "connected" && (
|
||||
<IconCircleCheckFilled color="green" />
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginLeft: "16px",
|
||||
fontSize: "12px",
|
||||
color: "red",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{line?.userOpenCLI
|
||||
? line?.userOpenCLI + " is using"
|
||||
: "Terminal is used"}
|
||||
</div>
|
||||
</Box>
|
||||
<Flex
|
||||
align={"center"}
|
||||
justify={"space-between"}
|
||||
gap={"md"}
|
||||
style={{
|
||||
width: "400px",
|
||||
}}
|
||||
>
|
||||
<div className={classes.info_line} style={{ fontSize: "14px" }}>
|
||||
PID: {line?.inventory?.pid || ""}
|
||||
</div>
|
||||
<div className={classes.info_line} style={{ fontSize: "14px" }}>
|
||||
SN: {line?.inventory?.sn || ""}
|
||||
</div>
|
||||
<div className={classes.info_line} style={{ fontSize: "14px" }}>
|
||||
VID: {line?.inventory?.vid || ""}
|
||||
</div>
|
||||
</Flex>
|
||||
<Box></Box>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Grid>
|
||||
|
|
@ -152,37 +176,52 @@ const ModalTerminal = ({
|
|||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={2}>
|
||||
{scenarios.map((scenario) => (
|
||||
<Button
|
||||
disabled={
|
||||
isDisable ||
|
||||
(typeof line?.userEmailOpenCLI !== "undefined" &&
|
||||
line?.userEmailOpenCLI !== user?.email)
|
||||
}
|
||||
className={classes.buttonScenario}
|
||||
key={scenario.id}
|
||||
miw={"100px"}
|
||||
mb={"6px"}
|
||||
style={{ minHeight: "24px" }}
|
||||
mr={"5px"}
|
||||
variant={"outline"}
|
||||
onClick={async () => {
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
if (line)
|
||||
socket?.emit(
|
||||
"run_scenario",
|
||||
Object.assign(line, {
|
||||
scenario: scenario,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{scenario.title}
|
||||
</Button>
|
||||
))}
|
||||
<ScrollArea h={"60vh"} style={{ paddingBottom: "12px" }}>
|
||||
<Flex w={"100%"} direction={"column"} wrap={"wrap"} gap={"6px"}>
|
||||
<ButtonDPELP
|
||||
socket={socket}
|
||||
selectedLines={line ? [line] : []}
|
||||
isDisable={isDisable}
|
||||
onClick={() => {
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
}}
|
||||
/>
|
||||
{scenarios.map((scenario) => (
|
||||
<Button
|
||||
disabled={
|
||||
isDisable ||
|
||||
(typeof line?.userEmailOpenCLI !== "undefined" &&
|
||||
line?.userEmailOpenCLI !== user?.email)
|
||||
}
|
||||
className={classes.buttonScenario}
|
||||
key={scenario.id}
|
||||
miw={"100px"}
|
||||
mb={"6px"}
|
||||
style={{ minHeight: "24px" }}
|
||||
mr={"5px"}
|
||||
variant={"outline"}
|
||||
onClick={async () => {
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
if (line)
|
||||
socket?.emit(
|
||||
"run_scenario",
|
||||
Object.assign(line, {
|
||||
scenario: scenario,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{scenario.title}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Flex justify={"space-between"}>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export type TLine = {
|
|||
lineClear: number;
|
||||
line_clear?: number;
|
||||
station_id: number;
|
||||
stationId?: number;
|
||||
data?: string | any;
|
||||
type?: string;
|
||||
inventory?: any;
|
||||
|
|
@ -73,6 +74,7 @@ export type TLine = {
|
|||
cliOpened?: boolean;
|
||||
systemLogUrl?: string;
|
||||
apc_name: string;
|
||||
apcName?: string;
|
||||
created_at?: string; // or use Date if you're working with Date objects
|
||||
updated_at?: string; // or use Date if you're working with Date objects
|
||||
notes?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue