Skip to main content
Glama
ooples

MCP Console Automation Server

SerialProtocol.ts31.9 kB
import { SerialPort, SerialPortOpenOptions } from 'serialport'; import { AutoDetectTypes } from '@serialport/bindings-cpp'; import { ReadlineParser } from '@serialport/parser-readline'; import { ByteLengthParser } from '@serialport/parser-byte-length'; import { v4 as uuidv4 } from 'uuid'; import stripAnsi from 'strip-ansi'; import { BaseProtocol } from '../core/BaseProtocol.js'; import { ProtocolCapabilities, SessionState, ProtocolHealthStatus, } from '../core/IProtocol.js'; import { SerialConnectionOptions, SerialDeviceInfo, SerialProtocolProfile, ConsoleSession, ConsoleOutput, ConsoleEvent, SessionOptions, ConsoleType, } from '../types/index.js'; /** * Production-ready Serial/COM port protocol implementation for embedded device communication * Supports Arduino, ESP32, and various USB serial adapters (FTDI, CH340, CP2102) * Now extends BaseProtocol with session management fixes */ export class SerialProtocol extends BaseProtocol { public readonly type: ConsoleType = 'serial'; public readonly capabilities: ProtocolCapabilities; private connections: Map<string, SerialConnectionState> = new Map(); private deviceProfiles: Map<string, SerialProtocolProfile> = new Map(); private reconnectTimers: Map<string, NodeJS.Timeout> = new Map(); private static readonly DEFAULT_BAUD_RATES = [ 110, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000, 460800, 921600, ]; private static readonly DEVICE_VENDOR_IDS = { ARDUINO: ['2341', '2A03', '1B4F'], FTDI: ['0403'], CH340: ['1A86'], CP2102: ['10C4', '1BA4'], ESP32: ['303A', '1A86', '0403'], // Various ESP32 board manufacturers }; constructor() { super('SerialProtocol'); this.capabilities = { supportsStreaming: true, supportsFileTransfer: false, supportsX11Forwarding: false, supportsPortForwarding: false, supportsAuthentication: false, supportsEncryption: false, supportsCompression: false, supportsMultiplexing: false, supportsKeepAlive: false, supportsReconnection: true, supportsBinaryData: true, supportsCustomEnvironment: false, supportsWorkingDirectory: false, supportsSignals: true, supportsResizing: false, supportsPTY: false, maxConcurrentSessions: 10, defaultTimeout: 30000, supportedEncodings: ['utf-8', 'ascii', 'binary'], supportedAuthMethods: [], platformSupport: { windows: true, linux: true, macos: true, freebsd: true, }, }; // Health status will be managed by getHealthStatus() method this.initializeDeviceProfiles(); this.startDeviceMonitoring(); } /** * Initialize predefined device profiles for common embedded platforms */ private initializeDeviceProfiles(): void { // Arduino Uno/Nano/Pro Mini profile this.deviceProfiles.set('arduino_uno', { name: 'Arduino Uno/Nano', deviceType: 'arduino', defaultBaudRate: 9600, supportedBaudRates: [ 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, ], defaultSettings: { dataBits: 8, stopBits: 1, parity: 'none', rtscts: false, dtr: true, rts: true, resetOnConnect: true, resetDelay: 100, lineEnding: '\r\n', encoding: 'ascii', timeout: 5000, }, resetSequence: { dtr: false, rts: true, delay: 100, pulseWidth: 50, }, bootloaderDetection: { patterns: [/avrdude/i, /STK500/i], timeout: 2000, }, commandProtocol: { commandTerminator: '\r\n', responseTerminator: '\r\n', timeout: 3000, retryCount: 3, }, }); // ESP32 profile this.deviceProfiles.set('esp32', { name: 'ESP32', deviceType: 'esp32', defaultBaudRate: 115200, supportedBaudRates: [9600, 57600, 115200, 230400, 460800, 921600], defaultSettings: { dataBits: 8, stopBits: 1, parity: 'none', rtscts: false, dtr: true, rts: true, resetOnConnect: false, resetDelay: 250, lineEnding: '\r\n', encoding: 'utf8', timeout: 10000, bufferSize: 8192, }, resetSequence: { dtr: false, rts: false, delay: 250, pulseWidth: 100, }, bootloaderDetection: { patterns: [/esptool/i, /ESP32/i, /Brownout detector/i], timeout: 5000, }, commandProtocol: { commandTerminator: '\r\n', responseTerminator: '\r\n', timeout: 5000, retryCount: 2, }, }); // Generic UART device profile this.deviceProfiles.set('generic', { name: 'Generic UART', deviceType: 'generic', defaultBaudRate: 9600, supportedBaudRates: SerialProtocol.DEFAULT_BAUD_RATES, defaultSettings: { dataBits: 8, stopBits: 1, parity: 'none', rtscts: false, lineEnding: '\n', encoding: 'ascii', timeout: 5000, }, commandProtocol: { commandTerminator: '\n', responseTerminator: '\n', timeout: 3000, retryCount: 1, }, }); } /** * Discover and enumerate all available serial devices */ async discoverDevices(): Promise<SerialDeviceInfo[]> { try { const ports = await SerialPort.list(); const devices: SerialDeviceInfo[] = []; for (const port of ports) { const deviceInfo: SerialDeviceInfo = { path: port.path, manufacturer: port.manufacturer, serialNumber: port.serialNumber, pnpId: port.pnpId, locationId: port.locationId, vendorId: port.vendorId, productId: port.productId, description: port.manufacturer || 'Unknown Device', isConnected: false, deviceType: this.detectDeviceType( port.vendorId, port.productId, port.manufacturer ), supportedBaudRates: this.getSupportedBaudRates(port.vendorId), capabilities: this.getDeviceCapabilities(port.vendorId), }; devices.push(deviceInfo); } this.logger.info(`Discovered ${devices.length} serial devices`); return devices; } catch (error) { this.logger.error('Failed to discover serial devices:', error); throw new Error(`Serial device discovery failed: ${error.message}`); } } /** * Detect device type based on vendor/product IDs and manufacturer */ private detectDeviceType( vendorId?: string, productId?: string, manufacturer?: string ): SerialDeviceInfo['deviceType'] { if (!vendorId) return 'generic'; const vid = vendorId.toUpperCase(); if (SerialProtocol.DEVICE_VENDOR_IDS.ARDUINO.includes(vid)) { return 'arduino'; } if (SerialProtocol.DEVICE_VENDOR_IDS.FTDI.includes(vid)) { return 'ftdi'; } if (SerialProtocol.DEVICE_VENDOR_IDS.CH340.includes(vid)) { return 'ch340'; } if (SerialProtocol.DEVICE_VENDOR_IDS.CP2102.includes(vid)) { return 'cp2102'; } if (SerialProtocol.DEVICE_VENDOR_IDS.ESP32.includes(vid)) { return 'esp32'; } // Additional detection based on manufacturer string if (manufacturer) { const mfg = manufacturer.toLowerCase(); if (mfg.includes('arduino') || mfg.includes('arduino.cc')) return 'arduino'; if (mfg.includes('espressif') || mfg.includes('esp32')) return 'esp32'; if (mfg.includes('ftdi')) return 'ftdi'; if (mfg.includes('qinheng') || mfg.includes('ch340')) return 'ch340'; if (mfg.includes('silicon labs') || mfg.includes('cp210')) return 'cp2102'; } return 'generic'; } /** * Get supported baud rates for device type */ private getSupportedBaudRates(vendorId?: string): number[] { const deviceType = this.detectDeviceType(vendorId); switch (deviceType) { case 'esp32': return [9600, 57600, 115200, 230400, 460800, 921600]; case 'arduino': return [ 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, ]; case 'ftdi': return [ 110, 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, ]; default: return SerialProtocol.DEFAULT_BAUD_RATES; } } /** * Get device capabilities based on vendor ID */ private getDeviceCapabilities(vendorId?: string) { const deviceType = this.detectDeviceType(vendorId); return { hardwareFlowControl: ['ftdi', 'cp2102'].includes(deviceType), softwareFlowControl: true, dtrRtsControl: true, breakSignal: ['ftdi', 'cp2102'].includes(deviceType), }; } /** * Create a new serial connection */ async createConnection( sessionId: string, options: SerialConnectionOptions ): Promise<void> { try { // Apply device profile defaults if available const enhancedOptions = await this.enhanceOptionsWithProfile(options); // Validate the serial port path await this.validateSerialPort(enhancedOptions.path); const serialOptions: SerialPortOpenOptions<AutoDetectTypes> = { path: enhancedOptions.path, baudRate: enhancedOptions.baudRate || 9600, dataBits: enhancedOptions.dataBits || 8, stopBits: enhancedOptions.stopBits || 1, parity: enhancedOptions.parity || 'none', rtscts: enhancedOptions.rtscts || false, xon: enhancedOptions.xon || false, xoff: enhancedOptions.xoff || false, xany: enhancedOptions.xany || false, hupcl: enhancedOptions.hupcl !== false, autoOpen: enhancedOptions.autoOpen !== false, lock: enhancedOptions.lock !== false, highWaterMark: enhancedOptions.highWaterMark || 65536, vmin: enhancedOptions.vmin || 1, vtime: enhancedOptions.vtime || 0, }; const serialPort = new SerialPort(serialOptions); const connectionState: SerialConnectionState = { sessionId, port: serialPort, options: enhancedOptions, isConnected: false, reconnectAttempts: 0, lastActivity: new Date(), outputBuffer: [], parsers: new Map(), deviceInfo: await this.getDeviceInfoForPath(enhancedOptions.path), }; this.connections.set(sessionId, connectionState); this.setupSerialPortHandlers(sessionId, connectionState); this.setupParsers(sessionId, connectionState); // Handle device reset if required if (enhancedOptions.resetOnConnect) { await this.performDeviceReset(sessionId); } this.logger.info( `Serial connection created for session ${sessionId} on ${enhancedOptions.path}` ); } catch (error) { this.logger.error( `Failed to create serial connection for session ${sessionId}:`, error ); throw new Error(`Serial connection failed: ${error.message}`); } } /** * Enhance options with device profile defaults */ private async enhanceOptionsWithProfile( options: SerialConnectionOptions ): Promise<SerialConnectionOptions> { let profile: SerialProtocolProfile | undefined; // Use explicit device type or detect from path if (options.deviceType) { profile = this.getProfileForDeviceType(options.deviceType); } else { const deviceInfo = await this.getDeviceInfoForPath(options.path); if (deviceInfo?.deviceType) { profile = this.getProfileForDeviceType(deviceInfo.deviceType); } } if (!profile) { profile = this.deviceProfiles.get('generic'); } // Merge profile defaults with user options (user options take precedence) return { ...profile?.defaultSettings, ...options, baudRate: options.baudRate || profile?.defaultBaudRate || 9600, }; } /** * Get profile for device type */ private getProfileForDeviceType( deviceType: SerialDeviceInfo['deviceType'] ): SerialProtocolProfile | undefined { switch (deviceType) { case 'arduino': return this.deviceProfiles.get('arduino_uno'); case 'esp32': return this.deviceProfiles.get('esp32'); default: return this.deviceProfiles.get('generic'); } } /** * Validate that the serial port exists and is available */ private async validateSerialPort(path: string): Promise<void> { try { const ports = await SerialPort.list(); const portExists = ports.some((port) => port.path === path); if (!portExists) { throw new Error(`Serial port ${path} not found`); } } catch (error) { throw new Error(`Serial port validation failed: ${error.message}`); } } /** * Get device info for a specific path */ private async getDeviceInfoForPath( path: string ): Promise<SerialDeviceInfo | undefined> { try { const devices = await this.discoverDevices(); return devices.find((device) => device.path === path); } catch (error) { this.logger.warn(`Failed to get device info for ${path}:`, error); return undefined; } } /** * Setup serial port event handlers */ private setupSerialPortHandlers( sessionId: string, connectionState: SerialConnectionState ): void { const { port } = connectionState; port.on('open', () => { connectionState.isConnected = true; connectionState.reconnectAttempts = 0; connectionState.lastActivity = new Date(); // Apply DTR/RTS settings after connection this.applySignalSettings(sessionId); this.logger.info(`Serial port opened for session ${sessionId}`); this.emit('connection', { sessionId, type: 'connected' }); }); port.on('data', (data: Buffer) => { this.handleSerialData(sessionId, data); }); port.on('error', (error: Error) => { this.logger.error(`Serial port error for session ${sessionId}:`, error); connectionState.isConnected = false; this.emit('error', { sessionId, type: 'connection_error', error: error.message, }); // Attempt reconnection if enabled if (connectionState.options.reconnectOnDisconnect) { this.scheduleReconnect(sessionId); } }); port.on('close', () => { this.logger.info(`Serial port closed for session ${sessionId}`); connectionState.isConnected = false; this.emit('disconnection', { sessionId, type: 'disconnected' }); // Attempt reconnection if enabled if (connectionState.options.reconnectOnDisconnect) { this.scheduleReconnect(sessionId); } }); } /** * Setup data parsers based on connection options */ private setupParsers( sessionId: string, connectionState: SerialConnectionState ): void { const { port, options } = connectionState; // Setup line-based parser for text mode if (options.encoding !== 'binary') { const lineParser = port.pipe( new ReadlineParser({ delimiter: options.lineEnding || '\r\n', encoding: (options.encoding as BufferEncoding) || 'utf8', includeDelimiter: false, }) ); lineParser.on('data', (line: string) => { this.handleParsedLine(sessionId, line); }); connectionState.parsers.set('readline', lineParser); } // Setup binary parser if specified if (options.encoding === 'binary' && options.bufferSize) { const binaryParser = port.pipe( new ByteLengthParser({ length: options.bufferSize, }) ); binaryParser.on('data', (data: Buffer) => { this.handleBinaryData(sessionId, data); }); connectionState.parsers.set('binary', binaryParser); } } /** * Apply DTR/RTS signal settings */ private async applySignalSettings(sessionId: string): Promise<void> { const connectionState = this.connections.get(sessionId); if (!connectionState) return; const { port, options } = connectionState; try { if (options.dtr !== undefined) { await new Promise<void>((resolve, reject) => { port.set({ dtr: options.dtr }, (error) => { if (error) reject(error); else resolve(); }); }); } if (options.rts !== undefined) { await new Promise<void>((resolve, reject) => { port.set({ rts: options.rts }, (error) => { if (error) reject(error); else resolve(); }); }); } this.logger.debug(`Applied signal settings for session ${sessionId}`, { dtr: options.dtr, rts: options.rts, }); } catch (error) { this.logger.error( `Failed to apply signal settings for session ${sessionId}:`, error ); } } /** * Perform device reset sequence (e.g., Arduino reset) */ async performDeviceReset(sessionId: string): Promise<void> { const connectionState = this.connections.get(sessionId); if (!connectionState) return; const { port, options, deviceInfo } = connectionState; const profile = deviceInfo ? this.getProfileForDeviceType(deviceInfo.deviceType) : undefined; const resetSequence = profile?.resetSequence; if (!resetSequence) { this.logger.debug(`No reset sequence defined for session ${sessionId}`); return; } try { this.logger.info(`Performing device reset for session ${sessionId}`); // Apply reset sequence await new Promise<void>((resolve, reject) => { port.set( { dtr: resetSequence.dtr || false, rts: resetSequence.rts || false, }, (error) => { if (error) reject(error); else resolve(); } ); }); // Wait for pulse width await new Promise((resolve) => setTimeout(resolve, resetSequence.pulseWidth || 50) ); // Release reset await new Promise<void>((resolve, reject) => { port.set( { dtr: !resetSequence.dtr, rts: !resetSequence.rts, }, (error) => { if (error) reject(error); else resolve(); } ); }); // Wait for reset delay await new Promise((resolve) => setTimeout(resolve, resetSequence.delay || 100) ); this.logger.info(`Device reset completed for session ${sessionId}`); } catch (error) { this.logger.error(`Device reset failed for session ${sessionId}:`, error); throw error; } } /** * Handle incoming serial data */ private handleSerialData(sessionId: string, data: Buffer): void { const connectionState = this.connections.get(sessionId); if (!connectionState) return; connectionState.lastActivity = new Date(); // Store raw data const output: ConsoleOutput = { sessionId, type: 'stdout', data: data.toString( (connectionState.options.encoding as BufferEncoding) || 'utf8' ), timestamp: new Date(), raw: data.toString('hex'), }; connectionState.outputBuffer.push(output); // Add to BaseProtocol output buffer for session management this.addToOutputBuffer(sessionId, output); } /** * Handle parsed line data */ private handleParsedLine(sessionId: string, line: string): void { const connectionState = this.connections.get(sessionId); if (!connectionState) return; // Clean line of ANSI escape codes const cleanLine = stripAnsi(line); const output: ConsoleOutput = { sessionId, type: 'stdout', data: cleanLine, timestamp: new Date(), }; this.emit('line', output); // Check for bootloader patterns if device is in reset state const profile = connectionState.deviceInfo ? this.getProfileForDeviceType(connectionState.deviceInfo.deviceType) : undefined; if (profile?.bootloaderDetection) { const patterns = profile.bootloaderDetection.patterns; if (patterns.some((pattern) => pattern.test(cleanLine))) { this.emit('bootloader_detected', { sessionId, line: cleanLine }); } } } /** * Handle binary data */ private handleBinaryData(sessionId: string, data: Buffer): void { const output: ConsoleOutput = { sessionId, type: 'stdout', data: data.toString('hex'), timestamp: new Date(), raw: data.toString('hex'), }; this.emit('binary_data', output); } /** * Send data to serial device */ async sendData(sessionId: string, data: string | Buffer): Promise<void> { const connectionState = this.connections.get(sessionId); if (!connectionState || !connectionState.isConnected) { throw new Error(`No active serial connection for session ${sessionId}`); } const { port, options } = connectionState; try { let dataToSend: Buffer; if (typeof data === 'string') { // Add line ending if specified const dataWithEnding = options.lineEnding ? data + options.lineEnding : data; dataToSend = Buffer.from( dataWithEnding, (options.encoding as BufferEncoding) || 'utf8' ); } else { dataToSend = data; } await new Promise<void>((resolve, reject) => { port.write(dataToSend, (error) => { if (error) reject(error); else resolve(); }); }); connectionState.lastActivity = new Date(); this.logger.debug(`Sent data to session ${sessionId}:`, { length: dataToSend.length, type: typeof data, }); } catch (error) { this.logger.error(`Failed to send data to session ${sessionId}:`, error); throw error; } } /** * Close serial connection */ async closeConnection(sessionId: string): Promise<void> { const connectionState = this.connections.get(sessionId); if (!connectionState) return; // Cancel any pending reconnection const reconnectTimer = this.reconnectTimers.get(sessionId); if (reconnectTimer) { clearTimeout(reconnectTimer); this.reconnectTimers.delete(sessionId); } try { if (connectionState.port.isOpen) { await new Promise<void>((resolve) => { connectionState.port.close(() => resolve()); }); } this.connections.delete(sessionId); this.logger.info(`Serial connection closed for session ${sessionId}`); } catch (error) { this.logger.error( `Error closing serial connection for session ${sessionId}:`, error ); } } /** * Schedule reconnection attempt */ private scheduleReconnect(sessionId: string): void { const connectionState = this.connections.get(sessionId); if (!connectionState) return; const maxAttempts = connectionState.options.maxReconnectAttempts || 5; if (connectionState.reconnectAttempts >= maxAttempts) { this.logger.warn( `Max reconnection attempts reached for session ${sessionId}` ); return; } const delay = (connectionState.options.reconnectDelay || 1000) * Math.pow(2, connectionState.reconnectAttempts); // Exponential backoff this.logger.info( `Scheduling reconnection for session ${sessionId} in ${delay}ms` ); const timer = setTimeout(async () => { try { connectionState.reconnectAttempts++; await this.reconnectSession(sessionId); } catch (error) { this.logger.error( `Reconnection failed for session ${sessionId}:`, error ); this.scheduleReconnect(sessionId); // Schedule another attempt } }, delay); this.reconnectTimers.set(sessionId, timer); } /** * Attempt to reconnect a session */ private async reconnectSession(sessionId: string): Promise<void> { const connectionState = this.connections.get(sessionId); if (!connectionState) return; this.logger.info(`Attempting to reconnect session ${sessionId}`); try { // Close existing connection if still open if (connectionState.port.isOpen) { await new Promise<void>((resolve) => { connectionState.port.close(() => resolve()); }); } // Create new connection with same options await this.createConnection(sessionId, connectionState.options); this.logger.info(`Successfully reconnected session ${sessionId}`); } catch (error) { this.logger.error( `Reconnection attempt failed for session ${sessionId}:`, error ); throw error; } } /** * Get connection status */ getConnectionStatus(sessionId: string): SerialConnectionStatus | undefined { const connectionState = this.connections.get(sessionId); if (!connectionState) return undefined; return { sessionId, isConnected: connectionState.isConnected, path: connectionState.options.path, baudRate: connectionState.options.baudRate || 9600, deviceType: connectionState.deviceInfo?.deviceType || 'generic', lastActivity: connectionState.lastActivity, reconnectAttempts: connectionState.reconnectAttempts, outputBufferSize: connectionState.outputBuffer.length, }; } /** * Get output buffer for a session */ getOutputBuffer(sessionId: string, limit?: number): ConsoleOutput[] { const connectionState = this.connections.get(sessionId); if (!connectionState) return []; const buffer = connectionState.outputBuffer; return limit ? buffer.slice(-limit) : buffer; } /** * Clear output buffer for a session */ clearOutputBuffer(sessionId: string): void { const connectionState = this.connections.get(sessionId); if (connectionState) { connectionState.outputBuffer = []; } } /** * Start monitoring for device connections/disconnections */ private startDeviceMonitoring(): void { // Monitor device list changes every 5 seconds setInterval(async () => { try { const devices = await this.discoverDevices(); this.emit('device_list_updated', devices); } catch (error) { this.logger.error('Device monitoring error:', error); } }, 5000); } /** * Cleanup all connections */ async cleanup(): Promise<void> { this.logger.info('Cleaning up all serial connections'); const cleanupPromises = Array.from(this.connections.keys()).map( (sessionId) => this.closeConnection(sessionId) ); await Promise.allSettled(cleanupPromises); // Clear all reconnect timers this.reconnectTimers.forEach((timer) => { clearTimeout(timer); }); this.reconnectTimers.clear(); } // IProtocol required methods async initialize(): Promise<void> { if (this.isInitialized) return; this.logger.info( 'Initializing Serial protocol with session management fixes' ); this.isInitialized = true; } async createSession(options: SessionOptions): Promise<ConsoleSession> { if (!this.isInitialized) { await this.initialize(); } const sessionId = `serial-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; // Use session management fixes from BaseProtocol return await this.createSessionWithTypeDetection(sessionId, options); } protected async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { if (!options.serialOptions) { throw new Error('Serial options are required'); } try { // For persistent sessions, connect immediately // For one-shot sessions, connect on command execution if (!sessionState.isOneShot) { await this.createConnection(sessionId, options.serialOptions); } const session: ConsoleSession = { id: sessionId, command: options.command || '', args: options.args || [], cwd: '/', env: options.env || {}, createdAt: new Date(), status: sessionState.isOneShot ? 'initializing' : 'running', type: this.type, streaming: options.streaming ?? true, serialOptions: options.serialOptions, executionState: 'idle', activeCommands: new Map(), }; this.sessions.set(sessionId, session); this.outputBuffers.set(sessionId, []); this.logger.info( `Serial session ${sessionId} created (${sessionState.isOneShot ? 'one-shot' : 'persistent'})` ); return session; } catch (error) { this.logger.error(`Failed to create Serial session: ${error}`); throw error; } } async executeCommand( sessionId: string, command: string, args?: string[] ): Promise<void> { const sessionState = await this.getSessionState(sessionId); // For one-shot sessions, connect first if not already connected if (sessionState.isOneShot) { const connection = this.connections.get(sessionId); if (!connection || !connection.isConnected) { const session = this.sessions.get(sessionId); if (session?.serialOptions) { await this.createConnection(sessionId, session.serialOptions); } } } const fullCommand = args ? `${command} ${args.join(' ')}` : command; await this.sendData(sessionId, fullCommand + '\n'); // For one-shot sessions, mark as complete after command is sent if (sessionState.isOneShot) { setTimeout(() => { this.markSessionComplete(sessionId, 0); }, 1000); // Give time for output to be captured } } async sendInput(sessionId: string, input: string): Promise<void> { await this.sendData(sessionId, input); } async closeSession(sessionId: string): Promise<void> { await this.closeConnection(sessionId); // Remove from base class tracking this.sessions.delete(sessionId); this.outputBuffers.delete(sessionId); } async getHealthStatus(): Promise<ProtocolHealthStatus> { const now = new Date(); const errors: string[] = []; // Check for any connection errors Array.from(this.connections.entries()).forEach( ([sessionId, connection]) => { if (!connection.isConnected) { errors.push(`Connection ${sessionId} is not connected`); } } ); return { isHealthy: errors.length === 0, lastChecked: now, errors, warnings: [], metrics: { activeSessions: this.connections.size, totalSessions: this.connections.size, averageLatency: 0, successRate: errors.length === 0 ? 1.0 : 0.5, uptime: Date.now() - (this.isInitialized ? 0 : Date.now()), }, dependencies: { serialport: { available: true, version: '12.x', }, }, }; } async dispose(): Promise<void> { await this.cleanup(); await super.cleanup(); } } /** * Internal connection state interface */ interface SerialConnectionState { sessionId: string; port: SerialPort; options: SerialConnectionOptions; isConnected: boolean; reconnectAttempts: number; lastActivity: Date; outputBuffer: ConsoleOutput[]; parsers: Map<string, any>; deviceInfo?: SerialDeviceInfo; } /** * Connection status interface */ export interface SerialConnectionStatus { sessionId: string; isConnected: boolean; path: string; baudRate: number; deviceType: string; lastActivity: Date; reconnectAttempts: number; outputBufferSize: number; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ooples/mcp-console-automation'

If you have feedback or need assistance with the MCP directory API, please join our Discord server