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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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',
+ }
+ },
+ ]
+ }
}
-}
+]