@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/README.md +139 -0
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/i3x.config.example.yml +21 -0
- package/package.json +51 -0
- package/src/banner.ts +38 -0
- package/src/cli.ts +50 -0
- package/src/config.ts +119 -0
- package/src/demo.ts +246 -0
- package/src/index.ts +3 -0
- package/src/server.ts +82 -0
- package/tests/e2e.test.ts +766 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @node-i3x/app
|
|
2
|
+
|
|
3
|
+
> **Expose any OPC UA server as an i3X REST API -- one command.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@node-i3x/app)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://www.typescriptlang.org)
|
|
8
|
+
[](../../LICENSE)
|
|
9
|
+
[](https://sterfive.com)
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @node-i3x/app -e opc.tcp://my-plc:4840
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That's it. The i3X REST API is now available at `http://localhost:8080`.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @node-i3x/app
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or use directly with `npx` (no install needed).
|
|
26
|
+
|
|
27
|
+
## CLI Options
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Usage: i3x [options]
|
|
31
|
+
|
|
32
|
+
Expose OPC UA servers as i3X REST APIs
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
-V, --version Show version
|
|
36
|
+
-e, --endpoint <url> OPC UA endpoint URL
|
|
37
|
+
-p, --port <port> REST API port (default: 8080)
|
|
38
|
+
-H, --host <host> REST API bind address (default: 0.0.0.0)
|
|
39
|
+
--security-mode <mode> OPC UA security mode (default: None)
|
|
40
|
+
--optimized-client <mode> Optimized client: auto | disabled
|
|
41
|
+
--subscription-interval <seconds> Subscription interval in seconds
|
|
42
|
+
--log-level <level> debug | info | warn | error
|
|
43
|
+
--no-model-preload Skip model preload on startup
|
|
44
|
+
-c, --config <path> Path to config file
|
|
45
|
+
-h, --help Show help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
i3X uses layered configuration. Each layer overrides the previous:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
1. Built-in defaults
|
|
54
|
+
2. Config file (i3x.config.yml)
|
|
55
|
+
3. Environment variables (I3X_*)
|
|
56
|
+
4. CLI arguments (--endpoint, --port, ...)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Config File
|
|
60
|
+
|
|
61
|
+
Create an `i3x.config.yml` in your project root:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
# i3x.config.yml
|
|
65
|
+
endpoint: opc.tcp://192.168.1.100:4840
|
|
66
|
+
port: 8080
|
|
67
|
+
host: 0.0.0.0
|
|
68
|
+
logLevel: info
|
|
69
|
+
subscriptionInterval: 5
|
|
70
|
+
modelPreload: true
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Supported file names (auto-discovered):
|
|
74
|
+
|
|
75
|
+
| File | Format |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `i3x.config.yml` | YAML |
|
|
78
|
+
| `i3x.config.yaml` | YAML |
|
|
79
|
+
| `i3x.config.json` | JSON |
|
|
80
|
+
| `.i3xrc` | JSON |
|
|
81
|
+
| `.i3xrc.json` | JSON |
|
|
82
|
+
| `.i3xrc.yml` | YAML |
|
|
83
|
+
| `package.json` (`"i3x"` key) | JSON |
|
|
84
|
+
|
|
85
|
+
### Environment Variables
|
|
86
|
+
|
|
87
|
+
All environment variables are prefixed with `I3X_`:
|
|
88
|
+
|
|
89
|
+
| Variable | Config key | Default |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `I3X_OPCUA_ENDPOINT` | `endpoint` | `opc.tcp://localhost:4840` |
|
|
92
|
+
| `I3X_PORT` | `port` | `8080` |
|
|
93
|
+
| `I3X_HOST` | `host` | `0.0.0.0` |
|
|
94
|
+
| `I3X_OPCUA_SECURITY_MODE` | `securityMode` | `None` |
|
|
95
|
+
| `I3X_OPCUA_OPTIMIZED_CLIENT` | `optimizedClient` | `auto` |
|
|
96
|
+
| `I3X_SUBSCRIPTION_INTERVAL_SECONDS` | `subscriptionInterval` | `5` |
|
|
97
|
+
| `I3X_LOG_LEVEL` | `logLevel` | `info` |
|
|
98
|
+
| `I3X_MODEL_PRELOAD_ON_STARTUP` | `modelPreload` | `true` |
|
|
99
|
+
|
|
100
|
+
## Programmatic Usage
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { resolveConfig, startServer } from '@node-i3x/app';
|
|
104
|
+
|
|
105
|
+
const config = await resolveConfig({
|
|
106
|
+
endpoint: 'opc.tcp://my-plc:4840',
|
|
107
|
+
port: 9090,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await startServer(config, '1.0.0');
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## How It Works
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Your OPC UA Server i3X (@node-i3x/app)
|
|
117
|
+
+-----------------+ +---------------------------+
|
|
118
|
+
| AddressSpace | <-----> | OpcUaDataSourceAdapter |
|
|
119
|
+
| (PLCs, sensors) | opc.tcp | ModelService |
|
|
120
|
+
+-----------------+ | ValueService |
|
|
121
|
+
| HistoryService |
|
|
122
|
+
| SubscriptionService |
|
|
123
|
+
| |
|
|
124
|
+
| i3X REST API (Fastify) |
|
|
125
|
+
+----------+----------------+
|
|
126
|
+
|
|
|
127
|
+
http://0.0.0.0:8080
|
|
128
|
+
|
|
|
129
|
+
+----------v----------------+
|
|
130
|
+
| Your Web App / Dashboard |
|
|
131
|
+
+---------------------------+
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
**AGPL-3.0-or-later** -- Free for open-source projects.
|
|
137
|
+
|
|
138
|
+
For commercial / proprietary use, contact [Sterfive](https://sterfive.com)
|
|
139
|
+
for a commercial license.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createRequire as R}from"module";import{Command as U}from"commander";import{cosmiconfig as h}from"cosmiconfig";import{load as u}from"js-yaml";var O={endpoint:"opc.tcp://localhost:4840",port:8080,host:"0.0.0.0",securityMode:"None",optimizedClient:"auto",subscriptionInterval:5,logLevel:"info",modelPreload:!0,failOnPreloadError:!1};function d(o){return process.env[o]}function g(o){let e=process.env[o];return e?parseInt(e,10):void 0}function v(o){let e=process.env[o];if(e)return e==="1"||e.toLowerCase()==="true"}function y(){let o={},e=d("I3X_OPCUA_ENDPOINT")??d("I3X_ENDPOINT");e&&(o.endpoint=e);let n=g("I3X_PORT");n!==void 0&&(o.port=n);let t=d("I3X_HOST");t&&(o.host=t);let r=d("I3X_OPCUA_SECURITY_MODE");r&&(o.securityMode=r);let i=d("I3X_OPCUA_OPTIMIZED_CLIENT");(i==="auto"||i==="disabled")&&(o.optimizedClient=i);let s=g("I3X_SUBSCRIPTION_INTERVAL_SECONDS");s!==void 0&&(o.subscriptionInterval=s);let c=d("I3X_LOG_LEVEL");c&&(o.logLevel=c);let l=v("I3X_MODEL_PRELOAD_ON_STARTUP");l!==void 0&&(o.modelPreload=l);let a=v("I3X_FAIL_STARTUP_ON_MODEL_PRELOAD_ERROR");return a!==void 0&&(o.failOnPreloadError=a),o}async function I(o,e){let n={},t=h("i3x",{searchPlaces:["i3x.config.yml","i3x.config.yaml","i3x.config.json",".i3xrc",".i3xrc.json",".i3xrc.yml",".i3xrc.yaml","package.json"],loaders:{".yml":(i,s)=>u(s),".yaml":(i,s)=>u(s)}});try{let i=e?await t.load(e):await t.search();i&&!i.isEmpty&&(n=i.config)}catch{}let r=y();return{...O,...n,...r,...o}}import{consoleLogger as E,HistoryService as _,ModelService as b,SubscriptionService as L,ValueService as w}from"@node-i3x/core";import{OpcUaClient as A,OpcUaDataSourceAdapter as T}from"@node-i3x/opcua-connector";import{createApp as M}from"@node-i3x/rest-server";function C(o,e,n){let t=[""," i3X Server v"+o,""," OPC UA: "+e.endpoint," REST: http://"+e.host+":"+e.port];n!==void 0&&t.push(" Model: "+n+" nodes"),t.push(""),t.push(" Press Ctrl+C to stop"),t.push("");let i=Math.max(...t.map(a=>a.length))+2,s="-".repeat(i),c=" +"+s+"+",l=" +"+s+"+";console.log(c);for(let a of t)console.log(" |"+a.padEnd(i)+"|");console.log(l),console.log()}async function P(o,e){let n=E,t=new A({endpointUrl:o.endpoint,securityMode:o.securityMode,optimizedClient:o.optimizedClient},n),r=new T(t,n),i=new b(r,n),s=new w(r,i,n),c=new _(r,i,n),l=new L(r,i,n,o.subscriptionInterval),a=await M({dataSource:r,modelService:i,valueService:s,historyService:c,subscriptionService:l,logger:n});await r.connect();let p;if(o.modelPreload)try{p=(await i.preloadModel()).nodesById.size}catch(m){n.error("Model preload failed: "+String(m)),o.failOnPreloadError&&process.exit(1)}await a.listen({port:o.port,host:o.host}),C(e,o,p);let f=async()=>{n.info("Shutting down..."),await a.close(),await l.close(),await r.disconnect(),process.exit(0)};process.on("SIGINT",f),process.on("SIGTERM",f)}var z=R(import.meta.url),x=z("../package.json"),S=new U;S.name("i3x").description("Expose OPC UA servers as i3X REST APIs").version(x.version).option("-e, --endpoint <url>","OPC UA endpoint URL").option("-p, --port <port>","REST API port",parseInt).option("-H, --host <host>","REST API bind address").option("--security-mode <mode>","OPC UA security mode").option("--optimized-client <mode>","Optimized client: auto | disabled").option("--subscription-interval <seconds>","Subscription interval in seconds",parseInt).option("--log-level <level>","Log level: debug | info | warn | error").option("--no-model-preload","Skip model preload on startup").option("-c, --config <path>","Path to config file").action(async o=>{let e={};o.endpoint&&(e.endpoint=o.endpoint),o.port!==void 0&&(e.port=o.port),o.host&&(e.host=o.host),o.securityMode&&(e.securityMode=o.securityMode),o.optimizedClient&&(e.optimizedClient=o.optimizedClient),o.subscriptionInterval!==void 0&&(e.subscriptionInterval=o.subscriptionInterval),o.logLevel&&(e.logLevel=o.logLevel),o.modelPreload===!1&&(e.modelPreload=!1);let n=await I(e,o.config);await P(n,x.version)});S.parse();
|
|
3
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/config.ts","../src/server.ts","../src/banner.ts"],"mappings":"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","names":["createRequire","Command","cosmiconfig","loadYaml","DEFAULTS","envStr","key","envInt","v","envBool","fromEnv","result","endpoint","port","host","securityMode","optimizedClient","interval","logLevel","preload","failOnPreload","resolveConfig","cliArgs","configPath","fileConfig","explorer","_filepath","content","envConfig","consoleLogger","HistoryService","ModelService","SubscriptionService","ValueService","OpcUaClient","OpcUaDataSourceAdapter","createApp","printBanner","version","config","nodeCount","lines","width","l","hr","top","bot","line","startServer","config","version","logger","consoleLogger","opcuaClient","OpcUaClient","dataSource","OpcUaDataSourceAdapter","modelService","ModelService","valueService","ValueService","historyService","HistoryService","subscriptionService","SubscriptionService","app","createApp","nodeCount","err","printBanner","shutdown","require","createRequire","pkg","program","Command","opts","cliArgs","config","resolveConfig","startServer"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface I3xConfig {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
port: number;
|
|
4
|
+
host: string;
|
|
5
|
+
securityMode: string;
|
|
6
|
+
optimizedClient: 'auto' | 'disabled';
|
|
7
|
+
subscriptionInterval: number;
|
|
8
|
+
logLevel: string;
|
|
9
|
+
modelPreload: boolean;
|
|
10
|
+
failOnPreloadError: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare function resolveConfig(cliArgs: Partial<I3xConfig>, configPath?: string): Promise<I3xConfig>;
|
|
13
|
+
|
|
14
|
+
declare function startServer(config: I3xConfig, version: string): Promise<void>;
|
|
15
|
+
|
|
16
|
+
export { type I3xConfig, resolveConfig, startServer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{cosmiconfig as C}from"cosmiconfig";import{load as m}from"js-yaml";var x={endpoint:"opc.tcp://localhost:4840",port:8080,host:"0.0.0.0",securityMode:"None",optimizedClient:"auto",subscriptionInterval:5,logLevel:"info",modelPreload:!0,failOnPreloadError:!1};function d(o){return process.env[o]}function g(o){let e=process.env[o];return e?parseInt(e,10):void 0}function I(o){let e=process.env[o];if(e)return e==="1"||e.toLowerCase()==="true"}function P(){let o={},e=d("I3X_OPCUA_ENDPOINT")??d("I3X_ENDPOINT");e&&(o.endpoint=e);let n=g("I3X_PORT");n!==void 0&&(o.port=n);let r=d("I3X_HOST");r&&(o.host=r);let i=d("I3X_OPCUA_SECURITY_MODE");i&&(o.securityMode=i);let t=d("I3X_OPCUA_OPTIMIZED_CLIENT");(t==="auto"||t==="disabled")&&(o.optimizedClient=t);let s=g("I3X_SUBSCRIPTION_INTERVAL_SECONDS");s!==void 0&&(o.subscriptionInterval=s);let c=d("I3X_LOG_LEVEL");c&&(o.logLevel=c);let l=I("I3X_MODEL_PRELOAD_ON_STARTUP");l!==void 0&&(o.modelPreload=l);let a=I("I3X_FAIL_STARTUP_ON_MODEL_PRELOAD_ERROR");return a!==void 0&&(o.failOnPreloadError=a),o}async function S(o,e){let n={},r=C("i3x",{searchPlaces:["i3x.config.yml","i3x.config.yaml","i3x.config.json",".i3xrc",".i3xrc.json",".i3xrc.yml",".i3xrc.yaml","package.json"],loaders:{".yml":(t,s)=>m(s),".yaml":(t,s)=>m(s)}});try{let t=e?await r.load(e):await r.search();t&&!t.isEmpty&&(n=t.config)}catch{}let i=P();return{...x,...n,...i,...o}}import{consoleLogger as _,HistoryService as O,ModelService as h,SubscriptionService as E,ValueService as y}from"@node-i3x/core";import{OpcUaClient as w,OpcUaDataSourceAdapter as L}from"@node-i3x/opcua-connector";import{createApp as b}from"@node-i3x/rest-server";function v(o,e,n){let r=[""," i3X Server v"+o,""," OPC UA: "+e.endpoint," REST: http://"+e.host+":"+e.port];n!==void 0&&r.push(" Model: "+n+" nodes"),r.push(""),r.push(" Press Ctrl+C to stop"),r.push("");let t=Math.max(...r.map(a=>a.length))+2,s="-".repeat(t),c=" +"+s+"+",l=" +"+s+"+";console.log(c);for(let a of r)console.log(" |"+a.padEnd(t)+"|");console.log(l),console.log()}async function T(o,e){let n=_,r=new w({endpointUrl:o.endpoint,securityMode:o.securityMode,optimizedClient:o.optimizedClient},n),i=new L(r,n),t=new h(i,n),s=new y(i,t,n),c=new O(i,t,n),l=new E(i,t,n,o.subscriptionInterval),a=await b({dataSource:i,modelService:t,valueService:s,historyService:c,subscriptionService:l,logger:n});await i.connect();let p;if(o.modelPreload)try{p=(await t.preloadModel()).nodesById.size}catch(u){n.error("Model preload failed: "+String(u)),o.failOnPreloadError&&process.exit(1)}await a.listen({port:o.port,host:o.host}),v(e,o,p);let f=async()=>{n.info("Shutting down..."),await a.close(),await l.close(),await i.disconnect(),process.exit(0)};process.on("SIGINT",f),process.on("SIGTERM",f)}export{S as resolveConfig,T as startServer};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/server.ts","../src/banner.ts"],"mappings":"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","names":["cosmiconfig","loadYaml","DEFAULTS","envStr","key","envInt","v","envBool","fromEnv","result","endpoint","port","host","securityMode","optimizedClient","interval","logLevel","preload","failOnPreload","resolveConfig","cliArgs","configPath","fileConfig","explorer","_filepath","content","envConfig","consoleLogger","HistoryService","ModelService","SubscriptionService","ValueService","OpcUaClient","OpcUaDataSourceAdapter","createApp","printBanner","version","config","nodeCount","lines","width","l","hr","top","bot","line","startServer","config","version","logger","consoleLogger","opcuaClient","OpcUaClient","dataSource","OpcUaDataSourceAdapter","modelService","ModelService","valueService","ValueService","historyService","HistoryService","subscriptionService","SubscriptionService","app","createApp","nodeCount","err","printBanner","shutdown"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# i3X configuration
|
|
2
|
+
# See: npx @node-i3x/app --help
|
|
3
|
+
|
|
4
|
+
# OPC UA server to connect to
|
|
5
|
+
endpoint: opc.tcp://localhost:4840
|
|
6
|
+
|
|
7
|
+
# REST API settings
|
|
8
|
+
port: 8080
|
|
9
|
+
host: 0.0.0.0
|
|
10
|
+
|
|
11
|
+
# OPC UA security
|
|
12
|
+
securityMode: None
|
|
13
|
+
# optimizedClient: auto
|
|
14
|
+
|
|
15
|
+
# Domain settings
|
|
16
|
+
subscriptionInterval: 5
|
|
17
|
+
modelPreload: true
|
|
18
|
+
# failOnPreloadError: false
|
|
19
|
+
|
|
20
|
+
# Logging
|
|
21
|
+
logLevel: info
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@node-i3x/app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "i3X CLI -- expose OPC UA servers as i3X REST APIs",
|
|
5
|
+
"license": "AGPL-3.0-or-later OR LicenseRef-Sterfive-Commercial",
|
|
6
|
+
"author": "Sterfive SAS <contact@sterfive.com> (https://sterfive.com)",
|
|
7
|
+
"homepage": "https://sterfive.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/node-opcua/i3x2ua-node.git",
|
|
11
|
+
"directory": "packages/app"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"i3x": "./dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsx src/cli.ts",
|
|
31
|
+
"start": "node dist/cli.js",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@node-i3x/core": "*",
|
|
36
|
+
"@node-i3x/opcua-connector": "*",
|
|
37
|
+
"@node-i3x/rest-server": "*",
|
|
38
|
+
"commander": "^13.0.0",
|
|
39
|
+
"cosmiconfig": "^9.0.0",
|
|
40
|
+
"fastify": "^5.0.0",
|
|
41
|
+
"@fastify/cors": "^11.0.0",
|
|
42
|
+
"fastify-plugin": "^5.0.0",
|
|
43
|
+
"js-yaml": "^4.1.0",
|
|
44
|
+
"node-opcua": "^2.128.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/js-yaml": "^4.0.9",
|
|
48
|
+
"tsup": "^8.0.0",
|
|
49
|
+
"tsx": "^4.16.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/banner.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
}
|