ATC_SIMPLE/BACKEND/app/services/apc_connection.ts

310 lines
7.8 KiB
TypeScript

import net, { Socket } from 'node:net'
import { appendLog } from '../ultils/helper.js'
interface APCOptions {
host: string
port?: number
username: string
password: string
onData?: (data: string, status: string) => void
number?: number
keep_connect?: boolean
stationId: number
stationName: string
stationIP: string
}
interface PromptCallback {
prompt: string
callback: (data: string) => void
}
class APCController {
public apc_number?: number
public apc_ip: string
private apc_port: number
private apc_username: string
private apc_password: string
public status: 'CONNECTED' | 'DISCONNECTED' | 'TIMEOUT'
private socket: Socket
private buffer: string
public output: string
private promptCallbacks: PromptCallback[]
private onData: (data: string, status: string) => void
private retryConnect: number
private stationId: number
private stationName: string
private stationIP: string
constructor({
host,
port = 23,
username,
password,
onData,
number,
stationId,
stationName,
stationIP,
}: 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
this.stationId = stationId
this.stationName = stationName
this.stationIP = stationIP
}
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.retryConnect = 0
this.socket.setEncoding('utf8')
resolve()
})
this.socket.on('data', (data) => this._handleData(data.toString()))
this.socket.on('close', () => {
this._handleClose()
resolve()
})
this.socket.on('timeout', () => {
this._handleTimeout()
resolve()
})
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(-10000)
this.onData(this.buffer, this.status)
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 = ''
}
}
appendLog(data, this.stationId, this.stationName, this.stationIP, 'APC_' + this.apc_number)
}
private _handleClose(): void {
this.status = 'DISCONNECTED'
this.output += '\r\n\r\n[DISCONNECTED] Socket closed'
this.onData(this.output, this.status)
this._cleanup()
}
private async _handleTimeout(): Promise<void> {
this.status = 'TIMEOUT'
this.output += '\r\n\r\n[TIMEOUT] Connection timed out'
this.onData(this.output, this.status)
if (this.retryConnect <= 5) {
await this.sleep(5000)
console.log('Retry connect times', this.retryConnect)
this.retryConnect += 1
await this.reconnect()
} else {
this.retryConnect = 0
}
}
private async _handleError(err: NodeJS.ErrnoException): Promise<void> {
this.output += `\r\n\r\n[ERROR] ${err.message}`
this.onData(this.output, this.status)
if (err.code === 'ECONNRESET') {
console.log('[ECONNRESET] Trying reconnect apc:', this.apc_ip)
if (this.retryConnect <= 5) {
await this.sleep(15000)
console.log('Retry connect times', this.retryConnect)
this.retryConnect += 1
await this.reconnect()
}
}
}
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)
},
})
})
}
public _send(command: string): void {
if (this.socket && !this.socket.destroyed && this.socket.readyState) {
// console.log('SEND COMMAND TO APC:', command)
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 navigateToOutlets(): Promise<void> {
await this.returnToMainMenu()
this._send('1')
}
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