From ff7d9f00d374f2fcdea502f4bd6cebbde9d3acdb Mon Sep 17 00:00:00 2001 From: Marin Karamihalev <marin.karamihalev@iopsys.eu> Date: Fri, 22 Jan 2021 15:56:58 +0100 Subject: [PATCH] index & protocol: fixed error throwing so that all errors can be caught --- package.json | 2 +- public/index.html | 126 +++++++++++--------- src/index.ts | 200 +++++++++++++++++--------------- src/protocol/index.ts | 1 - src/types.d.ts | 2 + tests/integration/config.json | 8 +- tests/integration/index.test.js | 6 +- 7 files changed, 187 insertions(+), 158 deletions(-) diff --git a/package.json b/package.json index 09006bb..c8fd9e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "usp-js", - "version": "0.0.9", + "version": "0.0.10", "description": "Helper library for easy usp communication using mqtt over tcp or ws.", "main": "build/index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 01551cf..5d48b14 100644 --- a/public/index.html +++ b/public/index.html @@ -68,7 +68,10 @@ <h1>usp-js</h1> </a> <p>Helper library for easy usp communication using mqtt over tcp or ws.</p> - <p><a href="https://usp-data-models.broadband-forum.org/tr-181-2-13-0-usp.html">USP Reference</a></p> + <ul> + <li><a href="https://iopsys.se/usp-js/index.html">API documentation for usp-js</a></li> + <li><a href="https://usp-data-models.broadband-forum.org/tr-181-2-13-0-usp.html">BBF's USP reference documentation</a></li> + </ul> <a href="#installation" id="installation" style="color: inherit; text-decoration: none;"> <h1>Installation</h1> </a> @@ -76,14 +79,19 @@ <a href="#usage" id="usage" style="color: inherit; text-decoration: none;"> <h1>Usage</h1> </a> - <pre><code class="language-javascript"><span class="hljs-keyword">const</span> connect = <span class="hljs-built_in">require</span>(<span class="hljs-string">"usp-js"</span>).default + <p>To connect provide necessary info to the default export. (Values will differ depending on setup)</p> + <pre><code class="language-javascript"><span class="hljs-keyword">const</span> connect = <span class="hljs-built_in">require</span>(<span class="hljs-string">"usp-js"</span>).default; <span class="hljs-keyword">const</span> run = <span class="hljs-keyword">async</span> () => { <span class="hljs-comment">// Connect</span> <span class="hljs-keyword">const</span> usp = <span class="hljs-keyword">await</span> connect({ - <span class="hljs-attr">host</span>: <span class="hljs-string">"192.168.1.1"</span>, - <span class="hljs-attr">username</span>: <span class="hljs-string">"admin"</span>, - <span class="hljs-attr">password</span>: <span class="hljs-string">"admin"</span> + <span class="hljs-attr">host</span>: <span class="hljs-string">"my.ip.here"</span>, + <span class="hljs-attr">username</span>: <span class="hljs-string">"username"</span>, + <span class="hljs-attr">password</span>: <span class="hljs-string">"password"</span>, + <span class="hljs-attr">port</span>: <span class="hljs-number">90001</span>, + <span class="hljs-attr">protocol</span>: <span class="hljs-string">"ws"</span>, + <span class="hljs-attr">fromId</span>: <span class="hljs-string">"from::id"</span>, + <span class="hljs-attr">toId</span>: <span class="hljs-string">"to::id"</span>, }); <span class="hljs-comment">// Get property</span> @@ -98,89 +106,91 @@ run();</code></pre> <h1>API</h1> </a> <ul> - <li><p>Connect</p> - <pre><code class="language-javascript"><span class="hljs-comment">// options are based on https://github.com/mqttjs/MQTT.js#mqttconnecturl-options</span> + <li>Connect</li> + </ul> + <pre><code class="language-javascript"><span class="hljs-comment">// options are based on https://github.com/mqttjs/MQTT.js#mqttconnecturl-options</span> <span class="hljs-comment">// they additionaly require fromId and toId, more info: url.here</span> <span class="hljs-keyword">const</span> usp = <span class="hljs-keyword">await</span> connect(options);</code></pre> - </li> + <ul> <li><p>Get</p> <ul> - <li><p>get object - all object end with a dot</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">"Device.Time."</span>); + <li>get object - all object end with a dot</li> + </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">"Device.Time."</span>); <span class="hljs-comment">// => {</span> <span class="hljs-comment">// "CurrentLocalTime": "2020-12-15T12:33:19Z",</span> <span class="hljs-comment">// "Enable": true,</span> <span class="hljs-comment">// ...</span> <span class="hljs-comment">// }</span></code></pre> - </li> - <li><p>get property</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">"Device.Time.CurrentLocalTime"</span>); <span class="hljs-comment">// => "2020-12-15T12:33:19Z"</span></code></pre> - </li> - <li><p>get multiple paths</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get([<span class="hljs-string">"Device.WiFi.Radio.1."</span>, <span class="hljs-string">"Device.WiFi.Radio.2."</span>]); + <ul> + <li>get property</li> + </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">"Device.Time.CurrentLocalTime"</span>); <span class="hljs-comment">// => "2020-12-15T12:33:19Z"</span></code></pre> + <ul> + <li>get multiple paths</li> + </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get([<span class="hljs-string">"Device.WiFi.Radio.1."</span>, <span class="hljs-string">"Device.WiFi.Radio.2."</span>]); <span class="hljs-comment">// => [</span> <span class="hljs-comment">// { ... },</span> <span class="hljs-comment">// { ... }</span> <span class="hljs-comment">// ]</span></code></pre> - </li> - <li><p>get using pattern</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">'Device.Ethernet.Interface.[Alias=="WAN"].CurrentBitRate'</span>); <span class="hljs-comment">// => 0</span></code></pre> - </li> - <li><p>resolve references in get</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">"Device.WiFi.Radio.1."</span>).then(usp.resolve); <span class="hljs-comment">// => { ... }</span> -<span class="hljs-comment">// or if deeper resolution is needed (be careful when using level, going above 3 often causes an infinite reference loop)</span> -<span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">"Device.WiFi.Radio.1."</span>).then(<span class="hljs-function"><span class="hljs-params">msg</span> =></span> usp.resolve(msg, <span class="hljs-number">3</span> <span class="hljs-comment">/* level - defaults to 1 */</span>)); <span class="hljs-comment">// => { ... } </span></code></pre> - </li> + <ul> + <li>get using pattern</li> + </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">'Device.Ethernet.Interface.[Alias=="WAN"].CurrentBitRate'</span>); <span class="hljs-comment">// => 0</span></code></pre> + <ul> + <li>resolve references in get</li> </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.get(<span class="hljs-string">"Device.WiFi.Radio.1."</span>).then(usp.resolve); <span class="hljs-comment">// => { ... }</span> +<span class="hljs-comment">// or if deeper resolution is needed (be careful when using level, going above 3 often causes an infinite reference loop)</span> +<span class="hljs-keyword">await</span> usp + .get(<span class="hljs-string">"Device.WiFi.Radio.1."</span>) + .then(<span class="hljs-function">(<span class="hljs-params">msg</span>) =></span> usp.resolve(msg, <span class="hljs-number">3</span> <span class="hljs-comment">/* level - defaults to 1 */</span>)); <span class="hljs-comment">// => { ... }</span></code></pre> </li> - </ul> - <ul> <li><p>Set</p> <ul> - <li><p>set object - does not need to have all attributes, but some may be required (check USP Reference)</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.set(<span class="hljs-string">"Device.WiFi.Radio.1."</span>, { <span class="hljs-attr">Name</span>: <span class="hljs-string">"radio-1"</span> }); <span class="hljs-comment">// => void</span></code></pre> - </li> - <li><p>set property</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.set(<span class="hljs-string">"Device.WiFi.Radio.1.Name"</span>, <span class="hljs-string">"radio-1"</span>); <span class="hljs-comment">// => void</span></code></pre> - </li> + <li>set object - does not need to have all attributes, but some may be required (check USP Reference)</li> </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.set(<span class="hljs-string">"Device.WiFi.Radio.1."</span>, { <span class="hljs-attr">Name</span>: <span class="hljs-string">"radio-1"</span> }); <span class="hljs-comment">// => void</span></code></pre> + <ul> + <li>set property</li> + </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.set(<span class="hljs-string">"Device.WiFi.Radio.1.Name"</span>, <span class="hljs-string">"radio-1"</span>); <span class="hljs-comment">// => void</span></code></pre> </li> <li><p>Operate - WIP (response message not working yet)</p> <ul> - <li><p>operate without no arguments</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.operate(<span class="hljs-string">"Device.SelfTestDiagnostics()"</span>);</code></pre> - </li> - <li><p>operate with arguments (for required args check USP Reference)</p> - <pre><code class="language-javascript"><span class="hljs-keyword">const</span> [ping, cleanPing] = <span class="hljs-keyword">await</span> usp.operate(<span class="hljs-string">"Device.IP.Diagnostics.IPPing()"</span>); -<span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> ping({ <span class="hljs-attr">Host</span>: <span class="hljs-string">"iopsys.eu"</span> }) -<span class="hljs-keyword">await</span> cleanPing() <span class="hljs-comment">// clears ping subscription (optional)</span></code></pre> - </li> + <li>operate without no arguments</li> + </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.operate(<span class="hljs-string">"Device.SelfTestDiagnostics()"</span>);</code></pre> + <ul> + <li>operate with arguments (for required args check USP Reference)</li> </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">const</span> [ping, cleanPing] = <span class="hljs-keyword">await</span> usp.operate(<span class="hljs-string">"Device.IP.Diagnostics.IPPing()"</span>); +<span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> ping({ <span class="hljs-attr">Host</span>: <span class="hljs-string">"iopsys.eu"</span> }); +<span class="hljs-keyword">await</span> cleanPing(); <span class="hljs-comment">// clears ping subscription (optional)</span></code></pre> </li> - </ul> - <ul> <li><p>Add</p> <ul> - <li><p>add with no arguments - adds a new default object</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.add(<span class="hljs-string">"Device.NAT.PortMapping."</span>); <span class="hljs-comment">// => "Device.NAT.PortMapping.3."</span></code></pre> - </li> - <li><p>add with arguments - adds a new object with provided values</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.add(<span class="hljs-string">"Device.NAT.PortMapping."</span>, { -<span class="hljs-attr">Description</span>: <span class="hljs-string">"webserver1-set"</span>, -<span class="hljs-attr">ExternalPort</span>: <span class="hljs-string">"80"</span>, -<span class="hljs-attr">Protocol</span>: <span class="hljs-string">"TCP"</span>, -<span class="hljs-attr">Interface</span>: <span class="hljs-string">"Device.IP.Interface.1"</span>, -<span class="hljs-attr">Enable</span>: <span class="hljs-string">"true"</span>, -<span class="hljs-attr">InternalClient</span>: <span class="hljs-string">"192.168.1.125"</span>, -<span class="hljs-attr">InternalPort</span>: <span class="hljs-string">"5000"</span>, -}); <span class="hljs-comment">// => "Device.NAT.PortMapping.4."</span></code></pre> - </li> + <li>add with no arguments - adds a new default object</li> </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.add(<span class="hljs-string">"Device.NAT.PortMapping."</span>); <span class="hljs-comment">// => "Device.NAT.PortMapping.3."</span></code></pre> + <ul> + <li>add with arguments - adds a new object with provided values</li> + </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.add(<span class="hljs-string">"Device.NAT.PortMapping."</span>, { + <span class="hljs-attr">Description</span>: <span class="hljs-string">"webserver1-set"</span>, + <span class="hljs-attr">ExternalPort</span>: <span class="hljs-string">"80"</span>, + <span class="hljs-attr">Protocol</span>: <span class="hljs-string">"TCP"</span>, + <span class="hljs-attr">Interface</span>: <span class="hljs-string">"Device.IP.Interface.1"</span>, + <span class="hljs-attr">Enable</span>: <span class="hljs-string">"true"</span>, + <span class="hljs-attr">InternalClient</span>: <span class="hljs-string">"192.168.1.125"</span>, + <span class="hljs-attr">InternalPort</span>: <span class="hljs-string">"5000"</span>, +}); <span class="hljs-comment">// => "Device.NAT.PortMapping.4."</span></code></pre> </li> <li><p>Delete</p> - <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.del(<span class="hljs-string">"Device.NAT.PortMapping.4."</span>); <span class="hljs-comment">// => void</span></code></pre> </li> </ul> + <pre><code class="language-javascript"><span class="hljs-keyword">await</span> usp.del(<span class="hljs-string">"Device.NAT.PortMapping.4."</span>); <span class="hljs-comment">// => void</span></code></pre> </div> </div> <div class="col-4 col-menu menu-sticky-wrap menu-highlight"> diff --git a/src/index.ts b/src/index.ts index c457f79..a5532e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import use from "./protocol"; import config from "./config.json"; -import mqttAsync from "async-mqtt"; import { makeRouter, timer, loopTimer } from "./lib"; /** @@ -44,24 +43,14 @@ const resolveReferences = async ( /** * Shorthand for throwing message-related error */ -const throwMsgError = (path: string | string[]) => ({ - data: { reason }, -}: ErrorMessage) => { - throw { +const throwMsgError = (path: string | string[], reject: (err: any) => void) => ( + err: any +) => { + reject({ type: "error", path: Array.isArray(path) ? path.join(",") : path, - reason, - } as DeviceError; -}; - -/** - * Shorthand for throwing generic error - */ -const throwError = (reason: any): void => { - throw { - type: "error", - reason, - } as DeviceError; + reason: err, + }); }; /** @@ -96,43 +85,60 @@ const decode = (msg: USPMessage): JSType | void => { /** * Wrapper function for sending message (decode message and catch errors) */ -const handleSend = (msg: Promise<USPMessage>, path: string | string[]) => - msg.then(decode).catch(throwMsgError(path)); +const handleSend = ( + msg: Promise<USPMessage>, + path: string | string[], + reject: RejectFn +) => { + try { + return msg.then(decode); + } catch (reason) { + throwMsgError(path, reject)(reason); + } +}; /** * Make get function */ -const makeGet = (send: SendFn) => (path: string | string[]) => +const makeGet = (send: SendFn, reject: RejectFn) => (path: string | string[]) => handleSend( send("get", Array.isArray(path) ? { paths: path } : { path }), - path + path, + reject ) as Promise<JSType>; /** * Make set function */ -const makeSet = (send: SendFn) => (path: string, value: JSType) => +const makeSet = (send: SendFn, reject: RejectFn) => ( + path: string, + value: JSType +) => handleSend( send("set", { path, value, }), - path + path, + reject ) as Promise<void>; /** * Make add function */ -const makeAdd = (send: SendFn) => (path: string, input?: JSObject) => - handleSend(send("add", { path, input }), path) as Promise<string>; +const makeAdd = (send: SendFn, reject: RejectFn) => ( + path: string, + input?: JSObject +) => handleSend(send("add", { path, input }), path, reject) as Promise<string>; /** * Make del function */ -const makeDel = (send: SendFn) => (path: string | string[]) => +const makeDel = (send: SendFn, reject: RejectFn) => (path: string | string[]) => handleSend( send("delete", Array.isArray(path) ? { paths: path } : { path }), - path + path, + reject ) as Promise<void>; const operateSubscriptionPath = "Device.LocalAgent.Subscription."; @@ -142,7 +148,12 @@ type DelFn = (path: string | string[]) => Promise<void>; /** * Make operate function */ -const makeOperate = (send: SendFn, add: AddFn, del: DelFn) => async ( +const makeOperate = ( + send: SendFn, + add: AddFn, + del: DelFn, + reject: RejectFn +) => async ( path: string, opts?: OperateOptions ): Promise<[OperateFunction, OperateCleanupFunction]> => { @@ -165,7 +176,8 @@ const makeOperate = (send: SendFn, add: AddFn, del: DelFn) => async ( input, operateID: ID, }), - path + path, + reject ) as Promise<JSType>; const cleanup = () => del(newSubPath); @@ -181,70 +193,76 @@ const makeOperate = (send: SendFn, add: AddFn, del: DelFn) => async ( const connect = async ( opts: ConnectOptions, events: ConnectEvents = {} -): Promise<Device> => { - // Init values - const router = makeRouter(); - - let restartTimeout: () => void = () => null; - let sessionID: string = ""; - let roles: Role[] = ["none"]; - - // Setup protocol - const protocol = use(opts, { - onMessage: (resp: USPMessage) => { - // Handle message - const handle = router.get(resp.id); - if (isPromiseResult(handle)) { - if (isError(resp)) handle.reject(resp); - else handle.resolve(resp); - } - }, - }); +): Promise<Device> => + new Promise(async (resolve, reject) => { + // Init values + const router = makeRouter(); + + let restartTimeout: () => void = () => null; + let sessionID: string = ""; + let roles: Role[] = ["none"]; - /** - * Send message to device - */ - const send: SendFn = (command, args) => - new Promise((resolve, reject) => { - const msg = protocol.encode(command, args); - router.add(msg.id, { resolve, reject }); - protocol.send(msg); - restartTimeout(); + // Setup protocol + const protocol = use(opts, { + onMessage: (resp: USPMessage) => { + // Handle message + const handle = router.get(resp.id); + if (isPromiseResult(handle)) { + if (isError(resp)) handle.reject(resp.data.reason); + else handle.resolve(resp); + } + }, }); - // Connection timeout timer - const stopTimer = timer(config.connectionTimeout, () => - throwError("Connection timed out") - ); - - // Initialize connection - await protocol - .init() - .then(({ timeout }) => { - stopTimer(); - if (events && events.onTimeout) - restartTimeout = loopTimer(timeout, () => { - protocol.close(); - (events as any).onTimeout(); - }); - }) - .catch(throwError); - - const get = makeGet(send); - const add = makeAdd(send); - const del = makeDel(send); - return { - id: () => sessionID, - roles: () => roles, - get, - set: makeSet(send), - add, - del, - operate: makeOperate(send, add, del), - disconnect: protocol.close, - resolve: async (data: JSType, level = 1) => - await resolveReferences(data, get, level), - }; -}; + /** + * Send message to device + */ + const send: SendFn = (command, args) => + new Promise((resolve, reject) => { + try { + const msg = protocol.encode(command, args); + router.add(msg.id, { resolve, reject }); + protocol.send(msg); + restartTimeout(); + } catch (err) { + reject(err); + } + }); + + // Connection timeout timer + const stopTimer = timer(config.connectionTimeout, () => + reject("Connection timed out") + ); + + // Initialize connection + await protocol + .init() + .then(({ timeout }) => { + stopTimer(); + if (events && events.onTimeout) + restartTimeout = loopTimer(timeout, () => { + protocol.close(); + (events as any).onTimeout(); + }); + }) + .catch(reject); + + const get = makeGet(send, reject); + const add = makeAdd(send, reject); + const del = makeDel(send, reject); + + resolve({ + id: () => sessionID, + roles: () => roles, + get, + set: makeSet(send, reject), + add, + del, + operate: makeOperate(send, add, del, reject), + disconnect: protocol.close, + resolve: async (data: JSType, level = 1) => + await resolveReferences(data, get, level), + }); + }); export default connect; diff --git a/src/protocol/index.ts b/src/protocol/index.ts index 9cc64fe..376a452 100644 --- a/src/protocol/index.ts +++ b/src/protocol/index.ts @@ -1,5 +1,4 @@ import mqttAsync from "async-mqtt"; -import { parse } from "protobufjs"; import * as messages from "./js-usp-protobuf/protoMessage"; import { search, searchAll, unflatten, unwrapArray, unwrapObject } from "./lib"; diff --git a/src/types.d.ts b/src/types.d.ts index bd98cca..a360532 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -248,3 +248,5 @@ interface MQTTRequest { /** Message data */ data: any; } + +type RejectFn = (reason?: any) => void \ No newline at end of file diff --git a/tests/integration/config.json b/tests/integration/config.json index 346f211..9415177 100644 --- a/tests/integration/config.json +++ b/tests/integration/config.json @@ -3,8 +3,8 @@ "host": "192.168.1.1", "username": "admin", "password": "admin", - "fromId": "self::usp-controller", - "toId": "proto::rx_usp_agent_mqtt" + "fromId": "proto::interop-usp-controller", + "toId": "os::002207-E40A24H17A022443" }, "ws": { "host": "192.168.1.1", @@ -12,7 +12,7 @@ "password": "admin", "port": 9001, "protocol": "ws", - "fromId": "self::usp-controller", - "toId": "proto::rx_usp_agent_mqtt" + "fromId": "proto::interop-usp-controller", + "toId": "os::002207-E40A24H17A022443" } } diff --git a/tests/integration/index.test.js b/tests/integration/index.test.js index 197304e..26f37c8 100644 --- a/tests/integration/index.test.js +++ b/tests/integration/index.test.js @@ -1,5 +1,5 @@ const assert = require("assert"); -const connect = require("../../build").default; +const connect = require("../../build/src").default; const config = require("./config.json"); describe("Test general API", () => { @@ -27,7 +27,7 @@ describe("Test general API", () => { it("get incorrect path throws an error", async () => { await device .get("Device.Not.A.Thing") - .catch((err) => assert.strictEqual(err.type, "error")); + .catch((err) => assert.strictEqual(typeof err, "string")); }); // SET @@ -45,7 +45,7 @@ describe("Test general API", () => { it("set throws an error on incorrect path", async () => { await device .set("Device.Not.A.Path", { Cat: "cute" }) - .catch((err) => assert.strictEqual(err.type, "error")); + .catch((err) => assert.strictEqual(typeof err, "string")); }); // OPERATE -- GitLab