UNPKG

@etm-professional-control/winccoa-mcp-server

Version:

MCP Server for WinCC OA with field-specific configurations

336 lines 15.5 kB
/** * Base Connection Class * * Abstract base class for all driver connections (OPC UA, S7Plus, BACnet, etc.). * Provides shared functionality and enforces common patterns. */ import { WinccoaManager } from 'winccoa-manager'; /** * Abstract base class for driver connections * * All driver-specific connection classes must extend this class. * Provides common helper methods for datapoint management and validation. */ export class BaseConnection { constructor() { this.winccoa = new WinccoaManager(); } /** * Protected method to set address configuration (common implementation) * Sets all address fields * * @param dpName - Full datapoint element name (e.g., 'MyDP.Value') * @param config - Address configuration * @returns true on success, false on failure */ async setAddressConfig(dpName, config) { try { // Build the address configuration datapoints const dpes = [ `${dpName}:_address.._type`, `${dpName}:_address.._drv_ident`, `${dpName}:_address.._connection`, `${dpName}:_address.._reference`, `${dpName}:_address.._internal`, `${dpName}:_address.._direction`, `${dpName}:_address.._datatype`, `${dpName}:_address.._subindex` ]; const values = [ config._type, config._drv_ident, config._connection, config._reference, config._internal, config._direction, config._datatype, config._subindex ]; // Add optional fields if provided if (config._lowlevel !== undefined) { dpes.push(`${dpName}:_address.._lowlevel`); values.push(config._lowlevel); } if (config._offset !== undefined) { dpes.push(`${dpName}:_address.._offset`); values.push(config._offset); } if (config._poll_group !== undefined) { dpes.push(`${dpName}:_address.._poll_group`); values.push(config._poll_group); } // Note: _active is typically set separately via dpSet after address config if (config._active !== undefined) { dpes.push(`${dpName}:_address.._active`); values.push(config._active); } console.log(`\n========================================`); console.log(`Setting address config for ${dpName}:`); console.log(`========================================`); console.log(`- Type: ${config._type}`); console.log(`- Driver: ${config._drv_ident}`); console.log(`- Connection: ${config._connection}`); console.log(`- Reference: ${config._reference}`); console.log(`- Datatype: ${config._datatype} (CRITICAL FIELD!)`); console.log(`- Direction: ${config._direction}`); console.log(`- Subindex: ${config._subindex}`); console.log(`- Internal: ${config._internal}`); if (config._lowlevel !== undefined) { console.log(`- Lowlevel: ${config._lowlevel}`); } if (config._offset !== undefined) { console.log(`- Offset: ${config._offset}`); } if (config._poll_group !== undefined) { console.log(`- Poll Group: ${config._poll_group}`); } if (config._active !== undefined) { console.log(`- Active: ${config._active}`); } console.log(`\nExecuting dpSetWait with ${dpes.length} attributes:`); for (let i = 0; i < dpes.length; i++) { console.log(` [${i}] ${dpes[i]} = ${JSON.stringify(values[i])}`); } console.log(`========================================\n`); // Apply configuration using dpSetWait for synchronous execution await this.winccoa.dpSetWait(dpes, values); console.log(`Successfully configured address for ${dpName}`); return true; } catch (error) { console.error(`Error configuring address for ${dpName}:`, error); return false; } } /** * Protected method to set BOTH address and distribution configs in a single atomic operation * CRITICAL: WinCC OA requires _address and _distrib to be set together atomically! * * @param dpName - Full datapoint element name (e.g., 'MyDP.Value') * @param addressConfig - Address configuration * @param distribConfig - Distribution configuration * @returns true on success, false on failure */ async setAddressAndDistribConfig(dpName, addressConfig, distribConfig) { try { // Build COMBINED datapoint list for BOTH _distrib AND _address // CRITICAL: Must set _distrib first, then _address (as per reference implementation) const dpes = [ // _distrib config first `${dpName}:_distrib.._type`, `${dpName}:_distrib.._driver`, // _address config `${dpName}:_address.._type`, `${dpName}:_address.._drv_ident`, `${dpName}:_address.._reference`, `${dpName}:_address.._direction`, `${dpName}:_address.._datatype`, `${dpName}:_address.._subindex`, `${dpName}:_address.._internal` ]; const values = [ // _distrib values distribConfig._type, distribConfig._driver, // _address values addressConfig._type, addressConfig._drv_ident, addressConfig._reference, addressConfig._direction, addressConfig._datatype, addressConfig._subindex, addressConfig._internal ]; // Add optional address fields if provided if (addressConfig._connection !== undefined) { dpes.push(`${dpName}:_address.._connection`); values.push(addressConfig._connection); } if (addressConfig._lowlevel !== undefined) { dpes.push(`${dpName}:_address.._lowlevel`); values.push(addressConfig._lowlevel); } if (addressConfig._offset !== undefined) { dpes.push(`${dpName}:_address.._offset`); values.push(addressConfig._offset); } if (addressConfig._poll_group !== undefined) { dpes.push(`${dpName}:_address.._poll_group`); values.push(addressConfig._poll_group); } if (addressConfig._active !== undefined) { dpes.push(`${dpName}:_address.._active`); values.push(addressConfig._active); } console.log(`\n╔════════════════════════════════════════════════════════════════╗`); console.log(`║ ATOMIC CONFIG: Setting _distrib + _address for ${dpName.padEnd(17)} ║`); console.log(`╠════════════════════════════════════════════════════════════════╣`); console.log(`║ _distrib config: ║`); console.log(` - Type: ${distribConfig._type}`); console.log(` - Driver: ${distribConfig._driver}`); console.log(`╠════════════════════════════════════════════════════════════════╣`); console.log(`║ _address config: ║`); console.log(` - Type: ${addressConfig._type}`); console.log(` - Driver: ${addressConfig._drv_ident}`); if (addressConfig._connection !== undefined) { console.log(` - Connection: ${addressConfig._connection}`); } console.log(` - Reference: ${addressConfig._reference}`); console.log(` - Datatype: ${addressConfig._datatype} ⚠️ CRITICAL FIELD!`); console.log(` - Direction: ${addressConfig._direction}`); console.log(` - Subindex: ${addressConfig._subindex}`); console.log(` - Internal: ${addressConfig._internal}`); if (addressConfig._lowlevel !== undefined) { console.log(` - Lowlevel: ${addressConfig._lowlevel}`); } if (addressConfig._offset !== undefined) { console.log(` - Offset: ${addressConfig._offset}`); } if (addressConfig._poll_group !== undefined) { console.log(` - Poll Group: ${addressConfig._poll_group}`); } if (addressConfig._active !== undefined) { console.log(` - Active: ${addressConfig._active}`); } console.log(`╠════════════════════════════════════════════════════════════════╣`); console.log(`║ Executing SINGLE ATOMIC dpSetWait with ${dpes.length} attributes: ║`); console.log(`╚════════════════════════════════════════════════════════════════╝`); for (let i = 0; i < dpes.length; i++) { console.log(` [${i.toString().padStart(2)}] ${dpes[i]} = ${JSON.stringify(values[i])}`); } console.log(`════════════════════════════════════════════════════════════════\n`); // Apply BOTH configurations using a SINGLE dpSetWait call (ATOMIC OPERATION) await this.winccoa.dpSetWait(dpes, values); console.log(`✓ Successfully configured _distrib + _address atomically for ${dpName}\n`); return true; } catch (error) { console.error(`✗ Error configuring _distrib + _address for ${dpName}:`, error); return false; } } /** * Protected method to set distribution config (manager allocation) * This is a separate config parallel to _address * * @param dpName - Full datapoint element name (e.g., 'MyDP.Value') * @param config - Distribution configuration * @returns true on success, false on failure */ async setDistribConfig(dpName, config) { try { const dpes = [ `${dpName}:_distrib.._type`, `${dpName}:_distrib.._driver` ]; const values = [ config._type, config._driver ]; console.log(`\n========================================`); console.log(`Setting distrib config for ${dpName}:`); console.log(`========================================`); console.log(`- Type: ${config._type}`); console.log(`- Driver: ${config._driver}`); console.log(`\nExecuting dpSetWait with ${dpes.length} attributes:`); for (let i = 0; i < dpes.length; i++) { console.log(` [${i}] ${dpes[i]} = ${JSON.stringify(values[i])}`); } console.log(`========================================\n`); // Apply configuration using dpSetWait for synchronous execution await this.winccoa.dpSetWait(dpes, values); console.log(`Successfully configured distrib for ${dpName}`); return true; } catch (error) { console.error(`Error configuring distrib for ${dpName}:`, error); return false; } } /** * Check if a datapoint exists * @param dpName - Name of the datapoint * @returns true if datapoint exists */ checkDpExists(dpName) { try { return this.winccoa.dpExists(dpName); } catch (error) { console.error(`Error checking if datapoint ${dpName} exists:`, error); return false; } } /** * Ensure that a connection datapoint exists, create if necessary * @param dpName - Name of the datapoint * @param dpType - Type of the datapoint (e.g., '_OPCUAServer', '_S7PlusConnection') * @returns true if datapoint exists or was created successfully */ async ensureConnectionDpExists(dpName, dpType) { try { if (this.checkDpExists(dpName)) { console.log(`Connection datapoint ${dpName} already exists`); return true; } console.log(`Creating connection datapoint ${dpName} of type ${dpType}`); const created = await this.winccoa.dpCreate(dpName, dpType); if (!created) { console.error(`Failed to create connection datapoint ${dpName}`); return false; } console.log(`Successfully created connection datapoint ${dpName}`); return true; } catch (error) { console.error(`Error creating connection datapoint ${dpName}:`, error); return false; } } /** * Validate IP address or hostname * @param ipAddress - IP address or hostname to validate * @returns true if valid */ validateIpAddress(ipAddress) { if (!ipAddress || ipAddress.trim() === '') { return false; } const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?)*$/; return ipRegex.test(ipAddress) || hostnameRegex.test(ipAddress); } /** * Validate port number * @param port - Port number to validate * @returns true if valid (1-65535) */ validatePort(port) { return port >= 1 && port <= 65535; } /** * Validate manager/driver number * @param num - Manager number to validate * @returns true if valid (1-99) */ validateManagerNumber(num) { return num >= 1 && num <= 99; } /** * Generate a unique connection name with a given prefix * @param prefix - Prefix for the connection name (e.g., '_OpcUAConnection', '_S7Connection') * @returns Unique connection name */ async generateConnectionName(prefix) { let counter = 1; let dpName = `${prefix}${counter}`; // Find the next free number while (this.checkDpExists(dpName)) { counter++; dpName = `${prefix}${counter}`; } console.log(`Generated connection name: ${dpName}`); return dpName; } } //# sourceMappingURL=BaseConnection.js.map