361 lines
10 KiB
JavaScript
361 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Data Seeding Script for ModPulse
|
|
* Populates the system with realistic PLC/SCADA data
|
|
* Scenario: Food processing plant
|
|
*/
|
|
|
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:5000';
|
|
const API_KEY = process.env.MODPULSE_API_KEY || 'dev-api-key-for-local-testing';
|
|
|
|
async function api(path, method = 'GET', body = null) {
|
|
const url = `${BASE_URL}${path}`;
|
|
const options = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': API_KEY
|
|
}
|
|
};
|
|
if (body) options.body = JSON.stringify(body);
|
|
|
|
const res = await fetch(url, options);
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(`API ${method} ${path} failed: ${res.status} ${text}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
async function seed() {
|
|
console.log('🌱 Seeding ModPulse with realistic industrial data...\n');
|
|
|
|
// 1. CREATE MASTERS (Modbus connections)
|
|
console.log('📡 Creating Modbus Masters...');
|
|
|
|
const masters = [
|
|
{
|
|
name: 'Line-A-Main-PLC',
|
|
description: 'Production Line A - Allen-Bradley ControlLogix controlling mixing and packaging',
|
|
type: 'TCP',
|
|
connectionString: '192.168.10.10:502',
|
|
pollIntervalMs: 1000,
|
|
timeoutMs: 5000,
|
|
isActive: true
|
|
},
|
|
{
|
|
name: 'Line-B-Main-PLC',
|
|
description: 'Production Line B - Siemens S7-1500 controlling filling and capping',
|
|
type: 'TCP',
|
|
connectionString: '192.168.10.20:502',
|
|
pollIntervalMs: 1000,
|
|
timeoutMs: 5000,
|
|
isActive: true
|
|
},
|
|
{
|
|
name: 'Cold-Storage-Gateway',
|
|
description: 'Moxa MGate MB3170 - 8 temperature sensors in cold storage',
|
|
type: 'TCP',
|
|
connectionString: '192.168.10.50:502',
|
|
pollIntervalMs: 2000,
|
|
timeoutMs: 3000,
|
|
isActive: true
|
|
},
|
|
{
|
|
name: 'Boiler-Control-PLC',
|
|
description: 'Honeywell MasterLogic - Steam boiler pressure/temperature control',
|
|
type: 'TCP',
|
|
connectionString: '192.168.10.100:502',
|
|
pollIntervalMs: 500,
|
|
timeoutMs: 2000,
|
|
isActive: true
|
|
}
|
|
];
|
|
|
|
const createdMasters = [];
|
|
for (const m of masters) {
|
|
const result = await api('/api/masters', 'POST', m);
|
|
createdMasters.push(result);
|
|
console.log(` ✓ Master: ${m.name} (ID: ${result.id})`);
|
|
}
|
|
|
|
// 2. CREATE SLAVES (Field Devices)
|
|
console.log('\n🔌 Creating Field Devices (Slaves)...');
|
|
|
|
const slaves = [
|
|
// Line A devices
|
|
{ masterId: createdMasters[0].id, slaveId: 1, label: 'Mixing-Tank-VFD-01', isActive: true },
|
|
{ masterId: createdMasters[0].id, slaveId: 2, label: 'Transfer-Pump-01', isActive: true },
|
|
// Line B devices
|
|
{ masterId: createdMasters[1].id, slaveId: 10, label: 'Filler-Machine-01', isActive: true },
|
|
{ masterId: createdMasters[1].id, slaveId: 11, label: 'Capper-Machine-01', isActive: true },
|
|
// Cold storage devices
|
|
{ masterId: createdMasters[2].id, slaveId: 1, label: 'Cold-Room-01-Temp', isActive: true },
|
|
{ masterId: createdMasters[2].id, slaveId: 2, label: 'Cold-Room-02-Temp', isActive: true },
|
|
// Boiler devices
|
|
{ masterId: createdMasters[3].id, slaveId: 1, label: 'Steam-Pressure-PT101', isActive: true },
|
|
{ masterId: createdMasters[3].id, slaveId: 2, label: 'Steam-Temp-TT101', isActive: true },
|
|
{ masterId: createdMasters[3].id, slaveId: 3, label: 'Boiler-High-Level-LSH101', isActive: true }
|
|
];
|
|
|
|
const createdSlaves = [];
|
|
for (const s of slaves) {
|
|
const result = await api('/api/slaves', 'POST', s);
|
|
createdSlaves.push(result);
|
|
console.log(` ✓ Slave: ${s.label} (ID: ${result.id})`);
|
|
}
|
|
|
|
// 3. CREATE REGISTER MAPS (Data points)
|
|
console.log('\n📊 Creating Register Maps...');
|
|
|
|
const registerMaps = [
|
|
// VFD - Analog values
|
|
{
|
|
slaveId: createdSlaves[0].id,
|
|
name: 'VFD-01-Speed-Feedback',
|
|
description: 'Motor speed from VFD encoder',
|
|
functionCode: 3, // Holding Registers
|
|
startAddress: 40001,
|
|
registerCount: 1,
|
|
dataType: 5, // Float32
|
|
scaleFactor: 0.1,
|
|
unit: 'RPM',
|
|
logDeadband: 1.0,
|
|
maxLogIntervalSec: 60,
|
|
isActive: true
|
|
},
|
|
{
|
|
slaveId: createdSlaves[0].id,
|
|
name: 'VFD-01-Motor-Current',
|
|
description: 'Motor current for overload monitoring',
|
|
functionCode: 3,
|
|
startAddress: 40002,
|
|
registerCount: 1,
|
|
dataType: 1, // UInt16
|
|
scaleFactor: 0.01,
|
|
unit: 'A',
|
|
logDeadband: 0.5,
|
|
maxLogIntervalSec: 30,
|
|
isActive: true
|
|
},
|
|
// Cold storage - Temperature
|
|
{
|
|
slaveId: createdSlaves[4].id,
|
|
name: 'Cold-Room-01-Temp',
|
|
description: 'Cold storage room 1 temperature',
|
|
functionCode: 4, // Input Registers
|
|
startAddress: 30001,
|
|
registerCount: 1,
|
|
dataType: 2, // Int16
|
|
scaleFactor: 0.01,
|
|
unit: '°C',
|
|
logDeadband: 0.1,
|
|
maxLogIntervalSec: 10,
|
|
isActive: true
|
|
},
|
|
{
|
|
slaveId: createdSlaves[5].id,
|
|
name: 'Cold-Room-02-Temp',
|
|
description: 'Cold storage room 2 temperature',
|
|
functionCode: 4,
|
|
startAddress: 30001,
|
|
registerCount: 1,
|
|
dataType: 2,
|
|
scaleFactor: 0.01,
|
|
unit: '°C',
|
|
logDeadband: 0.1,
|
|
maxLogIntervalSec: 10,
|
|
isActive: true
|
|
},
|
|
// Boiler - Steam pressure
|
|
{
|
|
slaveId: createdSlaves[6].id,
|
|
name: 'Steam-Pressure-PT101',
|
|
description: 'Steam header pressure',
|
|
functionCode: 4,
|
|
startAddress: 30001,
|
|
registerCount: 1,
|
|
dataType: 1, // UInt16
|
|
scaleFactor: 0.001,
|
|
unit: 'psi',
|
|
logDeadband: 0.5,
|
|
maxLogIntervalSec: 5,
|
|
isActive: true
|
|
},
|
|
// Boiler - Steam temperature
|
|
{
|
|
slaveId: createdSlaves[7].id,
|
|
name: 'Steam-Temp-TT101',
|
|
description: 'Superheated steam temperature',
|
|
functionCode: 4,
|
|
startAddress: 30001,
|
|
registerCount: 1,
|
|
dataType: 1,
|
|
scaleFactor: 0.1,
|
|
unit: '°C',
|
|
logDeadband: 0.5,
|
|
maxLogIntervalSec: 5,
|
|
isActive: true
|
|
},
|
|
// Production counter
|
|
{
|
|
slaveId: createdSlaves[2].id,
|
|
name: 'Filler-Bottle-Count',
|
|
description: 'Bottles filled since last reset',
|
|
functionCode: 3,
|
|
startAddress: 40010,
|
|
registerCount: 1,
|
|
dataType: 1,
|
|
scaleFactor: 1,
|
|
unit: 'bottles',
|
|
logDeadband: 0,
|
|
maxLogIntervalSec: 0,
|
|
isActive: true
|
|
},
|
|
// Digital values - Coils
|
|
{
|
|
slaveId: createdSlaves[0].id,
|
|
name: 'VFD-01-Running',
|
|
description: 'Motor running status',
|
|
functionCode: 1, // Coils
|
|
startAddress: 1,
|
|
registerCount: 1,
|
|
dataType: 0, // Bool
|
|
scaleFactor: 1,
|
|
unit: '',
|
|
logDeadband: 0,
|
|
maxLogIntervalSec: 0,
|
|
isActive: true
|
|
},
|
|
{
|
|
slaveId: createdSlaves[0].id,
|
|
name: 'VFD-01-Fault',
|
|
description: 'VFD fault/alarm status',
|
|
functionCode: 1,
|
|
startAddress: 2,
|
|
registerCount: 1,
|
|
dataType: 0,
|
|
scaleFactor: 1,
|
|
unit: '',
|
|
logDeadband: 0,
|
|
maxLogIntervalSec: 0,
|
|
isActive: true
|
|
},
|
|
{
|
|
slaveId: createdSlaves[8].id,
|
|
name: 'Boiler-High-Level',
|
|
description: 'High level safety interlock',
|
|
functionCode: 2, // Discrete Inputs
|
|
startAddress: 10001,
|
|
registerCount: 1,
|
|
dataType: 0,
|
|
scaleFactor: 1,
|
|
unit: '',
|
|
logDeadband: 0,
|
|
maxLogIntervalSec: 0,
|
|
isActive: true
|
|
},
|
|
{
|
|
slaveId: createdSlaves[2].id,
|
|
name: 'Filler-Running',
|
|
description: 'Filler machine running',
|
|
functionCode: 1,
|
|
startAddress: 100,
|
|
registerCount: 1,
|
|
dataType: 0,
|
|
scaleFactor: 1,
|
|
unit: '',
|
|
logDeadband: 0,
|
|
maxLogIntervalSec: 0,
|
|
isActive: true
|
|
},
|
|
{
|
|
slaveId: createdSlaves[3].id,
|
|
name: 'Capper-Torque-OK',
|
|
description: 'Cap torque within spec',
|
|
functionCode: 1,
|
|
startAddress: 200,
|
|
registerCount: 1,
|
|
dataType: 0,
|
|
scaleFactor: 1,
|
|
unit: '',
|
|
logDeadband: 0,
|
|
maxLogIntervalSec: 0,
|
|
isActive: true
|
|
}
|
|
];
|
|
|
|
const createdMaps = [];
|
|
for (const m of registerMaps) {
|
|
const result = await api('/api/registermaps', 'POST', m);
|
|
createdMaps.push(result);
|
|
console.log(` ✓ Register: ${m.name} (ID: ${result.id})`);
|
|
}
|
|
|
|
// 4. CREATE RULES (Pulse Rules)
|
|
console.log('\n⚡ Creating Pulse Rules...');
|
|
|
|
const rules = [
|
|
// Conditional rules (alarms)
|
|
{
|
|
name: 'Cold-Room-Hi-Temp-Alarm',
|
|
slaveId: createdSlaves[4].id,
|
|
triggerType: 2, // Conditional
|
|
registerMapId: createdMaps[2].id, // Cold-Room-01-Temp
|
|
conditionExpression: 'value > 4.0',
|
|
targetAction: 'WriteSingleCoil',
|
|
targetParameters: '{"address":100,"value":1}',
|
|
isActive: true
|
|
},
|
|
{
|
|
name: 'Steam-Pressure-Hi-Alarm',
|
|
slaveId: createdSlaves[6].id,
|
|
triggerType: 2,
|
|
registerMapId: createdMaps[4].id, // Steam-Pressure
|
|
conditionExpression: 'value > 140',
|
|
targetAction: 'WriteSingleCoil',
|
|
targetParameters: '{"address":101,"value":1}',
|
|
isActive: true
|
|
},
|
|
{
|
|
name: 'VFD-Overload-Warning',
|
|
slaveId: createdSlaves[0].id,
|
|
triggerType: 2,
|
|
registerMapId: createdMaps[1].id, // VFD-Current
|
|
conditionExpression: 'value > 45',
|
|
targetAction: 'WriteSingleRegister',
|
|
targetParameters: '{"address":500,"value":1}',
|
|
isActive: true
|
|
},
|
|
// Scheduled maintenance reminder
|
|
{
|
|
name: 'Daily-Status-Report',
|
|
slaveId: createdSlaves[0].id,
|
|
triggerType: 1, // Cron
|
|
cronExpression: '0 6 * * *', // 6 AM daily
|
|
targetAction: 'WriteSingleRegister',
|
|
targetParameters: '{"address":999,"value":1}',
|
|
isActive: true
|
|
}
|
|
];
|
|
|
|
const createdRules = [];
|
|
for (const r of rules) {
|
|
const result = await api('/api/rules', 'POST', r);
|
|
createdRules.push(result);
|
|
console.log(` ✓ Rule: ${r.name} (ID: ${result.id})`);
|
|
}
|
|
|
|
// Summary
|
|
console.log('\n✅ Data seeding complete!');
|
|
console.log(` ${createdMasters.length} Masters`);
|
|
console.log(` ${createdSlaves.length} Devices (Slaves)`);
|
|
console.log(` ${createdMaps.length} Register Maps`);
|
|
console.log(` ${createdRules.length} Pulse Rules`);
|
|
}
|
|
|
|
seed().catch(err => {
|
|
console.error('❌ Seeding failed:', err.message);
|
|
process.exit(1);
|
|
});
|