@node-i3x/app 0.1.0 → 0.2.4

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/banner.ts DELETED
@@ -1,38 +0,0 @@
1
- import type { I3xConfig } from './config.js';
2
-
3
- export function printBanner(
4
- version: string,
5
- config: I3xConfig,
6
- nodeCount?: number,
7
- ): void {
8
- const lines = [
9
- '',
10
- ' i3X Server v' + version,
11
- '',
12
- ' OPC UA: ' + config.endpoint,
13
- ' REST: http://' + config.host + ':' + config.port,
14
- ];
15
-
16
- if (nodeCount !== undefined) {
17
- lines.push(' Model: ' + nodeCount + ' nodes');
18
- }
19
-
20
- lines.push('');
21
- lines.push(' Press Ctrl+C to stop');
22
- lines.push('');
23
-
24
- // Calculate box width
25
- const maxLen = Math.max(...lines.map((l) => l.length));
26
- const width = maxLen + 2;
27
-
28
- const hr = '-'.repeat(width);
29
- const top = ' +' + hr + '+';
30
- const bot = ' +' + hr + '+';
31
-
32
- console.log(top);
33
- for (const line of lines) {
34
- console.log(' |' + line.padEnd(width) + '|');
35
- }
36
- console.log(bot);
37
- console.log();
38
- }
package/src/cli.ts DELETED
@@ -1,50 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import { Command } from 'commander';
3
- import type { I3xConfig } from './config.js';
4
- import { resolveConfig } from './config.js';
5
- import { startServer } from './server.js';
6
-
7
- const require = createRequire(import.meta.url);
8
- const pkg = require('../package.json') as { version: string };
9
-
10
- const program = new Command();
11
-
12
- program
13
- .name('i3x')
14
- .description('Expose OPC UA servers as i3X REST APIs')
15
- .version(pkg.version)
16
- .option('-e, --endpoint <url>', 'OPC UA endpoint URL')
17
- .option('-p, --port <port>', 'REST API port', parseInt)
18
- .option('-H, --host <host>', 'REST API bind address')
19
- .option('--security-mode <mode>', 'OPC UA security mode')
20
- .option(
21
- '--optimized-client <mode>',
22
- 'Optimized client: auto | disabled',
23
- )
24
- .option(
25
- '--subscription-interval <seconds>',
26
- 'Subscription interval in seconds',
27
- parseInt,
28
- )
29
- .option('--log-level <level>', 'Log level: debug | info | warn | error')
30
- .option('--no-model-preload', 'Skip model preload on startup')
31
- .option('-c, --config <path>', 'Path to config file')
32
- .action(async (opts) => {
33
- // Build partial config from CLI args
34
- const cliArgs: Partial<I3xConfig> = {};
35
- if (opts.endpoint) cliArgs.endpoint = opts.endpoint;
36
- if (opts.port !== undefined) cliArgs.port = opts.port;
37
- if (opts.host) cliArgs.host = opts.host;
38
- if (opts.securityMode) cliArgs.securityMode = opts.securityMode;
39
- if (opts.optimizedClient)
40
- cliArgs.optimizedClient = opts.optimizedClient;
41
- if (opts.subscriptionInterval !== undefined)
42
- cliArgs.subscriptionInterval = opts.subscriptionInterval;
43
- if (opts.logLevel) cliArgs.logLevel = opts.logLevel;
44
- if (opts.modelPreload === false) cliArgs.modelPreload = false;
45
-
46
- const config = await resolveConfig(cliArgs, opts.config);
47
- await startServer(config, pkg.version);
48
- });
49
-
50
- program.parse();
package/src/config.ts DELETED
@@ -1,119 +0,0 @@
1
- import { cosmiconfig } from 'cosmiconfig';
2
- import { load as loadYaml } from 'js-yaml';
3
-
4
- export interface I3xConfig {
5
- endpoint: string;
6
- port: number;
7
- host: string;
8
- securityMode: string;
9
- optimizedClient: 'auto' | 'disabled';
10
- subscriptionInterval: number;
11
- logLevel: string;
12
- modelPreload: boolean;
13
- failOnPreloadError: boolean;
14
- }
15
-
16
- const DEFAULTS: I3xConfig = {
17
- endpoint: 'opc.tcp://localhost:4840',
18
- port: 8080,
19
- host: '0.0.0.0',
20
- securityMode: 'None',
21
- optimizedClient: 'auto',
22
- subscriptionInterval: 5,
23
- logLevel: 'info',
24
- modelPreload: true,
25
- failOnPreloadError: false,
26
- };
27
-
28
- // ── Environment variable helpers ───────────────────────────
29
- function envStr(key: string): string | undefined {
30
- return process.env[key];
31
- }
32
- function envInt(key: string): number | undefined {
33
- const v = process.env[key];
34
- return v ? parseInt(v, 10) : undefined;
35
- }
36
- function envBool(key: string): boolean | undefined {
37
- const v = process.env[key];
38
- if (!v) return undefined;
39
- return v === '1' || v.toLowerCase() === 'true';
40
- }
41
-
42
- function fromEnv(): Partial<I3xConfig> {
43
- const result: Partial<I3xConfig> = {};
44
- const endpoint = envStr('I3X_OPCUA_ENDPOINT') ?? envStr('I3X_ENDPOINT');
45
- if (endpoint) result.endpoint = endpoint;
46
-
47
- const port = envInt('I3X_PORT');
48
- if (port !== undefined) result.port = port;
49
-
50
- const host = envStr('I3X_HOST');
51
- if (host) result.host = host;
52
-
53
- const securityMode = envStr('I3X_OPCUA_SECURITY_MODE');
54
- if (securityMode) result.securityMode = securityMode;
55
-
56
- const optimizedClient = envStr('I3X_OPCUA_OPTIMIZED_CLIENT');
57
- if (optimizedClient === 'auto' || optimizedClient === 'disabled')
58
- result.optimizedClient = optimizedClient;
59
-
60
- const interval = envInt('I3X_SUBSCRIPTION_INTERVAL_SECONDS');
61
- if (interval !== undefined) result.subscriptionInterval = interval;
62
-
63
- const logLevel = envStr('I3X_LOG_LEVEL');
64
- if (logLevel) result.logLevel = logLevel;
65
-
66
- const preload = envBool('I3X_MODEL_PRELOAD_ON_STARTUP');
67
- if (preload !== undefined) result.modelPreload = preload;
68
-
69
- const failOnPreload = envBool('I3X_FAIL_STARTUP_ON_MODEL_PRELOAD_ERROR');
70
- if (failOnPreload !== undefined) result.failOnPreloadError = failOnPreload;
71
-
72
- return result;
73
- }
74
-
75
- // ── Config file discovery ──────────────────────────────────
76
- export async function resolveConfig(
77
- cliArgs: Partial<I3xConfig>,
78
- configPath?: string,
79
- ): Promise<I3xConfig> {
80
- // 1. Load config file (i3x.config.yml, i3x.config.json, ...)
81
- let fileConfig: Partial<I3xConfig> = {};
82
- const explorer = cosmiconfig('i3x', {
83
- searchPlaces: [
84
- 'i3x.config.yml',
85
- 'i3x.config.yaml',
86
- 'i3x.config.json',
87
- '.i3xrc',
88
- '.i3xrc.json',
89
- '.i3xrc.yml',
90
- '.i3xrc.yaml',
91
- 'package.json',
92
- ],
93
- loaders: {
94
- '.yml': (_filepath: string, content: string) => loadYaml(content),
95
- '.yaml': (_filepath: string, content: string) => loadYaml(content),
96
- },
97
- });
98
-
99
- try {
100
- const result = configPath
101
- ? await explorer.load(configPath)
102
- : await explorer.search();
103
- if (result && !result.isEmpty) {
104
- fileConfig = result.config as Partial<I3xConfig>;
105
- }
106
- } catch {
107
- // No config file found -- that's fine
108
- }
109
-
110
- // 2. Layer: defaults < config file < env vars < CLI args
111
- const envConfig = fromEnv();
112
-
113
- return {
114
- ...DEFAULTS,
115
- ...fileConfig,
116
- ...envConfig,
117
- ...cliArgs,
118
- };
119
- }
package/src/demo.ts DELETED
@@ -1,246 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- export type { I3xConfig } from './config.js';
2
- export { resolveConfig } from './config.js';
3
- export { startServer } from './server.js';
package/src/server.ts DELETED
@@ -1,82 +0,0 @@
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
- }