Production-grade Atlas Copco Open Protocol client for Node.js. Built for real manufacturing environments with robust error recovery, automatic reconnection, and comprehensive industrial safety interlocks.
Designed and tested in actual automotive and aerospace assembly lines in Pune, India.
- ✅ Full Open Protocol Support - MID 0001-0101 (Specification 2.8.0+)
- ✅ Production-Hardened - Handles TCP fragmentation, network glitches, firmware variations
- ✅ Automatic Reconnection - Exponential backoff with state recovery
- ✅ Safety Interlocks - Enforces controller-side safety rules (VIN, job, alarms)
- ✅ Multi-Spindle Tools - Auto-detection from MID 0061/0101
- ✅ VIN Traceability - Automotive-grade part tracking and locking
- ✅ Batch Manufacturing - Real-time progress tracking and completion events
- ✅ Alarm Handling - Subscribe, acknowledge, and recover from controller alarms
- ✅ Command Safety - One-per-MID enforcement prevents state corruption
- ✅ Event-Driven API - Perfect for OPC UA, MQTT, MES integration
- ✅ Zero Dependencies - Uses only Node.js core modules
npm install node-nutrunner-open-libraryRequirements: Node.js >= 14.0.0
const { OpenProtocolNutrunner } = require('node-nutrunner-open-library');
const nutrunner = new OpenProtocolNutrunner({
host: '192.168.1.100',
port: 4545,
autoReconnect: true
});
// Handle tightening results
nutrunner.on('tighteningCycleCompleted', ({ results, overallOk }) => {
console.log(`Tightening ${overallOk ? 'OK' : 'NOK'}`);
results.forEach(r => {
console.log(`Spindle ${r.spindle}: ${r.torque} Nm, ${r.angle}°`);
});
});
// Connect and setup
await nutrunner.connect();
await nutrunner.selectJob(123);
await nutrunner.enableTool();
console.log('Ready for tightening!');| Manufacturer | Models | Status |
|---|---|---|
| Atlas Copco | PowerFocus 4000/6000, PowerMACS | ✅ Tested |
| Stanley Assembly Technologies | Open Protocol compatible | ✅ Tested |
| Desoutter | CVI controllers | ✅ Tested |
| Ingersoll Rand | QX Series | ✅ Compatible |
const { OpenProtocolNutrunner } = require('node-nutrunner-open-library');
const nutrunner = new OpenProtocolNutrunner({
host: '192.168.1.100'
});
nutrunner.on('connected', () => {
console.log('✓ Connected to controller');
});
nutrunner.on('tighteningCycleStarted', () => {
console.log('⚙ Tightening started...');
});
nutrunner.on('spindleResult', (result) => {
console.log(`Spindle ${result.spindle}: ${result.ok ? '✓' : '✗'}`);
console.log(` Torque: ${result.torque} Nm`);
console.log(` Angle: ${result.angle}°`);
});
nutrunner.on('tighteningCycleCompleted', ({ results, overallOk, duration }) => {
console.log(`${overallOk ? '✓' : '✗'} Completed in ${duration}ms`);
});
await nutrunner.connect();const nutrunner = new OpenProtocolNutrunner({
host: '192.168.1.100',
spindleCount: 2 // Multi-spindle tool
});
nutrunner.on('linkEstablished', async () => {
// Download VIN for current product
await nutrunner.downloadVIN('1HGBH41JXMN109186');
await nutrunner.selectJob(101);
await nutrunner.enableTool();
console.log('✓ VIN downloaded - ready for tightening');
});
nutrunner.on('vinLocked', (vin) => {
console.log(`🔒 VIN locked for traceability: ${vin}`);
});
nutrunner.on('tighteningCycleCompleted', ({ results }) => {
const record = {
vin: nutrunner.getState().product.vin,
timestamp: new Date().toISOString(),
results: results
};
// Save to database for traceability
saveToDatabase(record);
});
await nutrunner.connect();const nutrunner = new OpenProtocolNutrunner({
host: '192.168.1.100'
});
nutrunner.on('batchStarted', (batch) => {
console.log(`📦 Batch ${batch.batchId} started (Size: ${batch.size})`);
});
nutrunner.on('batchProgress', ({ counter, size, remaining }) => {
const percent = Math.round((counter / size) * 100);
console.log(`Progress: ${counter}/${size} (${percent}%) - ${remaining} remaining`);
});
nutrunner.on('batchCompleted', (batch) => {
console.log(`✓ Batch ${batch.batchId} completed!`);
});
await nutrunner.connect();
await nutrunner.selectJob(5); // Job configured with batch size
await nutrunner.enableTool();const nutrunner = new OpenProtocolNutrunner({
host: '192.168.1.100'
});
nutrunner.on('alarm', (alarm) => {
console.error(`🚨 ALARM: [${alarm.alarmCode}] ${alarm.message}`);
// Auto-acknowledge specific alarms
if (shouldAutoAcknowledge(alarm.alarmCode)) {
setTimeout(() => {
nutrunner.acknowledgeAlarm();
}, 2000);
}
});
nutrunner.on('alarmStatus', ({ alarmStatus }) => {
if (!alarmStatus) {
console.log('✓ All alarms cleared - system ready');
}
});
function shouldAutoAcknowledge(code) {
const autoAckCodes = ['0001', '0010', '0015'];
return autoAckCodes.includes(code);
}
await nutrunner.connect();const fleet = [
{ id: 'Station-A', host: '192.168.1.100' },
{ id: 'Station-B', host: '192.168.1.101' },
{ id: 'Station-C', host: '192.168.1.102' }
];
const controllers = fleet.map(config => {
const nutrunner = new OpenProtocolNutrunner(config);
nutrunner.on('tighteningCycleCompleted', ({ overallOk }) => {
console.log(`[${config.id}] Tightening ${overallOk ? 'OK' : 'NOK'}`);
});
return { id: config.id, nutrunner };
});
// Connect all controllers
await Promise.all(controllers.map(c => c.nutrunner.connect()));
// Setup all stations
for (const { id, nutrunner } of controllers) {
await nutrunner.selectJob(100);
await nutrunner.enableTool();
console.log(`[${id}] Ready`);
}const nutrunner = new OpenProtocolNutrunner({
host: '192.168.1.100', // Required: Controller IP address
port: 4545, // Default: 4545
autoReconnect: true, // Default: true (exponential backoff)
validateFrames: true, // Default: true (frame corruption detection)
spindleCount: null, // Override for controllers without MID 101
allowDuplicateCommands: false // Default: false (enforces command safety)
});connected- TCP connection establisheddisconnected- Connection lostreconnecting- Reconnection attempt in progress (emits{ attempt, delay })linkEstablished- Open Protocol handshake complete (emits{ revision })
tighteningCycleStarted- Tool running detected (emits{ timestamp })spindleResult- Individual spindle result received (emits result object)tighteningCycleCompleted- All spindles completed (emits{ results, overallOk, duration })tighteningIncomplete- Watchdog timeout (emits{ expected, received, results })
commandAccepted- MID 0005 received (emits{ mid })commandError- MID 0004 received (emits{ failedMid, errorCode, message })commandTimeout- No response within 5s (emits{ mid, cmdId })
jobSelected- Job activated (emits{ jobId })vinLocked- VIN locked for traceability (emits VIN string)batchStarted- Batch production started (emits batch object)batchProgress- Batch counter updated (emits{ counter, size, remaining })batchCompleted- Batch size reached (emits batch object)alarm- Controller alarm raised (emits alarm object)alarmStatus- Alarm state changed (emits{ alarmStatus, currentAlarms })stateChanged- Any state change (emits full state snapshot)
The library enforces industrial safety interlocks before allowing tightening operations:
try {
nutrunner.startTightening();
} catch (err) {
if (err instanceof InterlockError) {
console.log(`Interlock: ${err.code} - ${err.message}`);
}
}Interlock Error Codes:
NOT_CONNECTED- No TCP connection to controllerLINK_NOT_READY- Protocol handshake not completeTOOL_DISABLED- Tool not enabled (send MID 0042)TOOL_RUNNING- Tightening already in progressCTRL_NOT_READY- Controller not readyALARM_ACTIVE- Active alarm must be acknowledgedVIN_REQUIRED- VIN required but not downloadedJOB_NOT_ACTIVE- No job selected
await nutrunner.connect() // Connect to controller
nutrunner.disconnect() // Disconnect gracefully
nutrunner.isConnected() // Check connection status
nutrunner.isReady() // Check if ready for tighteningawait nutrunner.selectJob(jobId) // Select job by ID
await nutrunner.downloadVIN(vin) // Download VIN (max 25 chars)
await nutrunner.selectParameterSet(paramSetId) // Select parameter set
await nutrunner.enableTool() // Enable tool
await nutrunner.disableTool() // Disable tool
await nutrunner.startTightening() // Start tightening (interlocks enforced)
await nutrunner.resetBatch() // Reset batch counter
await nutrunner.decrementBatch() // Decrement batch counternutrunner.subscribeTighteningResults() // Subscribe to MID 0061
nutrunner.unsubscribeTighteningResults() // Unsubscribe from MID 0061
nutrunner.subscribeAlarms() // Subscribe to MID 0070
nutrunner.unsubscribeAlarms() // Unsubscribe from MID 0070
nutrunner.acknowledgeAlarm() // Acknowledge active alarmnutrunner.setSpindleCount(count) // Manually set spindle count
nutrunner.getSpindleCount() // Get spindle count and source
nutrunner.getState() // Get full state snapshotconst { OPCUAServer, Variant, DataType } = require('node-opcua');
const opcuaServer = new OPCUAServer({ port: 4840 });
await opcuaServer.initialize();
const namespace = opcuaServer.engine.addressSpace.getOwnNamespace();
const lastTorqueVar = namespace.addVariable({
browseName: 'LastTorque',
dataType: 'Double',
value: { dataType: DataType.Double, value: 0.0 }
});
nutrunner.on('tighteningCycleCompleted', ({ results }) => {
lastTorqueVar.setValueFromSource({
dataType: DataType.Double,
value: results.torque
});
});
await opcuaServer.start();
await nutrunner.connect();const { InfluxDB, Point } = require('@influxdata/influxdb-client');
const influx = new InfluxDB({ url: 'http://localhost:8086', token: 'your-token' });
const writeApi = influx.getWriteApi('org', 'manufacturing');
nutrunner.on('tighteningCycleCompleted', ({ results, overallOk }) => {
results.forEach(r => {
const point = new Point('tightening')
.tag('spindle', r.spindle.toString())
.floatField('torque', r.torque)
.intField('angle', r.angle)
.booleanField('ok', r.ok);
writeApi.writePoint(point);
});
writeApi.flush();
});
await nutrunner.connect();const mqtt = require('mqtt');
const client = mqtt.connect('mqtt://localhost:1883');
nutrunner.on('tighteningCycleCompleted', ({ results, overallOk }) => {
const payload = JSON.stringify({
timestamp: new Date().toISOString(),
ok: overallOk,
results: results
});
client.publish('factory/station1/tightening', payload);
});
await nutrunner.connect();node-nutrunner-open-library/
├── index.js # Main library (production-grade)
├── examples/
│ ├── 01-basic-tightening.js # Simple tightening workflow
│ ├── 02-vin-traceability.js # Automotive VIN tracking
│ ├── 03-batch-manufacturing.js # Batch production
│ ├── 04-alarm-handling.js # Alarm management
│ ├── 05-fleet-management.js # Multi-controller setup
│ ├── 06-influxdb-integration.js # Time-series database
│ ├── 07-opcua-bridge.js # OPC UA server bridge
│ └── 08-error-recovery.js # Error handling patterns
├── LICENSE # Apache 2.0
├── README.md # This file
├── CHANGELOG.md # Version history
└── package.json
nutrunner.on('error', (err) => {
console.error('Connection error:', err.message);
});
nutrunner.on('reconnecting', ({ attempt, delay }) => {
console.log(`Reconnection attempt ${attempt} in ${delay}ms...`);
});nutrunner.on('frameError', ({ type, buffer }) => {
console.error(`Frame error: ${type}`);
// Network corruption detected - library auto-recovers
});nutrunner.on('commandTimeout', ({ mid, cmdId }) => {
console.error(`Command MID ${mid} timed out (ID: ${cmdId})`);
});nutrunner.on('tighteningIncomplete', ({ expected, received }) => {
console.error(`Watchdog: Expected ${expected} spindles, got ${received}`);
// Check controller configuration and network stability
});- Some units send MID 0002 instead of MID 0003 for comm start ACK (handled automatically)
- Controllers without MID 0101 support require manual spindle count:
const nutrunner = new OpenProtocolNutrunner({ host: '192.168.1.100', spindleCount: 2 // Set manually });
- MID 0061 field positions vary by firmware version
- Library uses status flags instead of hard-coded offsets (production-safe)
Apache License 2.0
Copyright (c) 2026 Bufferstack.IO Analytics Technology LLP
Copyright (c) 2026 Harshad Joshi
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Developed for real manufacturing environments in Pune, India. Built on lessons learned from production deployments in:
- Automotive assembly lines
- Aerospace component manufacturing
- Heavy equipment production
Protocol Reference: Atlas Copco Open Protocol Specification v2.xx.yy+
Contributions welcome! Areas needing help:
- Additional MID implementations (parameter sets, multi-stage results, graphs)
- TypeScript type definitions
- Controller-specific quirks documentation
- More integration examples (PostgreSQL, Kafka, etc.)
- 🐛 Report Issues
- 💬 Discussions
- 📧 Email: harshad@bufferstack.io
If this library helps your manufacturing operations, please ⭐ star it on GitHub!
Made with ❤️ for the industrial automation community
Tested in production since 2026 | Zero dependencies | Production-grade reliability