From 6143be0ab5d95b65b5ade292ff5ceaab2d60f789 Mon Sep 17 00:00:00 2001 From: "Guan Yu , Chen" <43653109+JasonChenGt@users.noreply.github.com> Date: Mon, 12 Apr 2021 17:32:28 +0800 Subject: [PATCH] fix issues #34 (#35) --- .travis.yml | 6 +- examples/Dummy_Device/index.html | 20 ++ examples/Dummy_Device/js/sa.js | 27 +++ package.json | 4 +- src/context.js | 24 +++ src/dai.js | 203 +++++++++++++++++++ src/dan.js | 335 +++++++++++++++++++++++++++++++ src/dan2.js | 244 ---------------------- src/device-feature.js | 25 +++ src/exceptions.js | 10 + src/index.js | 3 + webpack.config.js | 43 ++-- 12 files changed, 675 insertions(+), 269 deletions(-) create mode 100644 examples/Dummy_Device/index.html create mode 100644 examples/Dummy_Device/js/sa.js create mode 100644 src/context.js create mode 100644 src/dai.js create mode 100644 src/dan.js delete mode 100644 src/dan2.js create mode 100644 src/device-feature.js create mode 100644 src/exceptions.js create mode 100644 src/index.js diff --git a/.travis.yml b/.travis.yml index e1481ee..7ee2f12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,13 +19,13 @@ before_deploy: - export DISTDIR=/tmp/dist - git fetch origin gh-pages:gh-pages - git worktree add ${DISTDIR} gh-pages - - cp ./build-web/dan2-web.js ${DISTDIR}/dan2.js - - cp ./build-web/dan2-web.js ${DISTDIR}/dan2-${TRAVIS_COMMIT}.js + - cp ./build-web/iottalkjs-web.js ${DISTDIR}/iottalkjs.js + - cp ./build-web/iottalkjs-web.js ${DISTDIR}/iottalkjs-${TRAVIS_COMMIT}.js - echo "TRAVIS_TAG = ${TRAVIS_TAG}" - echo "TRAVIS_BRANCH = ${TRAVIS_BRANCH}" - if [ "${TRAVIS_TAG}" ]; then echo 'Add tags build'; - cp ./build-web/dan2-web.js ${DISTDIR}/dan2-${TRAVIS_TAG}.js; + cp ./build-web/iottalkjs-web.js ${DISTDIR}/iottalkjs-${TRAVIS_TAG}.js; fi deploy: diff --git a/examples/Dummy_Device/index.html b/examples/Dummy_Device/index.html new file mode 100644 index 0000000..c2be1e5 --- /dev/null +++ b/examples/Dummy_Device/index.html @@ -0,0 +1,20 @@ + + + + + Dummy_Device + + + + + + + + Dummy_Sensor + _____________ + Dummy_Control + _____________ + + + diff --git a/examples/Dummy_Device/js/sa.js b/examples/Dummy_Device/js/sa.js new file mode 100644 index 0000000..cc87ea8 --- /dev/null +++ b/examples/Dummy_Device/js/sa.js @@ -0,0 +1,27 @@ +$(function () { + function Dummy_Sensor() { + var number = Math.floor((1 + Math.random()) * 0x10000); + $('.IDF_value')[0].innerText = number; + return [number]; + } + + function Dummy_Control(data) { + $('.ODF_value')[0].innerText = data[0]; + } + + var option = { + 'api_url': 'https://iottalk2.tw/csm', + 'device_model': 'Dummy_Device', + 'device_addr': 'c96ca71c-9e48-2a23-2868-acb420a2f105', + 'device_name': 'Dummy', + 'persistent_binding': true, + 'idf_list': [[Dummy_Sensor, ['int']]], + 'odf_list': [Dummy_Control], + 'push_interval': 0, + 'interval': { + 'Dummy_Sensor': 1.5, + } + }; + + new iottalkjs.dai(option).run(); +}); diff --git a/package.json b/package.json index da1e160..ea063b2 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,14 @@ "name": "iottalk-js", "version": "2.0.4", "description": "IoTtalk javascript library SDK", - "main": "dan2.js", + "main": "iottalkjs.js", "directories": { "example": "examples" }, "scripts": { "start": "npx webpack -w --mode development", "build": "npm run build:node && npm run build:web", - "build:node": "npx babel src --out-dir build-node && npx ncc build build-node/dan2.js -o build-node/dist && mv build-node/dist/index.js build-node/dist/dan2.js", + "build:node": "npx babel src --out-dir build-node && npx ncc build build-node/index.js -o build-node/dist && mv build-node/dist/index.js build-node/dist/iottalkjs.js", "build:web": "npx webpack", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/src/context.js b/src/context.js new file mode 100644 index 0000000..bb3d692 --- /dev/null +++ b/src/context.js @@ -0,0 +1,24 @@ +import ChannelPool from './channel-pool.js'; + +export default class { + + constructor() { + this.url = null; + this.app_id = null; + this.name = null; + this.mqtt_host = null; + this.mqtt_port = null; + this.mqtt_username = null; + this.mqtt_password = null; + this.mqtt_client = null; + this.i_chans = new ChannelPool(); + this.o_chans = new ChannelPool(); + this.rev = null; + this.on_signal = null; + this.on_data = null; + this.on_register = null; + this.on_deregister = null; + this.on_connect = null; + this.on_disconnect = null; + } +} diff --git a/src/dai.js b/src/dai.js new file mode 100644 index 0000000..5753d36 --- /dev/null +++ b/src/dai.js @@ -0,0 +1,203 @@ +import DeviceFeature from './device-feature.js'; +import { Client } from './dan.js'; +import { RegistrationError, ArgumentError } from './exceptions.js'; + +export default class { + + constructor(option) { + this.api_url = option['api_url']; + this.device_model = option['device_model']; + this.device_addr = option['device_addr']; + this.device_name = option['device_name']; + this.persistent_binding = option['persistent_binding'] || false; + this.username = option['username']; + this.extra_setup_webpage = option['extra_setup_webpage'] || ''; + this.device_webpage = option['device_webpage'] || ''; + + this.register_callback = option['register_callback']; + this.on_register = option['on_register']; + this.on_deregister = option['on_deregister']; + this.on_connect = option['on_connect']; + this.on_disconnect = option['on_disconnect']; + + this.push_interval = option['push_interval'] != undefined ? option['push_interval'] : 1; + this.interval = option['interval'] || {}; + + this.device_features = {}; + this.flags = {}; + + this.on_signal = this.on_signal.bind(this); + this.on_data = this.on_data.bind(this); + + this.parse_df_profile(option, 'idf'); + this.parse_df_profile(option, 'odf'); + } + + push_data(df_name) { + if (this.device_features[df_name].push_data == null) + return; + let _df_interval = this.interval[df_name] != undefined ? this.interval[df_name] : this.push_interval; + console.debug(`${df_name} : ${this.flags[df_name]} [message / ${_df_interval} s]`); + let _push_interval = setInterval(() => { + let _data = this.device_features[df_name].push_data(); + if (!this.flags[df_name]) { + clearInterval(_push_interval); + return; + } + if (_data === undefined) { + return; + } + this.dan.push(df_name, _data); + + }, _df_interval * 1000); + } + + on_signal(signal, df_list) { + console.log(`Receive signal: ${signal}, ${df_list}`); + if ('CONNECT' == signal) { + df_list.forEach(df_name => { + if (this.flags[df_name]) { + return; + } + this.flags[df_name] = true; + this.push_data(df_name); + }); + } + else if ('DISCONNECT' == signal) { + df_list.forEach(df_name => { + this.flags[df_name] = false; + }); + } + else if ('SUSPEND' == signal) { + // Not use + } + else if ('RESUME' == signal) { + // Not use + } + return true; + } + + on_data(df_name, data) { + try { + this.device_features[df_name].on_data(data); + } catch (err) { + console.error(err); + return false; + } + return true; + } + + df_func_name(df_name) { + if (df_name.match(/_[A-Z]?(I|O)[0-9]?$/i)) { + return df_name.replace('_', '-'); + } + return df_name; + } + + _check_parameter() { + if (!this.api_url) + throw new RegistrationError('api_url is required.'); + + if (!this.device_model) + throw new RegistrationError('device_model not given.'); + + if (this.persistent_binding && !this.device_addr) + throw new ArgumentError('In case of `persistent_binding` set to `True`, ' + + 'the `device_addr` should be set and fixed.'); + + if (Object.keys(this.device_features).length === 0) + throw new RegistrationError('Neither idf_list nor odf_list is empty.'); + } + + run() { + this._check_parameter(); + + this.dan = new Client(); + + let idf_list = []; + let odf_list = []; + + for (const [df_name, df] of Object.entries(this.device_features)) { + if (df.df_type == 'idf') + idf_list.push([df_name, df.df_type]); + else + odf_list.push([df_name, df.df_type]); + } + + const msg = { + 'url': this.api_url, + 'on_signal': this.on_signal, + 'on_data': this.on_data, + 'accept_protos': ['mqtt'], + 'id': this.device_addr, + 'idf_list': idf_list, + 'odf_list': odf_list, + 'name': this.device_name, + 'profile': { + 'model': this.device_model, + 'u_name': this.username, + 'extra_setup_webpage': this.extra_setup_webpage, + 'device_webpage': this.device_webpage, + }, + 'register_callback': this.register_callback, + 'on_register': this.on_register, + 'on_deregister': this.on_deregister, + 'on_connect': this.on_connect, + 'on_disconnect': () => { + for (const key in this.flags) { + this.flags[key] = false; + } + console.debug(`on_disconnect: _flag = ${this.flags}`); + if (on_disconnect) { + return on_disconnect; + } + } + }; + + this.dan.register(msg); + + // FIXME: window is not defined in node.js + window.onbeforeunload = function () { + try { + if (!this.persistent_binding) { + this.dan.deregister(); + } + } catch (error) { + console.error(`dai process cleanup exception: ${error}`); + } + }; + } + + parse_df_profile(option, typ) { + const df_list = `${typ}_list`; + for (let i = 0; i < option[df_list].length; i++) { + let df_name; + let param_type; + let on_data; + let push_data; + if (!Array.isArray(option[df_list][i])) { + df_name = this.df_func_name(option[df_list][i].name); + param_type = null; + on_data = push_data = option[df_list][i]; + } + else if (Array.isArray(option[df_list][i]) && option[df_list][i].length == 2) { + df_name = this.df_func_name(option[df_list][i][0].name); + param_type = option[df_list][i][1]; + on_data = push_data = option[df_list][i][0]; + } + else { + throw new RegistrationError(`Invalid ${df_list}, usage: [df_func, ...] or [[df_func, type], ...]`); + } + + let df = new DeviceFeature({ + 'df_name': df_name, + 'df_type': typ, + 'param_type': param_type, + 'push_data': push_data, + 'on_data': on_data + }); + + this.device_features[df_name] = df; + } + } +} diff --git a/src/dan.js b/src/dan.js new file mode 100644 index 0000000..5925126 --- /dev/null +++ b/src/dan.js @@ -0,0 +1,335 @@ +import Context from './context.js'; +import _UUID from './uuid.js'; +import mqtt from 'mqtt'; +import superagent from 'superagent'; +import { RegistrationError } from './exceptions.js'; + +export class Client { + + constructor() { + this.ctx = new Context(); + this._first_publish = false; + this._is_reconnect = false; + this.on_connect = this.on_connect.bind(this); + this.on_disconnect = this.on_disconnect.bind(this); + } + + publish(channel, message, retained, qos) { + if (!this.ctx.mqtt_client) + throw 'unable to publish without ctx.mqtt_client'; + + if (retained === undefined) + retained = false; + + if (qos === undefined) + qos = 2; + + return new Promise((resolve, reject) => { + this.ctx.mqtt_client.publish(channel, + JSON.stringify(message), + { retain: retained, qos: qos, }, + (err) => { + if (err) { + return reject(err); + } + resolve(); + } + ) + }); + } + + subscribe(channel, qos) { + if (!this.ctx.mqtt_client) + return; + + if (qos === undefined) + qos = 2; + + return new Promise((resolve, reject) => { + this.ctx.mqtt_client.subscribe(channel, + { qos: qos }, + (err, granted) => { + if (err) { + return reject(err); + } + resolve(); + }); + }); + } + + unsubscribe(channel) { + if (!this.ctx.mqtt_client) + return; + + return new Promise((resolve, reject) => { + this.ctx.mqtt_client.unsubscribe(channel, + (err) => { + if (err) { + return reject(err); + } + resolve(); + }); + }); + } + + on_connect() { + console.info('mqtt_connect'); + + let promise_thing; + + if (!this._is_reconnect) { + promise_thing = this.subscribe(this.ctx.o_chans['ctrl']) + .then(() => { + console.log(`Successfully connect to ${this.ctx.url}`); + console.log(`Device ID: ${this.ctx.app_id}`); + console.log(`Device name: ${this.ctx.name}.`); + if (typeof (document) !== "undefined") { + document.title = this.ctx.name; + } + }) + .catch(err => { + throw 'Subscribe to control channel failed'; + }); + } + else { + console.info(`Reconnect: ${this.ctx.name}.`); + promise_thing = this.publish( + this.ctx.i_chans['ctrl'], + { 'state': 'offline', 'rev': this.ctx.rev }, + true // retained message + ); + } + + promise_thing.then(() => { + this.ctx.i_chans.remove_all_df(); + this.ctx.o_chans.remove_all_df(); + + this.publish( + this.ctx.i_chans['ctrl'], + { 'state': 'online', 'rev': this.ctx.rev }, + true // retained message + ).then(() => { + this._first_publish = true; + }); + + this._is_reconnect = true; + + if (this.ctx.on_connect) { + this.ctx.on_connect(); + } + }).catch(err => { + console.error(err); + }); + } + + on_message(topic, message) { + if (topic == this.ctx.o_chans['ctrl']) { + const signal = JSON.parse(message); + let handling_result = null; + switch (signal['command']) { + case 'CONNECT': + if ('idf' in signal) { + const idf = signal['idf']; + this.ctx.i_chans.add(idf, signal['topic']); + handling_result = this.ctx.on_signal(signal['command'], [idf]); + + } else if ('odf' in signal) { + const odf = signal['odf']; + this.ctx.o_chans.add(odf, signal['topic']); + handling_result = this.ctx.on_signal(signal['command'], [odf]); + this.subscribe(this.ctx.o_chans.topic(odf)); + } + break; + case 'DISCONNECT': + if ('idf' in signal) { + const idf = signal['idf']; + this.ctx.i_chans.remove_df(idf); + handling_result = this.ctx.on_signal(signal['command'], [idf]); + + } else if ('odf' in signal) { + const odf = signal['odf']; + this.unsubscribe(this.ctx.o_chans.topic(odf)); + this.ctx.o_chans.remove_df(odf); + handling_result = this.ctx.on_signal(signal['command'], [odf]); + } + break; + } + const res_message = { + 'msg_id': signal['msg_id'], + }; + if (typeof handling_result === 'boolean' && handling_result) { + res_message['state'] = 'ok'; + } else { + res_message['state'] = 'error'; + res_message['reason'] = handling_result[1]; + } + this.publish(this.ctx.i_chans['ctrl'], res_message); + } + else { + let odf = this.ctx.o_chans.df(topic); + if (!odf) + return; + this.ctx.on_data(odf, JSON.parse(message)); + } + } + + on_disconnect() { + console.info(`${this.ctx.name} (${this.ctx.app_id}) disconnected from ${this.ctx.url}.`); + if (this.ctx.on_disconnect) { + this.ctx.on_disconnect(); + } + } + + register(params) { + if (this.ctx.mqtt_client) { + throw new RegistrationError('Already registered'); + } + + this.ctx.url = params['url']; + if (!this.ctx.url || this.ctx.url == '') { + throw new RegistrationError(`Invalid url: ${this.ctx.url}`); + } + + this.ctx.app_id = params['id'] || _UUID(); + + const body = { + 'name': params['name'], + 'idf_list': params['idf_list'], + 'odf_list': params['odf_list'], + 'accept_protos': params['accept_protos'] || 'mqtt', + 'profile': params['profile'], + }; + + // other callbacks + this.ctx.on_register = params['on_register']; + this.ctx.on_deregister = params['on_deregister']; + this.ctx.on_connect = params['on_connect']; + this.ctx.on_disconnect = params['on_disconnect']; + + // filter out the empty `df_list`, in case of empty list, server reponsed 403. + ['idf_list', 'odf_list'].forEach( + x => { + if (Array.isArray(body[x]) && body[x].length == 0) + delete body[x]; + } + ); + + superagent.put(`${this.ctx.url}/${this.ctx.app_id}`) + .type('json') + .accept('json') + .send(body) + .then(res => { + let metadata = res.body; + if (typeof metadata === 'string') { + metadata = JSON.parse(metadata); + } + + this.ctx.name = metadata['name']; + this.ctx.mqtt_host = metadata['url']['host']; + this.ctx.mqtt_port = metadata['url']['ws_port']; + this.ctx.mqtt_username = metadata['username'] || ''; + this.ctx.mqtt_password = metadata['password'] || ''; + this.ctx.i_chans['ctrl'] = metadata['ctrl_chans'][0]; + this.ctx.o_chans['ctrl'] = metadata['ctrl_chans'][1]; + this.ctx.rev = metadata['rev']; + + this.ctx.mqtt_client = mqtt.connect(`${metadata.url['ws_scheme']}://${this.ctx.mqtt_host}:${this.ctx.mqtt_port}`, { + clientId: `iottalk-js-${this.ctx.app_id}`, + username: this.ctx.mqtt_username, + password: this.ctx.mqtt_password, + will: { + topic: this.ctx.i_chans['ctrl'], + // in most case of js DA, it never connect back + payload: JSON.stringify({ 'state': 'offline', 'rev': this.ctx.rev }), + retain: true, + }, + keepalive: 30, // seems 60 is problematic for default mosquitto setup + }); + + this.ctx.mqtt_client.on('connect', this.on_connect); + this.ctx.mqtt_client.on('reconnect', () => { + console.info('mqtt_reconnect'); + }); + this.ctx.mqtt_client.on('disconnect', this.on_disconnect); + this.ctx.mqtt_client.on('error', (error) => { + console.error('mqtt_error', error); + }); + this.ctx.mqtt_client.on('message', (topic, message, packet) => { + this.on_message(topic, message.toString()); // Convert message from Uint8Array to String + }); + + this.ctx.on_signal = params['on_signal']; + this.ctx.on_data = params['on_data']; + + setTimeout(() => { + if (!this._first_publish) { + throw new RegistrationError('MQTT connection timeout'); + } + }, 5000); + + if (this.ctx.on_register) { + this.ctx.on_register(); + } + }) + .catch(err => { + console.error('on_failure', err); + }); + } + + deregister() { + if (!this.ctx.mqtt_client) { + throw new RegistrationError('Not registered'); + } + + this.publish( + this.ctx.i_chans['ctrl'], + { 'state': 'offline', 'rev': this.ctx.rev }, + true + ); + this.ctx.mqtt_client.end(); + + superagent.del(`${this.ctx.url}/${this.ctx.app_id}`) + .type('json') + .accept('json') + .send(JSON.stringify({ 'rev': this.ctx.rev })) + .then(res => { + this.ctx.mqtt_client = null; + if (this.ctx.on_deregister) { + this.ctx.on_deregister(); + } + }, err => { + console.error('deregister fail', err); + }); + } + + push(idf_name, data, qos) { + if (!this.ctx.mqtt_client || !this._first_publish) { + throw new RegistrationError('Not registered'); + } + if (!this.ctx.i_chans.topic(idf_name)) { + return; + } + if (qos === undefined) + qos = 1; + + if (!Array.isArray(data)) { + data = [data]; + } + + this.publish(this.ctx.i_chans.topic(idf_name), data, false, qos); + } +} + +let _default_client = new Client(); + +export function register(...args) { + return _default_client.register(...args); +} + +export function deregister() { + return _default_client.deregister(); +} + +export function push(...args) { + return _default_client.push(...args); +} diff --git a/src/dan2.js b/src/dan2.js deleted file mode 100644 index e8b2b68..0000000 --- a/src/dan2.js +++ /dev/null @@ -1,244 +0,0 @@ -import ChannelPool from './channel-pool.js' -import _UUID from './uuid.js' -import mqtt from 'mqtt' -import superagent from 'superagent' - -let _url; -let _id; -let _mqtt_host; -let _mqtt_port; -let _mqtt_scheme; -let _mqtt_client; -let _i_chans; -let _o_chans; -let _ctrl_i; -let _ctrl_o; -let _on_signal; -let _on_data; -let _rev; - -const publish = function(channel, message, retained, qos) { - if (!_mqtt_client) - { - console.warn('unable to publish without _mqtt_client'); - return; - } - if (retained === undefined) - retained = false; - if (qos === undefined) - qos = 2; - - _mqtt_client.publish(channel, message, { - retain: retained, - qos: qos, - }); -} - -const subscribe = function(channel, qos) { - if (!_mqtt_client) - return; - if (qos === undefined) - qos = 2; - return _mqtt_client.subscribe(channel, {qos: qos}); -} - -const unsubscribe = function(channel) { - if (!_mqtt_client) - return; - return _mqtt_client.unsubscribe(channel); -} - -const on_message = function(topic, message) { - if (topic == _ctrl_o) { - let signal = JSON.parse(message); - let handling_result = null; - switch (signal['command']) { - case 'CONNECT': - if ('idf' in signal) { - let idf = signal['idf']; - _i_chans.add(idf, signal['topic']); - handling_result = _on_signal(signal['command'], [idf]); - - } else if ('odf' in signal) { - let odf = signal['odf']; - _o_chans.add(odf, signal['topic']); - handling_result = _on_signal(signal['command'], [odf]); - subscribe(_o_chans.topic(odf)); - } - break; - case 'DISCONNECT': - if ('idf' in signal) { - let idf = signal['idf']; - _i_chans.remove_df(idf); - handling_result = _on_signal(signal['command'], [idf]); - - } else if ('odf' in signal) { - let odf = signal['odf']; - unsubscribe(_o_chans.topic(odf)); - _o_chans.remove_df(odf); - handling_result = _on_signal(signal['command'], [odf]); - } - break; - } - let res_message = { - 'msg_id': signal['msg_id'], - } - if (typeof handling_result == 'boolean' && handling_result) { - res_message['state'] = 'ok'; - } else { - res_message['state'] = 'error'; - res_message['reason'] = handling_result[1]; - } - publish(_ctrl_i, JSON.stringify(res_message)); - return; - } - else { - let odf = _o_chans.df(topic); - if (!odf) - return; - _on_data(odf, JSON.parse(message)); - } -} - -export const register = function(url, params, callback) { - _url = url; - _id = ('id' in params) ? params['id'] : _UUID(); - _on_signal = params['on_signal']; - _on_data = params['on_data']; - _i_chans = new ChannelPool(); - _o_chans = new ChannelPool(); - - const on_failure = function(err) { - console.error('on_failure', err); - if (callback) - callback(false, err); - }; - - let payload = { - 'name': params['name'], - 'idf_list': params['idf_list'], - 'odf_list': params['odf_list'], - 'accept_protos': params['accept_protos'], - 'profile': params['profile'], - }; - - // filter out the empty `df_list`, in case of empty list, server reponsed 403. - ['idf_list', 'odf_list'].forEach( - x => { - if (Array.isArray(payload[x]) && payload[x].length == 0) - delete payload[x]; - } - ); - - superagent.put(_url + '/' + _id) - .type('json') - .accept('json') - .send(payload) - .end((err, res) => { - if(err) { - on_failure(err); - return; - } - - let metadata = res.body; - console.debug('register metadata', metadata); - if (typeof metadata === 'string') { - metadata = JSON.parse(metadata); - } - _rev = metadata['rev']; - _ctrl_i = metadata['ctrl_chans'][0]; - _ctrl_o = metadata['ctrl_chans'][1]; - _mqtt_host = metadata.url['host']; - _mqtt_port = metadata.url['ws_port']; - _mqtt_scheme = metadata.url['ws_scheme']; - - function on_connect() { - console.info('mqtt_connect'); - _i_chans.remove_all_df(); - _o_chans.remove_all_df(); - publish( - _ctrl_i, - JSON.stringify({'state': 'online', 'rev': _rev}), - true // retained message - ); - subscribe(_ctrl_o); - if (callback) { - callback({ - 'raproto': _url, - 'mqtt': metadata['url'], - 'id': _id, - 'd_name': metadata['name'], - }); - } - } - - _mqtt_client = mqtt.connect(_mqtt_scheme + '://' + _mqtt_host + ':' + _mqtt_port, { - clientId: 'mqttjs_' + _id, - will: { - topic: _ctrl_i, - // in most case of js DA, it never connect back - payload: JSON.stringify({'state': 'offline', 'rev': _rev}), - retain: true, - }, - keepalive: 30, // seems 60 is problematic for default mosquitto setup - }); - _mqtt_client.on('connect', on_connect); - _mqtt_client.on('reconnect', () => { console.info('mqtt_reconnect'); }); - _mqtt_client.on('error', (err) => { console.error('mqtt_error', err); }); - _mqtt_client.on('message', (topic, message, packet) => { - // Convert message from Uint8Array to String - on_message(topic, message.toString()); - }); - - }); -} - -export const deregister = function(callback) { - if (!_mqtt_client) { - if (callback) - return callback(true); - return; - } - - publish( - _ctrl_i, - JSON.stringify({'state': 'offline', 'rev': _rev}) - ); - _mqtt_client.end(); - superagent.del(_url +'/'+ _id) - .set('Content-Type', 'application/json') - .set('Accept', '*/*') - .send(JSON.stringify({'rev': _rev})) - .end((err, res) => { - if(err) { - console.error('deregister fail', err); - if (callback) - return callback(false, err); - } - }); - - if (callback) - return callback(true); -} - -export const push = function(idf_name, data, qos) { - if (!_mqtt_client || !_i_chans.topic(idf_name)) - return; - if(qos === undefined) - qos = 1; - publish(_i_chans.topic(idf_name), JSON.stringify(data), false, qos); -} - -export const UUID = function() { - return _id ? _id : _UUID(); -} - -export const connected = function() { - if( typeof _mqtt_client !== 'object' ) return false; - return _mqtt_client.connected; -} - -export const reconnecting = function() { - if( typeof _mqtt_client !== 'object' ) return false; - return _mqtt_client.reconnecting; -} diff --git a/src/device-feature.js b/src/device-feature.js new file mode 100644 index 0000000..48314c5 --- /dev/null +++ b/src/device-feature.js @@ -0,0 +1,25 @@ +import { ArgumentError } from './exceptions.js'; +export default class { + + constructor(params) { + this.df_name = params['df_name']; + if (!this.df_name) { + throw new ArgumentError('device feature name is required.'); + } + + this.df_type = params['df_type']; // idf | odf + if (this.df_type != 'idf' && this.df_type != 'odf') { + throw new ArgumentError(`${this.df_name} df_type must be "idf" or "odf"`); + } + + this.param_type = params['param_type'] || [null]; + + this.on_data = null + if (params['df_type'] == 'odf' && params['on_data']) + this.on_data = params['on_data']; + + this.push_data = null + if (params['df_type'] == 'idf' && params['push_data']) + this.push_data = params['push_data']; + } +} diff --git a/src/exceptions.js b/src/exceptions.js new file mode 100644 index 0000000..680e312 --- /dev/null +++ b/src/exceptions.js @@ -0,0 +1,10 @@ +class CustomError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + } +} + +export class ArgumentError extends CustomError { } + +export class RegistrationError extends CustomError { } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..c7dd058 --- /dev/null +++ b/src/index.js @@ -0,0 +1,3 @@ +import dai from './dai.js'; +import * as dan from './dan.js'; +export { dai, dan }; diff --git a/webpack.config.js b/webpack.config.js index 92cdd01..f31ce56 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,23 +1,26 @@ const webpack = require('webpack'); -module.exports = { - target: 'web', - entry: __dirname + '/src/dan2.js', - output: { - path: __dirname + '/build-web', - filename: 'dan2-web.js', - library: ['dan2'], - libraryTarget: 'window', - }, - module: { - rules: [ - { - test: /\.m?js$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - } - }, - ] +module.exports = [ + { + mode: 'none', + target: 'web', + entry: __dirname + '/src/index.js', + output: { + path: __dirname + '/build-web', + filename: 'iottalkjs-web.js', + library: 'iottalkjs', + libraryTarget: 'window', + }, + module: { + rules: [ + { + test: /\.m?js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + } + }, + ] + } } -} +]