Initial commit with Dockerfile and demo code

This commit is contained in:
2026-08-28 18:16:30 +05:30
commit cf2575fd4e
53 changed files with 11602 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
/**
* OPC-UA telemetry source - DOCUMENTED STUB.
*
* OPC-UA is usually the right answer when the customer already has a PLC or SCADA
* layer, because it gives you a browsable address space, real subscriptions, and
* server-side timestamps rather than a flat topic namespace.
*
* To implement:
* 1. npm i node-opcua
* 2. Fill NODE_MAP with the customer's actual NodeIds (browse the server first).
* 3. Implement start() as marked below.
* 4. Set TELEMETRY_SOURCE=opcua and OPCUA_ENDPOINT in .env
*
* For testing without a plant, Prosys OPC-UA Simulation Server or the
* node-opcua sample server both work locally.
*/
import { TelemetrySource } from './source.js';
import { AnalyticsEngine } from '../analytics/alarms.js';
/**
* Maps an OPC-UA NodeId to a station signal.
*
* NodeIds are namespace-qualified and installation-specific - never guess them.
* Browse the server's address space and read them off.
*/
export const NODE_MAP = {
// 'ns=2;s=Line1.CONV01.BeltSpeed': { station: 'CONV-01', signal: 'beltSpeed' },
// 'ns=2;s=Line1.CNC02.VibrationRMS': { station: 'CNC-02', signal: 'vibration' },
// 'ns=2;s=Line1.CNC02.SpindleLoad': { station: 'CNC-02', signal: 'spindleLoad' },
// 'ns=2;s=Line1.OVN03.Zone2PV': { station: 'OVN-03', signal: 'zone2Temp' },
// 'ns=2;s=Line1.OVN03.Zone2SP': { station: 'OVN-03', signal: 'setpoint' },
// 'ns=2;s=Line1.INS04.RejectRate': { station: 'INS-04', signal: 'rejectRate' },
// 'ns=2;s=Line1.PKG05.UnitsPerMin': { station: 'PKG-05', signal: 'unitsPerMin' },
};
export class OpcUaSource extends TelemetrySource {
constructor({ endpoint, securityMode, username, password } = {}) {
super('opcua');
this.endpoint = endpoint;
this.securityMode = securityMode;
this.username = username;
this.password = password;
this.analytics = new AnalyticsEngine();
}
/**
* Read-only by default, deliberately.
*
* OPC-UA can write back to a PLC, and a twin that can change a real setpoint is
* a genuinely useful thing - but it is also a safety-critical action that needs
* interlocks, an audit trail, and the customer's explicit sign-off. Do not turn
* setpointControl on here because the demo UI has a slider.
*/
get capabilities() {
return { timeControl: false, faultInjection: false, setpointControl: false };
}
async start() {
throw new Error(
'OPC-UA source is not configured. This is a documented stub.\n' +
'To enable it: npm i node-opcua, populate NODE_MAP in ' +
'server/ingest/opcuaSource.js with NodeIds browsed from your server, ' +
'implement start(), then set TELEMETRY_SOURCE=opcua and OPCUA_ENDPOINT in .env.\n' +
'Run with TELEMETRY_SOURCE=simulated for the demo.',
);
/* Implementation outline:
*
* const { OPCUAClient, MessageSecurityMode, SecurityPolicy, AttributeIds,
* ClientSubscription, TimestampsToReturn } = await import('node-opcua');
*
* this.client = OPCUAClient.create({ endpointMustExist: false });
* await this.client.connect(this.endpoint);
* this.session = await this.client.createSession(
* this.username ? { userName: this.username, password: this.password } : undefined);
*
* this.subscription = await this.session.createSubscription2({
* requestedPublishingInterval: 500,
* publishingEnabled: true,
* });
*
* for (const [nodeId, tag] of Object.entries(NODE_MAP)) {
* const item = await this.subscription.monitor(
* { nodeId, attributeId: AttributeIds.Value },
* { samplingInterval: 500, queueSize: 10, discardOldest: true },
* TimestampsToReturn.Both);
* item.on('changed', (dataValue) => {
* // Honour the status code. A Bad or Uncertain value must NOT be
* // rendered as live data - that is how a twin starts lying.
* if (!dataValue.statusCode.isGood()) {
* this.markStale(tag);
* return;
* }
* this.values[`${tag.station}.${tag.signal}`] = {
* value: dataValue.value.value,
* // Prefer the SOURCE timestamp: it is when the PLC sampled the
* // sensor, not when the message happened to reach us.
* t: dataValue.sourceTimestamp ?? dataValue.serverTimestamp,
* };
* });
* }
*
* this.timer = setInterval(() => this.assembleAndEmit(), 500);
*
* assembleAndEmit() builds the same frame shape SimulatedSource emits, then
* calls this.analytics.update(snapshot) and this.emit(frame). The analytics
* layer needs no changes at all: TrendTracker and BaselineBank work on
* timestamped values regardless of where they came from.
*/
}
async stop() {
if (this.timer) clearInterval(this.timer);
if (this.subscription) await this.subscription.terminate();
if (this.session) await this.session.close();
if (this.client) await this.client.disconnect();
}
}