@node-i3x/app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/demo.ts ADDED
@@ -0,0 +1,246 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Demo: Start a representative OPC UA server + i3X REST API
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import {
6
+ consoleLogger,
7
+ HistoryService,
8
+ ModelService,
9
+ SubscriptionService,
10
+ ValueService,
11
+ } from '@node-i3x/core';
12
+ import { OpcUaClient, OpcUaDataSourceAdapter } from '@node-i3x/opcua-connector';
13
+ import { createApp } from '@node-i3x/rest-server';
14
+ import { DataType, nodesets, OPCUAServer, Variant } from 'node-opcua';
15
+
16
+ const PORT = 8000;
17
+ const OPCUA_PORT = 48400;
18
+
19
+ async function startOpcUaServer() {
20
+ const server = new OPCUAServer({
21
+ port: OPCUA_PORT,
22
+ resourcePath: '/UA/i3xDemo',
23
+ nodeset_filename: [nodesets.standard],
24
+ serverInfo: {
25
+ applicationName: { text: 'i3X Demo OPC UA Server' },
26
+ applicationUri: 'urn:i3x:demo',
27
+ productUri: 'urn:i3x:demo',
28
+ },
29
+ });
30
+
31
+ await server.initialize();
32
+ const addressSpace = server.engine.addressSpace!;
33
+ const ns = addressSpace.getOwnNamespace();
34
+
35
+ // ── Production Line ──────────────────────────────────────
36
+ const line = ns.addObject({
37
+ organizedBy: addressSpace.rootFolder.objects,
38
+ browseName: 'ProductionLine',
39
+ displayName: 'Production Line #1',
40
+ });
41
+
42
+ // ── CNC Milling Machine ─────────────────────────────────
43
+ const cnc = ns.addObject({
44
+ componentOf: line,
45
+ browseName: 'CncMachine',
46
+ displayName: 'CNC Milling Machine',
47
+ });
48
+
49
+ let cncTemp = 65.2;
50
+ ns.addVariable({
51
+ componentOf: cnc,
52
+ browseName: 'Temperature',
53
+ displayName: 'Temperature (°C)',
54
+ dataType: DataType.Double,
55
+ value: { get: () => new Variant({ dataType: DataType.Double, value: cncTemp }) },
56
+ });
57
+
58
+ let cncSpeed = 1500;
59
+ ns.addVariable({
60
+ componentOf: cnc,
61
+ browseName: 'SpindleSpeed',
62
+ displayName: 'Spindle Speed (RPM)',
63
+ dataType: DataType.Int32,
64
+ value: { get: () => new Variant({ dataType: DataType.Int32, value: cncSpeed }) },
65
+ });
66
+
67
+ ns.addVariable({
68
+ componentOf: cnc,
69
+ browseName: 'Status',
70
+ displayName: 'Status',
71
+ dataType: DataType.String,
72
+ value: { get: () => new Variant({ dataType: DataType.String, value: 'Running' }) },
73
+ });
74
+
75
+ ns.addVariable({
76
+ componentOf: cnc,
77
+ browseName: 'ToolWear',
78
+ displayName: 'Tool Wear (%)',
79
+ dataType: DataType.Float,
80
+ value: { get: () => new Variant({ dataType: DataType.Float, value: 37.8 }) },
81
+ });
82
+
83
+ // ── Laser Cutter ─────────────────────────────────────────
84
+ const laser = ns.addObject({
85
+ componentOf: line,
86
+ browseName: 'LaserCutter',
87
+ displayName: 'Laser Cutter',
88
+ });
89
+
90
+ ns.addVariable({
91
+ componentOf: laser,
92
+ browseName: 'Temperature',
93
+ displayName: 'Temperature (°C)',
94
+ dataType: DataType.Double,
95
+ value: { get: () => new Variant({ dataType: DataType.Double, value: 42.7 }) },
96
+ });
97
+
98
+ ns.addVariable({
99
+ componentOf: laser,
100
+ browseName: 'LaserPower',
101
+ displayName: 'Laser Power (W)',
102
+ dataType: DataType.Float,
103
+ value: { get: () => new Variant({ dataType: DataType.Float, value: 2400.5 }) },
104
+ });
105
+
106
+ ns.addVariable({
107
+ componentOf: laser,
108
+ browseName: 'JobCount',
109
+ displayName: 'Completed Jobs',
110
+ dataType: DataType.UInt32,
111
+ value: { get: () => new Variant({ dataType: DataType.UInt32, value: 1247 }) },
112
+ });
113
+
114
+ // ── Robot Arm ────────────────────────────────────────────
115
+ const robot = ns.addObject({
116
+ componentOf: line,
117
+ browseName: 'RobotArm',
118
+ displayName: 'Assembly Robot Arm',
119
+ });
120
+
121
+ ns.addVariable({
122
+ componentOf: robot,
123
+ browseName: 'JointAngle',
124
+ displayName: 'Joint Angle (deg)',
125
+ dataType: DataType.Double,
126
+ value: { get: () => new Variant({ dataType: DataType.Double, value: 127.3 }) },
127
+ });
128
+
129
+ ns.addVariable({
130
+ componentOf: robot,
131
+ browseName: 'Payload',
132
+ displayName: 'Current Payload (kg)',
133
+ dataType: DataType.Float,
134
+ value: { get: () => new Variant({ dataType: DataType.Float, value: 4.2 }) },
135
+ });
136
+
137
+ ns.addVariable({
138
+ componentOf: robot,
139
+ browseName: 'CycleCount',
140
+ displayName: 'Cycle Count',
141
+ dataType: DataType.UInt32,
142
+ value: { get: () => new Variant({ dataType: DataType.UInt32, value: 84291 }) },
143
+ });
144
+
145
+ ns.addVariable({
146
+ componentOf: robot,
147
+ browseName: 'OperatingMode',
148
+ displayName: 'Operating Mode',
149
+ dataType: DataType.String,
150
+ value: { get: () => new Variant({ dataType: DataType.String, value: 'Automatic' }) },
151
+ });
152
+
153
+ // Simulate live temperature changes
154
+ setInterval(() => {
155
+ cncTemp += (Math.random() - 0.4) * 1.5;
156
+ }, 1000);
157
+ setInterval(() => {
158
+ cncSpeed = 1500 + Math.floor((Math.random() - 0.5) * 100);
159
+ }, 2000);
160
+
161
+ await server.start();
162
+ return server;
163
+ }
164
+
165
+ async function main() {
166
+ const logger = consoleLogger;
167
+
168
+ console.log(`\n${'═'.repeat(60)}`);
169
+ console.log(' 🏭 i3x2ua-node Demo');
170
+ console.log('═'.repeat(60));
171
+
172
+ // 1. Start OPC UA server
173
+ console.log('\n▶ Starting OPC UA test server...');
174
+ const opcuaServer = await startOpcUaServer();
175
+ const endpointUrl = `opc.tcp://localhost:${OPCUA_PORT}/UA/i3xDemo`;
176
+ console.log(` ✓ OPC UA server ready at ${endpointUrl}`);
177
+
178
+ // 2. Connect i3X adapter
179
+ console.log('\n▶ Connecting i3X OPC UA adapter...');
180
+ const client = new OpcUaClient(
181
+ {
182
+ endpointUrl,
183
+ securityMode: 'None',
184
+ optimizedClient: 'auto',
185
+ },
186
+ logger,
187
+ );
188
+ const dataSource = new OpcUaDataSourceAdapter(client, logger);
189
+ await dataSource.connect();
190
+
191
+ // 3. Domain services
192
+ const modelService = new ModelService(dataSource, logger);
193
+ const valueService = new ValueService(dataSource, modelService, logger);
194
+ const historyService = new HistoryService(dataSource, modelService, logger);
195
+ const subscriptionService = new SubscriptionService(
196
+ dataSource,
197
+ modelService,
198
+ logger,
199
+ 1,
200
+ );
201
+
202
+ // 4. Preload model
203
+ console.log('\n▶ Building i3X model from OPC UA...');
204
+ const model = await modelService.preloadModel();
205
+ console.log(
206
+ ` ✓ Model: ${model.nodesById.size} nodes, ${model.rootIds.length} roots, ${model.propertyToSource.size} properties`,
207
+ );
208
+
209
+ // 5. Start REST server
210
+ const app = await createApp({
211
+ dataSource,
212
+ modelService,
213
+ valueService,
214
+ historyService,
215
+ subscriptionService,
216
+ logger,
217
+ });
218
+ await app.listen({ port: PORT, host: '127.0.0.1' });
219
+
220
+ console.log(`\n${'═'.repeat(60)}`);
221
+ console.log(` 🚀 i3X REST API ready at http://127.0.0.1:${PORT}`);
222
+ console.log('═'.repeat(60));
223
+ console.log('\n Try these endpoints:');
224
+ console.log(` curl http://localhost:${PORT}/v1/info`);
225
+ console.log(` curl http://localhost:${PORT}/v1/namespaces`);
226
+ console.log(` curl http://localhost:${PORT}/v1/objects`);
227
+ console.log(` curl http://localhost:${PORT}/health`);
228
+ console.log('\n Press Ctrl+C to stop.\n');
229
+
230
+ // Graceful shutdown
231
+ const shutdown = async () => {
232
+ console.log('\n\nShutting down...');
233
+ await app.close();
234
+ await subscriptionService.close();
235
+ await dataSource.disconnect();
236
+ await opcuaServer.shutdown(500);
237
+ process.exit(0);
238
+ };
239
+ process.on('SIGINT', shutdown);
240
+ process.on('SIGTERM', shutdown);
241
+ }
242
+
243
+ main().catch((err) => {
244
+ console.error('Fatal:', err);
245
+ process.exit(1);
246
+ });
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type { I3xConfig } from './config.js';
2
+ export { resolveConfig } from './config.js';
3
+ export { startServer } from './server.js';
package/src/server.ts ADDED
@@ -0,0 +1,82 @@
1
+ import {
2
+ consoleLogger,
3
+ HistoryService,
4
+ ModelService,
5
+ SubscriptionService,
6
+ ValueService,
7
+ } from '@node-i3x/core';
8
+ import { OpcUaClient, OpcUaDataSourceAdapter } from '@node-i3x/opcua-connector';
9
+ import { createApp } from '@node-i3x/rest-server';
10
+ import type { I3xConfig } from './config.js';
11
+ import { printBanner } from './banner.js';
12
+
13
+ export async function startServer(
14
+ config: I3xConfig,
15
+ version: string,
16
+ ): Promise<void> {
17
+ const logger = consoleLogger;
18
+
19
+ // 1. Outbound adapter (OPC UA)
20
+ const opcuaClient = new OpcUaClient(
21
+ {
22
+ endpointUrl: config.endpoint,
23
+ securityMode: config.securityMode,
24
+ optimizedClient: config.optimizedClient,
25
+ },
26
+ logger,
27
+ );
28
+ const dataSource = new OpcUaDataSourceAdapter(opcuaClient, logger);
29
+
30
+ // 2. Domain services (inject the port)
31
+ const modelService = new ModelService(dataSource, logger);
32
+ const valueService = new ValueService(dataSource, modelService, logger);
33
+ const historyService = new HistoryService(dataSource, modelService, logger);
34
+ const subscriptionService = new SubscriptionService(
35
+ dataSource,
36
+ modelService,
37
+ logger,
38
+ config.subscriptionInterval,
39
+ );
40
+
41
+ // 3. Inbound adapter (REST)
42
+ const app = await createApp({
43
+ dataSource,
44
+ modelService,
45
+ valueService,
46
+ historyService,
47
+ subscriptionService,
48
+ logger,
49
+ });
50
+
51
+ // 4. Connect to OPC UA
52
+ await dataSource.connect();
53
+
54
+ // 5. Preload model
55
+ let nodeCount: number | undefined;
56
+ if (config.modelPreload) {
57
+ try {
58
+ const model = await modelService.preloadModel();
59
+ nodeCount = model.nodesById.size;
60
+ } catch (err) {
61
+ logger.error('Model preload failed: ' + String(err));
62
+ if (config.failOnPreloadError) process.exit(1);
63
+ }
64
+ }
65
+
66
+ // 6. Start HTTP server
67
+ await app.listen({ port: config.port, host: config.host });
68
+
69
+ // 7. Banner
70
+ printBanner(version, config, nodeCount);
71
+
72
+ // 8. Graceful shutdown
73
+ const shutdown = async () => {
74
+ logger.info('Shutting down...');
75
+ await app.close();
76
+ await subscriptionService.close();
77
+ await dataSource.disconnect();
78
+ process.exit(0);
79
+ };
80
+ process.on('SIGINT', shutdown);
81
+ process.on('SIGTERM', shutdown);
82
+ }