add websocket feature and dashboard 19/18519/7
authorMinji Park <minjii.park@samsung.com>
Wed, 5 Apr 2017 04:13:39 +0000 (13:13 +0900)
committerJee Hyeok Kim <jihyeok13.kim@samsung.com>
Thu, 6 Apr 2017 06:55:08 +0000 (06:55 +0000)
- added dashboard using websocket
- modified websocket spec related to length 0
- added websocket client

Change-Id: I14bf29702165448d364d7544544519b48866e1c0
Signed-off-by: Jung Seungho <shonest.jung@samsung.com>
Signed-off-by: Minji Park <minjii.park@samsung.com>
Reviewed-on: https://gerrit.iotivity.org/gerrit/18519
Tested-by: jenkins-iotivity <jenkins@iotivity.org>
Reviewed-by: Yeonghun Nam <yeonghun.nam@samsung.com>
Reviewed-by: Jee Hyeok Kim <jihyeok13.kim@samsung.com>
24 files changed:
cloud/dashboard/.gitignore [new file with mode: 0644]
cloud/dashboard/README [new file with mode: 0644]
cloud/dashboard/package.json [new file with mode: 0644]
cloud/dashboard/public/favicon.ico [new file with mode: 0644]
cloud/dashboard/public/index.html [new file with mode: 0644]
cloud/dashboard/src/Client.js [new file with mode: 0644]
cloud/dashboard/src/components/CoapWebsocketClientSample/controlee_temperatureApp.js [new file with mode: 0644]
cloud/dashboard/src/components/CoapWebsocketClientSample/controller_ForTemperature.js [new file with mode: 0644]
cloud/dashboard/src/components/CoapWebsocketClientSample/controller_forAirconControlee.js [new file with mode: 0644]
cloud/dashboard/src/components/CoapWebsocketCodec.js [new file with mode: 0644]
cloud/dashboard/src/components/ConnectDialog.js [new file with mode: 0644]
cloud/dashboard/src/components/ErrorMessage.js [new file with mode: 0644]
cloud/dashboard/src/components/FirmwareManagement.js [new file with mode: 0644]
cloud/dashboard/src/components/MainAppBar.js [new file with mode: 0644]
cloud/dashboard/src/components/MainMenu.js [new file with mode: 0644]
cloud/dashboard/src/components/ResourceList.js [new file with mode: 0644]
cloud/dashboard/src/components/ResourceListToolbar.js [new file with mode: 0644]
cloud/dashboard/src/components/SigninPage.js [new file with mode: 0644]
cloud/dashboard/src/components/SignupButton.js [new file with mode: 0644]
cloud/dashboard/src/index.css [new file with mode: 0644]
cloud/dashboard/src/index.js [new file with mode: 0644]
cloud/stack/src/main/java/org/iotivity/cloud/base/protocols/coap/CoapDecoder.java
cloud/stack/src/main/java/org/iotivity/cloud/base/protocols/coap/CoapEncoder.java
cloud/stack/src/main/java/org/iotivity/cloud/base/protocols/coap/websocket/WebSocketFrameHandler.java

diff --git a/cloud/dashboard/.gitignore b/cloud/dashboard/.gitignore
new file mode 100644 (file)
index 0000000..2ccbe46
--- /dev/null
@@ -0,0 +1 @@
+/node_modules/
diff --git a/cloud/dashboard/README b/cloud/dashboard/README
new file mode 100644 (file)
index 0000000..8798448
--- /dev/null
@@ -0,0 +1,13 @@
+To Build and Run cloud dashboard
+
+1) Install Node.js if you don't have it
+
+    https://nodejs.org/en/download/
+
+2) Install packages that cloud dashboard depends on
+
+    $ npm install
+
+3) Run dashboard with command below, and it will be launched on your browser
+
+    $ npm start
\ No newline at end of file
diff --git a/cloud/dashboard/package.json b/cloud/dashboard/package.json
new file mode 100644 (file)
index 0000000..aa6c289
--- /dev/null
@@ -0,0 +1,31 @@
+{
+  "name": "dashboard",
+  "version": "0.1.0",
+  "private": true,
+    "devDependencies": {
+        "react-scripts": "0.9.0"
+    },
+  "engines": {
+    "node": ">= 0.10"
+  },
+  "dependencies": {
+    "blob-to-buffer": "^1.2.6",
+    "material-ui": "^0.17.0",
+    "node-localstorage": "^1.3.0",
+    "react": "^15.4.2",
+    "react-dom": "^15.4.2",
+    "react-tap-event-plugin": "^2.0.1",
+    "websocket": "^1.0.24",
+    "hashmap": "^2.0.6",
+    "crypto": "0.0.3",
+    "express": "^4.14.1",
+    "json-buffer": "^2.0.11",
+    "system-sleep": "^1.3.0"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test --env=jsdom",
+    "eject": "react-scripts eject"
+  }
+}
diff --git a/cloud/dashboard/public/favicon.ico b/cloud/dashboard/public/favicon.ico
new file mode 100644 (file)
index 0000000..5c125de
Binary files /dev/null and b/cloud/dashboard/public/favicon.ico differ
diff --git a/cloud/dashboard/public/index.html b/cloud/dashboard/public/index.html
new file mode 100644 (file)
index 0000000..e43545a
--- /dev/null
@@ -0,0 +1,35 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <!-- TODO set icon in header -->
+    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
+    <!--
+      Notice the use of %PUBLIC_URL% in the tag above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>Resource Manager</title>
+  </head>
+  <body>
+    <div id="appbar"></div>
+    <div id="menu" style="float: left; width: 15%; height: 91.9vh"></div>
+    <div id="body" style="float: right; width: 85%; height: 91.9vh"></div>
+    <div id="error"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start`.
+      To create a production bundle, use `npm run build`.
+    -->
+  </body>
+</html>
diff --git a/cloud/dashboard/src/Client.js b/cloud/dashboard/src/Client.js
new file mode 100644 (file)
index 0000000..2dc8b4e
--- /dev/null
@@ -0,0 +1,230 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+var EventEmitter = require('events').EventEmitter;
+var WS = require('websocket').w3cwebsocket;
+var convertBlob = require('blob-to-buffer');
+var HashMap = require('hashmap');
+var coapTokenMap = new HashMap();
+var localStorage = window.localStorage;
+if (typeof localStorage === "undefined" || localStorage === null) {
+    var LocalStorage = require('node-localstorage').LocalStorage;
+    localStorage = new LocalStorage('./client');
+}
+
+const coap = require('./components/CoapWebsocketCodec').coap;
+const parse = require('./components/CoapWebsocketCodec').parse;
+const path = require('./components/CoapWebsocketCodec').path;
+
+const CONNECTED = 'connected';
+const DISCONNECTED = 'disconnected';
+const SIGNUP = 'signup';
+const SIGNIN = 'signin';
+const SIGNOUT = 'signout';
+const ERROR = 'error';
+
+var Client = new function() {
+
+    var ws;
+    this.event = new EventEmitter();
+
+    // coap websocket client initialize.
+    this.init = function(address) {
+        console.log('client init: ' + address);
+
+        /* TODO websocket secured */
+        var serverURL = "ws://" + address + "/.well-known/coap";
+        this.ws = new WS(serverURL, 'coap');
+        this.ws.onopen = function() {
+            console.debug('Connected to server ' + address);
+            Client.event.emit(CONNECTED);
+        };
+        this.ws.onclose = function() {
+            console.debug('Disconnected from server ' + address);
+            Client.event.emit(DISCONNECTED);
+        };
+        this.ws.onerror = function() {
+            console.error('Error occurs');
+            Client.event.emit(ERROR, 'Error occurs while websocket connection');
+            Client.event.emit(DISCONNECTED);
+        };
+
+        // response callback.
+        this.ws.onmessage = function(event) {
+            console.debug('Message received -');
+
+            convertBlob(event.data, function (err, buffer) {
+                if (err) throw err;
+
+                var packet = parse(buffer);
+                console.debug(packet);
+                console.debug(packet.getPayloadObject);
+
+                var func = coapTokenMap.get(packet.getToken.toString());
+                func(packet);
+                if(packet.getSequenceNumber === -1){
+                  coapTokenMap.remove(packet.getToken.toString());
+                }
+            });
+        };
+    }
+
+    // coap websocket client close.
+    this.close = function() {
+        console.log('client close');
+        this.ws.close();
+        Client.event.emit(DISCONNECTED);
+    }
+
+    // send sign-up request.
+    this.onSignUp = function(packet) {
+        if(packet.getCode === 68) {
+          Client.event.emit(SIGNUP, packet.getPayloadObject.uid,
+            packet.getPayloadObject.accesstoken);
+        } else {
+          Client.event.emit(ERROR, "SignUp Failed" + packet.getCode);
+        }
+    }
+    this.signUp = function(di, provider, authcode) {
+        console.log('client signUp');
+        var payload = {
+            di: di,
+            authprovider: provider,
+            authcode: authcode,
+            devicetype: "device"
+        };
+        this.ws.send(this.doRequest("POST", path.ACCOUNT_FULL_URI, null, payload, this.onSignUp));
+    }
+
+    // send sign-in request.
+    this.onSignIn = function(packet) {
+        if(packet.getCode === 68) {
+          Client.event.emit(SIGNIN, packet.getPayloadObject.uid,
+            packet.getPayloadObject.accesstoken);
+        } else {
+          Client.event.emit(ERROR, "SignIn Failed" + packet.getCode);
+        }
+    }
+    this.signIn = function(di, uid, accesstoken) {
+        console.log('client signIn');
+        var payload = {
+            di: di,
+            uid: uid,
+            accesstoken: accesstoken,
+            login: true
+        };
+        this.ws.send(this.doRequest("POST", path.ACCOUNT_SESSION_FULL_URI, null, payload, this.onSignIn));
+    }
+
+    // send sign-out request.
+    this.onSignOut = function(packet) {
+        if(packet.getCode === 68) {
+          Client.event.emit(SIGNOUT);
+        } else {
+          Client.event.emit(ERROR, "SignOut Failed" + packet.getCode);
+        }
+    }
+    this.signOut = function(di,  accesstoken) {
+        console.log('client signOut');
+        var payload = {
+            di: di,
+            accesstoken: accesstoken,
+            login: false
+        };
+        this.ws.send(this.doRequest("POST", path.ACCOUNT_SESSION_FULL_URI, null, payload, this.onSignOut));
+    }
+
+    // send resource discovery request.
+    this.discoverResource = function(queries, response) {
+        console.log('client discoverResource ' + queries);
+        this.ws.send(this.doRequest("GET", path.WELL_KNOWN_FULL_URI, queries, null, response));
+    }
+
+    // send control message.
+    this.sendMessage = function(uri, method, payload, queries, response) {
+        console.log('client sendMessage');
+        this.ws.send(this.doRequest(method, uri, queries, payload, response));
+    }
+
+    this.doRequest = function(method, uri, query, payload, response)
+    {
+      var newCoaptoken = require('crypto').randomBytes(8);
+      coapTokenMap.set(newCoaptoken.toString(), response);
+      return coap.createTokenRequest(newCoaptoken, method, uri, query, payload);
+    }
+
+
+    // erase data in local storage
+    this.removeClientData = function(keyArray) {
+        for (var i = 0; i < keyArray.length; i++) {
+            localStorage.removeItem(keyArray[i]);
+        }
+    };
+
+    this.writeClientData = function(keyValArray) {
+        for (var i = 0; i < keyValArray.length; i++) {
+            localStorage.setItem(keyValArray[i][0], keyValArray[i][1]);
+        }
+    };
+
+    this.readClientData = function(key) {
+        return localStorage.getItem(key);
+    };
+
+
+    this.getResourceList = function(discoveryPayload) {
+        if (discoveryPayload === null) {
+            return [];
+        }
+
+        var resourceList = [];
+
+        console.debug("Discovered devices: " + discoveryPayload.length);
+        for (var i = 0; i < discoveryPayload.length; i++) {
+            for (var j = 0; j < discoveryPayload[i].links.length; j++) {
+                console.debug("[" + i + "] " + discoveryPayload[i].links[j].href);
+                resourceList.push({
+                    n:   discoveryPayload[i].n,
+                    di:  discoveryPayload[i].di,
+                    uri: discoveryPayload[i].links[j].href,
+                    rts: this.buildArrayString(discoveryPayload[i].links[j].rt),
+                    ifs: this.buildArrayString(discoveryPayload[i].links[j].if) });
+            }
+        }
+
+        return resourceList;
+    }
+
+    this.buildArrayString = function(array) {
+        var result = '';
+        var seperates = ', ';
+
+        for (var i = 0; i < array.length; i++) {
+            // last data
+            if (i === array.length -1) seperates = '';
+            result += array[i] + seperates;
+        }
+
+        return result;
+    }
+}();
+
+module.exports = Client;
diff --git a/cloud/dashboard/src/components/CoapWebsocketClientSample/controlee_temperatureApp.js b/cloud/dashboard/src/components/CoapWebsocketClientSample/controlee_temperatureApp.js
new file mode 100644 (file)
index 0000000..9d20237
--- /dev/null
@@ -0,0 +1,260 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+const coap = require('../CoapWebsocketCodec').coap;
+const parse = require('../CoapWebsocketCodec').parse;
+const path = require('../CoapWebsocketCodec').path;
+var WebSocketClient = require('websocket').client;
+var client = new WebSocketClient();
+var sleep = require('system-sleep');
+
+var readline = require('readline');
+var r = readline.createInterface({
+       input : process.stdin,
+       output : process.stdout
+});
+
+var seqNum = 1;
+var temperature = 0.0;
+var observeNotify = true;
+var arg = new Array();
+var deviceId = "61646d69-6e44-6576-6963-234231423441";
+
+process.argv.forEach(function(val, index, array) {
+       arg[index] = val;
+});
+
+client.on('connectFailed', function(error) {
+       console.log('Connect Error: ' + error.toString());
+});
+
+client.on('connect', function(connection) {
+       console.log('WebSocket Client Connected');
+
+       connection.on('error', function(error) {
+               console.log("Connection Error: " + error.toString());
+       });
+       connection.on('close', function() {
+               console.log('echo-protocol Connection Closed');
+       });
+       connection.on('message', function(message) {
+               if (message.type != 'binary')
+                       console.log("message type is not binary!!");
+
+               var packet = parse(message.binaryData);
+
+               // console.log();
+               console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
+               console.log('received Token : ' + packet.getToken);
+               console.log('received uriPath : ' + packet.getUriPath);
+               console.log('received UriQuery : ' + packet.getUriQuery);
+               console.log('received payload : ' + packet.getPayloadString);
+               console.log('received getSequenceNumber : ' + packet.getSequenceNumber);
+               console.log('received getMethod : ' + packet.getMethod);
+               console.log('received getCode : ' + packet.getCode);
+               console.log();
+
+               if (packet.getCode < 5) {
+                       console.log("Received Request Message");
+                       switch(packet.getMethod) {
+                       case "GET":
+                               console.log("request GET");
+
+                               var response = coap.createResponse(packet, "CONTENT", {
+                                       temperature : 0.0,
+                                       units : "C"
+                               });
+                               connection.sendBytes(response);
+
+                               break;
+                       case "GET OBSERVE":
+                               console.log("request GET OBSERVE");
+                               while (temperature < 1.3) {
+                                       var response = coap.createNotifyResponse(packet, "CONTENT", {
+                                               temperature : temperature,
+                                               units : "C"
+                                       }, seqNum++);
+                                       connection.sendBytes(response);
+                                       sleep(100);
+                                       temperature += 0.1;
+                               }
+                               break;
+
+                       case "GET OBSERVE CANCEL":
+                               console.log("request GET OBSERVE CANCEL");
+                               observeNotify = false;
+                               var response = coap.createNotifyResponse(packet, "CONTENT", null);
+                               connection.sendBytes(response);
+                               break;
+                       }
+               } else if (packet.getCode < 70) {
+
+                       switch(packet.getUriPath) {
+
+                       case path.ACCOUNT_FULL_URI:
+                               console.log('STEP 2 : Sign in');
+                               var payload = {
+                                       di : deviceId,
+                                       uid : packet.getPayloadObject.uid,
+                                       accesstoken : packet.getPayloadObject.accesstoken,
+                                       login : true
+                               };
+                               connection.sendBytes(signIn(payload));
+                               break;
+
+                       case path.ACCOUNT_SESSION_FULL_URI:
+                               console.log('STEP 3 : publish Resource');
+                               connection.sendBytes(publishResource('rt=oic.wk.rdpub', {
+                                       di : "61646d69-6e44-6576-6963-234231423441",
+                                       n : "Air Conditioner",
+                                       lt : 86400,
+                                       links : [{
+                                               href : "/oic/d",
+                                               rt : ["oic.wk.d", "oic.d.airconditioner"],
+                                               if : ["oic.if.baseline", "oic.if.r"],
+                                               ins : 0,
+                                               type : ["application/json"],
+                                               p : {
+                                                       bm : 1
+                                               }
+                                       }, {
+                                               href : "/oic/p",
+                                               rt : ["oic.wk.p"],
+                                               if : ["oic.if.baseline", "oic.if.r"],
+                                               ins : 0,
+                                               type : ["application/json"],
+                                               p : {
+                                                       bm : 1
+                                               }
+                                       }]
+                               }));
+
+                               connection.sendBytes(publishResource('rt=oic.wk.rdpub', {
+                                       di : "61646d69-6e44-6576-6963-234231423441",
+                                       n : "Air Conditioner",
+                                       lt : 86400,
+                                       links : [{
+                                               href : "/temperature",
+                                               rt : ["oic.r.temperature"],
+                                               if : ["oic.if.baseline"],
+                                               ins : 0,
+                                               type : ["application/json"],
+                                               p : {
+                                                       bm : 3
+                                               }
+                                       }]
+                               }));
+                               break;
+                       case path.RD_FULL_URI:
+                               break;
+
+                       case path.WELL_KNOWN_FULL_URI:
+                               if (packet.getPayloadObject[0].links.length == 1) {
+                               } else {
+                                       console.log('STEP 4 : Send Observe Presence');
+                                       connection.sendBytes(observePresence('di=' + packet.getPayloadObject[0].di));
+                                       console.log('STEP 5 : Send GET Observe Message');
+                                       connection.sendBytes(observeMessage(packet.getPayloadObject[0].links[2].href, 'if=' + packet.getPayloadObject[0].links[2].if[0] + ';rt=' + packet.getPayloadObject[0].links[2].rt[0], null));
+
+                               }
+                               break;
+
+                       case path.DEVICE_PRESENCE_FULL_URI:
+                               console.log('response for DEVICE_PRESENCE_FULL_URI');
+                               break;
+                       }
+
+                       if (packet.getUriPath.indexOf(path.ROUTE_FULL_URI) > -1) {
+                               console.log('temperature : ' + packet.getPayloadObject.temperature);
+                       }
+               } else {
+                       console.log("error : " + packet.getMethod);
+               }
+       });
+
+       function sendCoapPacketforSignUpIn() {
+               if (connection.connected) {
+                       var count = process.argv.length;
+                       switch(count) {
+                       case 4:
+                               console.log('STEP 1 : sign Up');
+                               var payload = {
+                                       di : deviceId,
+                                       authprovider : "github",
+                                       authcode : arg[3],
+                                       devicetype : "device"
+                               };
+                               connection.sendBytes(signUp(payload));
+                               break;
+                       case 5:
+                               console.log('STEP 2 : Sign in');
+                               var payload = {
+                                       di : deviceId,
+                                       uid : arg[3],
+                                       accesstoken : arg[4],
+                                       login : true
+                               };
+                               connection.sendBytes(signIn(payload));
+                               break;
+                       default:
+                               console.log("argument is not valid!!");
+                               break;
+                       }
+               }
+       }
+
+       sendCoapPacketforSignUpIn();
+});
+
+function signUp(payload) {
+       return postMessage(path.ACCOUNT_FULL_URI, null, payload);
+}
+
+function signIn(payload) {
+       return postMessage(path.ACCOUNT_SESSION_FULL_URI, null, payload);
+}
+
+function findResource(uriQury) {
+       return getMessage(path.WELL_KNOWN_FULL_URI, uriQury, null);
+}
+
+function publishResource(uriQury, payload) {
+       return postMessage(path.RD_FULL_URI, uriQury, payload);
+}
+
+function observePresence(uriQury) {
+       return observeMessage(path.DEVICE_PRESENCE_FULL_URI, uriQury, null);
+}
+
+function postMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("POST", uriPath, uriQury, payload);
+}
+
+function getMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("GET", uriPath, uriQury, payload);
+}
+
+function observeMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("GET OBSERVE", uriPath, uriQury, payload);
+};
+
+//client.connect('ws://52.78.151.180:8080/.well-known/coap', 'coap');
+client.connect('ws://' + arg[2] + '/.well-known/coap', 'coap');
diff --git a/cloud/dashboard/src/components/CoapWebsocketClientSample/controller_ForTemperature.js b/cloud/dashboard/src/components/CoapWebsocketClientSample/controller_ForTemperature.js
new file mode 100644 (file)
index 0000000..e851777
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+const coap = require('../CoapWebsocketCodec').coap;
+const parse = require('../CoapWebsocketCodec').parse;
+const path = require('../CoapWebsocketCodec').path;
+var WebSocketClient = require('websocket').client;
+var client = new WebSocketClient();
+
+var readline = require('readline');
+var r = readline.createInterface({
+       input : process.stdin,
+       output : process.stdout
+});
+
+var arg = new Array();
+var deviceId = "ac59e41e-1111-bfd0-9292-20a95af72863";
+
+process.argv.forEach(function(val, index, array) {
+       arg[index] = val;
+});
+
+client.on('connectFailed', function(error) {
+       console.log('Connect Error: ' + error.toString());
+});
+
+client.on('connect', function(connection) {
+       console.log('WebSocket Client Connected');
+
+       connection.on('error', function(error) {
+               console.log("Connection Error: " + error.toString());
+       });
+       connection.on('close', function() {
+               console.log('echo-protocol Connection Closed');
+       });
+       connection.on('message', function(message) {
+               if (message.type != 'binary')
+                       console.log("message type is not binary!!");
+
+               var packet = parse(message.binaryData);
+
+               // console.log();
+               // console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
+               // console.log('received Token : ' + packet.getToken);
+               // console.log('received uriPath : ' + packet.getUriPath);
+               // console.log('received UriQuery : ' + packet.getUriQuery);
+               console.log('received payload : ' + packet.getPayloadString);
+               // console.log('received getMethod : ' + packet.getMethod);
+               // console.log('received getCode : ' + packet.getCode);
+               console.log('received getSequenceNumber : ' + packet.getSequenceNumber);
+               // console.log();
+
+               if (packet.getCode < 70) {
+                       switch(packet.getUriPath) {
+                       case path.ACCOUNT_FULL_URI:
+                               console.log('STEP 2 : Sign in');
+                               var payload = {
+                                       di : deviceId,
+                                       uid : packet.getPayloadObject.uid,
+                                       accesstoken : packet.getPayloadObject.accesstoken,
+                                       login : true
+                               };
+                               connection.sendBytes(signIn(payload));
+                               break;
+
+                       case path.ACCOUNT_SESSION_FULL_URI:
+                               console.log('STEP 3 : findResource rt');
+
+                               connection.sendBytes(findResource(null));
+                               //61646d69-6e44-6576-6963-234231423441 2aaad9a5-a034-62e7-9d39-efd7ec9203d9 ac59e41e-cf97-bfd0-9292-20a95af72863
+                       case path.RD_FULL_URI:
+                               break;
+
+                       case path.WELL_KNOWN_FULL_URI:
+                               if (packet.getPayloadObject[0].links.length == 1) {
+                               } else {
+                                       console.log('STEP 4 : Send Observe Presence');
+                                       connection.sendBytes(observePresence('di=' + packet.getPayloadObject[0].di));
+
+                                       console.log('STEP 5-2 : Send GET Observe Message');
+                                       connection.sendBytes(observeMessage(packet.getPayloadObject[0].links[2].href, 'if=' + packet.getPayloadObject[0].links[2].if[0] + ';rt=' + packet.getPayloadObject[0].links[2].rt[0], null));
+
+                               }
+                               break;
+
+                       case path.DEVICE_PRESENCE_FULL_URI:
+                               console.log('response for DEVICE_PRESENCE_FULL_URI');
+                               break;
+                       }
+
+                       if (packet.getUriPath.indexOf(path.ROUTE_FULL_URI) > -1) {
+                               console.log('temperature : ' + packet.getPayloadObject.temperature);
+                               if(packet.getPayloadObject.temperature == 1.2)
+                               connection.sendBytes(observeCancelMessage(packet.getToken, packet.getUriPath, packet.getUriQuery, null));
+                       }
+               } else {
+                       console.log("error : " + packet.getMethod);
+               }
+       });
+
+       function sendCoapPacketforSignUpIn() {
+               if (connection.connected) {
+                       var count = process.argv.length;
+                       switch(count) {
+                       case 4:
+                               console.log('STEP 1 : sign Up');
+                               var payload = {
+                                       di : deviceId,
+                                       authprovider : "github",
+                                       authcode : arg[3],
+                                       devicetype : "device"
+                               };
+                               connection.sendBytes(signUp(payload));
+                               break;
+                       case 5:
+                               console.log('STEP 2 : Sign in');
+                               var payload = {
+                                       di : deviceId,
+                                       uid : arg[3],
+                                       accesstoken : arg[4],
+                                       login : true
+                               };
+                               connection.sendBytes(signIn(payload));
+                               break;
+                       default:
+                               console.log("argument is not valid!!");
+                               break;
+                       }
+               }
+       }
+
+       sendCoapPacketforSignUpIn();
+});
+
+function signUp(payload) {
+       return postMessage(path.ACCOUNT_FULL_URI, null, payload);
+}
+
+function signIn(payload) {
+       return postMessage(path.ACCOUNT_SESSION_FULL_URI, null, payload);
+}
+
+function findResource(uriQury) {
+       return getMessage(path.WELL_KNOWN_FULL_URI, uriQury, null);
+}
+
+function publishResource(uriQury, payload) {
+       return postMessage(path.RD_FULL_URI, uriQury, payload);
+}
+
+function observePresence(uriQury) {
+       return observeMessage(path.DEVICE_PRESENCE_FULL_URI, uriQury, null);
+}
+
+function postMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("POST", uriPath, uriQury, payload);
+}
+
+function getMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("GET", uriPath, uriQury, payload);
+}
+
+function observeMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("GET OBSERVE", uriPath, uriQury, payload);
+};
+
+function observeCancelMessage(token, uriPath, uriQury, payload) {
+       return coap.createTokenRequest(token, "GET OBSERVE CANCEL", uriPath, uriQury, payload);
+};
+
+//client.connect('ws://52.78.151.180:8080/.well-known/coap', 'coap');
+client.connect('ws://' + arg[2] + '/.well-known/coap', 'coap');
diff --git a/cloud/dashboard/src/components/CoapWebsocketClientSample/controller_forAirconControlee.js b/cloud/dashboard/src/components/CoapWebsocketClientSample/controller_forAirconControlee.js
new file mode 100644 (file)
index 0000000..d2b26ef
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+const coap = require('../CoapWebsocketCodec').coap;
+const parse = require('../CoapWebsocketCodec').parse;
+const path = require('../CoapWebsocketCodec').path;
+var WebSocketClient = require('websocket').client;
+var client = new WebSocketClient();
+
+var readline = require('readline');
+var r = readline.createInterface({
+       input : process.stdin,
+       output : process.stdout
+});
+
+var arg = new Array();
+var deviceId = "ac59e41e-4444-bfd0-9292-20a95af72863";
+
+process.argv.forEach(function(val, index, array) {
+       arg[index] = val;
+});
+
+client.on('connectFailed', function(error) {
+       console.log('Connect Error: ' + error.toString());
+});
+
+client.on('connect', function(connection) {
+       console.log('WebSocket Client Connected');
+
+       connection.on('error', function(error) {
+               console.log("Connection Error: " + error.toString());
+       });
+       connection.on('close', function() {
+               console.log('echo-protocol Connection Closed');
+       });
+       connection.on('message', function(message) {
+               if (message.type != 'binary')
+                       console.log("message type is not binary!!");
+
+               var packet = parse(message.binaryData);
+
+               // console.log();
+               // console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
+               // console.log('received Token : ' + packet.getToken);
+               console.log('received uriPath : ' + packet.getUriPath);
+               // console.log('received UriQuery : ' + packet.getUriQuery);
+               console.log('received payload : ' + packet.getPayloadString);
+               // console.log('received getMethod : ' + packet.getMethod);
+               // console.log('received getCode : ' + packet.getCode);
+               console.log('received getSequenceNumber : ' + packet.getSequenceNumber);
+               // console.log();
+
+               if (packet.getCode < 70) {
+                       switch(packet.getUriPath) {
+                       case path.ACCOUNT_FULL_URI:
+                               console.log('STEP 2 : Sign in');
+                               var payload = {
+                                       di : deviceId,
+                                       uid : packet.getPayloadObject.uid,
+                                       accesstoken : packet.getPayloadObject.accesstoken,
+                                       login : true
+                               };
+                               connection.sendBytes(signIn(payload));
+                               break;
+
+                       case path.ACCOUNT_SESSION_FULL_URI:
+                               console.log('STEP 3 : findResource rt');
+
+                               connection.sendBytes(findResource(null));
+                       //61646d69-6e44-6576-6963-234231423441 2aaad9a5-a034-62e7-9d39-efd7ec9203d9  ac59e41e-cf97-bfd0-9292-20a95af72863
+                       case path.RD_FULL_URI:
+                               break;
+
+                       case path.WELL_KNOWN_FULL_URI:
+                               if (packet.getPayloadObject[0].links.length == 1) {
+                               } else {
+                                       console.log('STEP 4 : Send Observe Presence');
+                                       connection.sendBytes(observePresence('di=' + packet.getPayloadObject[0].di));
+
+                                       console.log('STEP 5-1 : Send GET Message');
+                                       connection.sendBytes(getMessage(packet.getPayloadObject[0].links[2].href, 'if=' + packet.getPayloadObject[0].links[2].if[2]));
+                                       //+ ';rt=' + packet.getPayloadObject[0].links[2].rt[0], null));
+
+                               }
+                               break;
+
+                       case path.DEVICE_PRESENCE_FULL_URI:
+                               console.log('response for DEVICE_PRESENCE_FULL_URI');
+                               break;
+                       }
+
+                       if (packet.getUriPath.indexOf(path.ROUTE_FULL_URI) > -1) {
+                               if (packet.getUriPath.indexOf('aircon') > -1) {
+                                       connection.sendBytes(observeMessage(packet.getPayloadObject[1].href, 'if=' + packet.getPayloadObject[1].if[0] + ';rt=' + packet.getPayloadObject[1].rt[0], null));
+                               } else {
+                                       console.log("power value : " + packet.getPayloadObject.value);
+                               }
+                       }
+               } else {
+                       console.log("error : " + packet.getMethod);
+               }
+       });
+
+       function sendCoapPacketforSignUpIn() {
+               if (connection.connected) {
+                       var count = process.argv.length;
+                       switch(count) {
+                       case 4:
+                               console.log('STEP 1 : sign Up');
+                               var payload = {
+                                       di : deviceId,
+                                       authprovider : "github",
+                                       authcode : arg[3],
+                                       devicetype : "device"
+                               };
+                               connection.sendBytes(signUp(payload));
+                               break;
+                       case 5:
+                               console.log('STEP 2 : Sign in');
+                               var payload = {
+                                       di : deviceId,
+                                       uid : arg[3],
+                                       accesstoken : arg[4],
+                                       login : true
+                               };
+                               connection.sendBytes(signIn(payload));
+                               break;
+                       default:
+                               console.log("argument is not valid!!");
+                               break;
+                       }
+               }
+       }
+
+       sendCoapPacketforSignUpIn();
+});
+
+function signUp(payload) {
+       return postMessage(path.ACCOUNT_FULL_URI, null, payload);
+}
+
+function signIn(payload) {
+       return postMessage(path.ACCOUNT_SESSION_FULL_URI, null, payload);
+}
+
+function findResource(uriQury) {
+       return getMessage(path.WELL_KNOWN_FULL_URI, uriQury, null);
+}
+
+function publishResource(uriQury, payload) {
+       return postMessage(path.RD_FULL_URI, uriQury, payload);
+}
+
+function observePresence(uriQury) {
+       return observeMessage(path.DEVICE_PRESENCE_FULL_URI, uriQury, null);
+}
+
+function postMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("POST", uriPath, uriQury, payload);
+}
+
+function getMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("GET", uriPath, uriQury, payload);
+}
+
+function observeMessage(uriPath, uriQury, payload) {
+       return coap.createRequest("GET OBSERVE", uriPath, uriQury, payload);
+};
+
+function observeCancelMessage(token, uriPath, uriQury, payload) {
+       return coap.createTokenRequest(token, "GET OBSERVE CANCEL", uriPath, uriQury, payload);
+};
+
+//client.connect('ws://52.78.151.180:8080/.well-known/coap', 'coap');
+client.connect('ws://' + arg[2] + '/.well-known/coap', 'coap');
diff --git a/cloud/dashboard/src/components/CoapWebsocketCodec.js b/cloud/dashboard/src/components/CoapWebsocketCodec.js
new file mode 100644 (file)
index 0000000..2bbf24a
--- /dev/null
@@ -0,0 +1,933 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+var empty = new Buffer(0)
+
+// a global index for parsing the options and the payload
+// we can do this as the parsing is a sync operation
+,
+    index,
+    lengthfield
+
+// last five bits are 1
+// 31.toString(2) => '111111'
+,
+    lowerCodeMask = 31,
+    nextMsgId = Math.floor(Math.random() * 65535),
+    codes;
+
+codes = {
+    'GET' : 1,
+    'GET OBSERVE' : 1,
+    'GET OBSERVE CANCEL' : 1,
+    'POST' : 2,
+    'PUT' : 3,
+    'DELETE' : 4,
+
+    'CREATED' : 65,
+    'DELETED' : 66,
+    'VALID' : 67,
+    'CHANGED' : 68,
+    'CONTENT' : 69,
+
+    'BAD_REQUEST' : 128,
+    'UNAUTHORIZED' : 129,
+    'BAD_OPTION' : 130,
+    'FORBIDDEN' : 131,
+    'NOT_FOUND' : 132,
+    'METHOD_NOT_ALLOWED' : 133,
+    'NOT_ACCEPTABLE' : 134,
+
+    'PRECONDITION_FAILED' : 140,
+    'REQUEST_ENTITY_TOO_LARGE' : 141,
+    'UNSUPPORTED_CONTENT_FORMAT' : 143,
+
+    'INTERNAL_SERVER_ERROR' : 160,
+    'NOT_IMPLEMENTED' : 161,
+    'BAD_GATEWAY' : 162,
+    'SERVICE_UNAVAILABLE' : 163,
+    'GATEWAY_TIMEOUT' : 164,
+    'PROXY_NOT_SUPPORTED' : 165,
+};
+
+module.exports.path = {
+    'ACCOUNT_FULL_URI' : '/oic/account',
+    'ACCOUNT_SEARCH_FULL_URI' : '/oic/account/search',
+    'ACCOUNT_SESSION_FULL_URI' : '/oic/account/session',
+    'ACCOUNT_TOKENREFRESH_FULL_URI' : '/oic/account/tokenrefresh',
+    'KEEP_ALIVE_FULL_URI' : '/oic/ping',
+    'ROUTE_FULL_URI' : '/oic/route',
+    'RD_FULL_URI' : '/oic/rd',
+    'WELL_KNOWN_FULL_URI' : '/oic/res',
+    'DEVICE_PRESENCE_FULL_URI' : '/oic/prs',
+};
+
+module.exports.coap = {
+
+    createRequest : function(requestMethod, uriPath, uriQuery, payload) {
+        return createMessage(null, requestMethod, uriPath, uriQuery, payload, null);
+    },
+
+    createTokenRequest : function(token, requestMethod, uriPath, uriQuery, payload) {
+        return createMessage(token, requestMethod, uriPath, uriQuery, payload, null);
+    },
+
+    createResponse : function(request, responseMethod, payload) {
+        return createMessage(request.getToken, responseMethod, request.getUriPath, request.getUriQuery, payload, null);
+    },
+
+    createNotifyResponse : function(request, responseMethod, payload, seqNum) {
+        return createMessage(request.getToken, responseMethod, request.getUriPath, request.getUriQuery, payload, seqNum);
+    }
+};
+
+module.exports.parse = function parse(buffer) {
+
+    index = 1;
+    var result = {
+        getCode : getCode(buffer),
+        getMethod : getMethod(buffer),
+        getToken : getToken(buffer),
+        getUriPath : getUriPath(buffer),
+        getUriPathSegment : getUriPathSegment(buffer),
+        getUriQuery : getUriQuery(buffer),
+        getUriQuerySegment : getUriQuerySegment(buffer),
+        getPayloadString : getPayloadString(buffer),
+        getPayloadObject : getPayloadObject(buffer),
+        getSequenceNumber : getSequenceNumber(buffer),
+
+    };
+
+    return result;
+};
+
+function createMessage(token, requestMethod, uriPath, uriQuery, payload, seqNum) {
+    var tokenValue,
+        codeValue = (codes[requestMethod] > -1 ? codes[requestMethod] : toCode(requestMethod)),
+        optionsValue = new Array(),
+        payloadValue;
+    if (token != null) {
+        tokenValue = new Buffer(token);
+    } else {
+        tokenValue = require('crypto').randomBytes(8);
+    }
+
+    if (payload != null)
+        payloadValue = new Buffer(JSON.stringify(payload));
+    console.log();
+    console.log("Token : " + tokenValue);
+    console.log("Method : " + requestMethod);
+    console.log("Request uri : " + uriPath);
+    console.log("Request query : " + uriQuery);
+    console.log("Request payload : " + payloadValue);
+    console.log("Request seqNum : " + seqNum);
+
+    if (seqNum != null) {
+        var buf = Buffer.allocUnsafe(3);
+        buf.writeUInt8(seqNum & 0xFF, 2);
+        buf.writeUInt8((seqNum >> 8) & 0xFF, 1);
+        buf.writeUInt8((seqNum >> 16) & 0xFF, 0);
+        optionsValue.push({
+            name : 'Observe',
+            value : buf
+        });
+
+    } else {
+        if (requestMethod.indexOf('OBSERVE CANCEL') > -1) {
+            optionsValue.push({
+                name : 'Observe',
+                value : Buffer.alloc(1, 1)
+            });
+        } else if (requestMethod.indexOf('OBSERVE') > -1) {
+            optionsValue.push({
+                name : 'Observe',
+                value : Buffer.alloc(0)
+            });
+        }
+    }
+
+    if (uriPath != null) {
+        uriPath = setUriPath(uriPath);
+        while (uriPath.length != 0) {
+            var path = uriPath.shift();
+            optionsValue.push(path);
+        }
+    }
+
+    optionsValue.push({
+        name : 'Content-Format',
+        value : Buffer.alloc(1, 50)
+    });
+
+    if (uriQuery != null) {
+        uriQuery = setUriQuery(uriQuery);
+        while (uriQuery.length != 0) {
+            var query = uriQuery.shift();
+            optionsValue.push(query);
+        }
+    }
+
+    var packet = encoder({
+        token : tokenValue,
+        code : codeValue,
+        payload : payloadValue,
+        options : optionsValue
+    });
+
+    return packet;
+}
+
+function getkey(value) {
+    var flag = false;
+    var keyVal;
+    for (var key in codes) {
+        if (codes[key] == value) {
+            flag = true;
+            keyVal = key;
+            break;
+        }
+    }
+    if (flag) {
+        return keyVal;
+    } else {
+        return false;
+    }
+}
+
+function getMethod(packet) {
+    var value = toCode(decoder(packet).code);
+    if (value == 1) {
+
+        var observeOption = getSequenceNumber(packet).pop();
+        var observe = 0;
+        if(observeOption.length == 3) {
+            ;
+        } else {
+            observe += observeOption & 0xFF;
+        }
+
+        if (observe == -1)
+            ;
+        else if (observe == 1)
+            return 'GET OBSERVE CANCEL';
+        else
+            return 'GET OBSERVE';
+    }
+    var str = getkey(toCode(decoder(packet).code));
+    return str;
+}
+
+function getCode(packet) {
+    return toCode(decoder(packet).code);
+}
+
+function getToken(packet) {
+    return decoder(packet).token;
+}
+
+function getUriPath(packet) {
+    var uriPath = new String(),
+        value = getOptionValue(packet, 'Uri-Path');
+
+    while (value.length != 0) {
+        uriPath += ("/" + value.pop());
+    }
+
+    return uriPath;
+}
+
+function getUriPathSegment(packet) {
+    var uriPathSegment = new Array(),
+        value = getOptionValue(packet, 'Uri-Path');
+
+    while (value.length != 0) {
+        uriPathSegment.push(value.pop());
+    }
+    return uriPathSegment;
+}
+
+function getPayloadString(packet) {
+    var payload = decoder(packet).payload;
+    if (payload.length != 0)
+        return decoder(packet).payload;
+    return null;
+}
+
+function getPayloadObject(packet) {
+    var payload = decoder(packet).payload;
+    if (payload.length != 0)
+        return JSON.parse(decoder(packet).payload);
+    return null;
+}
+
+function getPayloadValue(packet, property) {
+    if (payload.length != 0) {
+        var payload = JSON.parse(decoder(packet).payload);
+        if (payload.property != null) {
+            return payload.property;
+        }
+    }
+    return null;
+}
+
+function setUriPath(path) {
+    var options = new Array();
+    var pathArray = path.split('/');
+    for (var i = 0; i < pathArray.length; i++) {
+        var value = {
+            name : 'Uri-Path',
+            value : new Buffer(pathArray[i])
+        };
+        if (pathArray[i].length != 0) {
+            options.push(value);
+        }
+    }
+    return options;
+}
+
+function setUriQuery(query) {
+    var options = new Array();
+    var queryArray = query.split(';');
+    for (var i = 0; i < queryArray.length; i++) {
+        var value = {
+            name : 'Uri-Query',
+            value : new Buffer(queryArray[i])
+        };
+        if (queryArray[i].length != 0 && queryArray[i] != null) {
+            options.push(value);
+        }
+    }
+    return options;
+}
+
+function getUriQuery(packet) {
+    var uriQuery = new String(),
+        value = getOptionValue(packet, 'Uri-Query');
+
+    if (value.length == 0 || value == null)
+        return null;
+
+    while (value.length != 0) {
+        uriQuery += value.pop();
+        if (value.length != 0)
+            uriQuery += ";";
+    }
+    return uriQuery;
+}
+
+function getSequenceNumber(packet) {
+    var value = getOptionValue(packet, 'Observe');
+
+    if (value.length == 0)
+        return -1;
+
+    return value;
+}
+
+function getUriQuerySegment(packet) {
+    var uriQuerySegment = new Array(),
+        value = getOptionValue(packet, 'Uri-Path');
+
+    if (value.length == 0)
+        return null;
+
+    while (value.length != 0) {
+        var valueArray = value.pop().toString().split('=');
+        uriQuerySegment.push(valueArray[1]);
+    }
+    return uriQuerySegment;
+}
+
+function getOptionValue(packet, optionName) {
+    var value = new Array(),
+        options = decoder(packet).options;
+    while (options.length != 0) {
+        var option = options.pop();
+        if (option.name == optionName) {
+            value.push(option.value);
+        }
+    }
+    return value;
+}
+
+function encoder(packet) {
+    var buffer,
+        byte,
+        pos = 0,
+        options,
+        i,
+        bufferLength,
+        length;
+
+    packet = fillGenDefaults(packet);
+    options = prepareOptions(packet);
+    length = calculateLength(packet, options);
+    bufferLength = calculateBuffer(packet, length);
+
+    console.log("length for Option and Payload : " + length);
+
+    if (bufferLength > 4294967295 + 65535 + 1 + 14)
+        throw new Error('Max packet size is ' + (4294967295 + 65535 + 1 + 14) + ': current is ' + bufferLength);
+
+    buffer = new Buffer(bufferLength);
+
+    byte = 0;
+    // The Length (Len) field MUST be set to zero
+    // because the WebSockets frame containsthe length.
+    // byte |= calulateExtendedLength(length) << 4;
+    // first four bits are Length
+    byte |= packet.token.length;
+    // last four bits are Token Length
+    buffer.writeUInt8(byte, pos++);
+
+    // if (length < 13) {
+        // ;
+    // } else if (length < 256 + 13 + 1) {
+        // buffer.writeUInt8(length - 13, pos++);
+    // } else if (length < 65535 + 269 + 1) {
+        // buffer.writeUInt16BE(length - 269, pos);
+        // pos += 2;
+    // } else if (length < 4294967295 + 65535 + 1) {
+        // buffer.writeUInt8(length - 65805, pos);
+        // pos += 2;
+    // } else {
+        // console.log('Length must be less than 4GB, current is ' + length);
+    // }
+
+    // code can be humized or not
+    if (codes[packet.code])
+        buffer.writeUInt8(codes[packet.code], pos++);
+    else
+        buffer.writeUInt8(toCode(packet.code), pos++);
+
+    // the token might be an empty buffer
+    packet.token.copy(buffer, pos);
+    pos += packet.token.length;
+
+    // write the options
+    for ( i = 0; i < options.length; i++) {
+        options[i].copy(buffer, pos);
+        pos += options[i].length;
+    }
+
+    if (packet.code !== '0.00' && packet.payload != '') {
+
+        // payload separator
+        buffer.writeUInt8(255, pos++);
+        packet.payload.copy(buffer, pos);
+    }
+
+    return buffer;
+}
+
+module.exports.generater = function generater(packet) {
+    var buffer,
+        byte,
+        pos = 0,
+        options,
+        i,
+        bufferLength,
+        length;
+
+    /* fillGenDefaults : add default values to generate packet */
+    packet = fillGenDefaults(packet);
+    options = prepareOptions(packet);
+    length = calculateLength(packet, options);
+    bufferLength = calculateBuffer(packet, length);
+
+    console.log("length for Option and Payload : " + length);
+
+    if (bufferLength > 4294967295 + 65535 + 1 + 14)
+        throw new Error('Max packet size is ' + (4294967295 + 65535 + 1 + 14) + ': current is ' + bufferLength);
+
+    buffer = new Buffer(bufferLength);
+
+    // first byte
+    byte = 0;
+    // byte |= calulateExtendedLength(length) << 4;
+    // first four bits are Length
+    byte |= packet.token.length;
+    // last four bits are Token Length
+    buffer.writeUInt8(byte, pos++);
+
+    // if (length < 13) {
+        // ;
+    // } else if (length < 256 + 13 + 1) {
+        // buffer.writeUInt8(length - 13, pos++);
+    // } else if (length < 65535 + 269 + 1) {
+        // buffer.writeUInt16BE(length - 269, pos);
+        // pos += 2;
+    // } else if (length < 4294967295 + 65535 + 1) {
+        // buffer.writeUInt8(length - 65805, pos);
+        // pos += 2;
+    // } else {
+        // console.log('Length must be less than 4GB, current is ' + length);
+    // }
+
+
+    // code can be humized or not
+    if (codes[packet.code])
+        buffer.writeUInt8(codes[packet.code], pos++);
+    else
+        buffer.writeUInt8(toCode(packet.code), pos++);
+
+    // the token might be an empty buffer
+    packet.token.copy(buffer, pos);
+    pos += packet.token.length;
+
+    // write the options
+    for ( i = 0; i < options.length; i++) {
+        options[i].copy(buffer, pos);
+        pos += options[i].length;
+    }
+
+    if (packet.code !== '0.00' && packet.payload != '') {
+
+        // payload separator
+        buffer.writeUInt8(255, pos++);
+        packet.payload.copy(buffer, pos);
+    }
+
+    return buffer;
+};
+
+// module.exports.decoder = function decoder(buffer) {
+function decoder(buffer) {
+    index = 1;
+    lengthfield = parseLength(buffer);
+
+    if (lengthfield >= 13) {
+        var extendedLength = parseExtendedLength(buffer, lengthfield);
+    }
+
+    var result = {
+        code : parseCode(buffer),
+        token : parseToken(buffer),
+        options : null,
+        payload : null,
+    };
+
+    if (result.code !== '0.00') {
+        result.options = parseOptions(buffer);
+        result.payload = buffer.slice(index + 1);
+    } else {
+        if (buffer.length != 4)
+            throw new Error('Empty messages must be empty');
+
+        result.options = [];
+        result.payload = empty;
+    }
+
+    return result;
+};
+
+function parseLength(buffer) {
+    var length = buffer.readUInt8(0) >> 4;
+
+    // if (length == 0)
+        // throw new Error('Message Length is 0');
+
+    // console.log('length : ' + length);
+
+    return length;
+}
+
+function parseExtendedLength(buffer, lengthfield) {
+    var extendedLength;
+
+    if (lengthfield == 13) {
+        extendedLength = buffer.slice(index, index++) + 13;
+    } else if (lengthfield == 14) {
+        extendedLength = buffer.slice(index, index + 2) + 269;
+        index += 2;
+    } else if (lengthfield == 15) {
+        extendedLength = buffer.slice(index, index + 4) + 65805;
+        index += 4;
+    } else
+        throw new Error('lengthfield is invalid : current lengthfield is ' + lengthfield);
+
+    // console.log('extendedLength : ' + extendedLength);
+
+    return extendedLength;
+}
+
+function parseCode(buffer) {
+    var byte = buffer.readUInt8(index),
+        code = '' + (byte >> 5) + '.';
+
+    index++;
+
+    byte = byte & lowerCodeMask;
+
+    if (byte < 10)
+        code += '0' + byte;
+    else
+        code += byte;
+
+    // console.log('code : ' + code);
+
+    return code;
+}
+
+function parseToken(buffer) {
+    var length = buffer.readUInt8(0) & 0x0F,
+        token;
+
+    if (length > 8) {
+        throw new Error('Token length not allowed');
+    }
+
+    token = buffer.slice(index, index + length);
+
+    index += length;
+
+    // console.log('token : ' + token);
+
+    return token;
+}
+
+var numMap = {
+    '1' : 'If-Match',
+    '3' : 'Uri-Host',
+    '4' : 'ETag',
+    '5' : 'If-None-Match',
+    '6' : 'Observe',
+    '7' : 'Uri-Port',
+    '8' : 'Location-Path',
+    '11' : 'Uri-Path',
+    '12' : 'Content-Format',
+    '14' : 'Max-Age',
+    '15' : 'Uri-Query',
+    '17' : 'Accept',
+    '20' : 'Location-Query',
+    '35' : 'Proxy-Uri',
+    '39' : 'Proxy-Scheme',
+    '60' : 'Size1'
+};
+
+var optionNumberToString = (function genOptionParser() {
+
+    var code = Object.keys(numMap).reduce(function(acc, key) {
+
+        acc += 'case ' + key + ':\n';
+        acc += '  return \'' + numMap[key] + '\'\n';
+
+        return acc;
+    }, 'switch(number) {\n');
+
+    code += 'default:\n';
+    code += 'return \'\' + number';
+    code += '}\n';
+
+    return new Function('number', code);
+})();
+
+function parseOptions(buffer) {
+
+    var byte,
+        number = 0,
+        delta,
+        length,
+        nextOption = true,
+        options = [],
+        option;
+
+    while (index < buffer.length) {
+        byte = buffer.readUInt8(index);
+
+        if (byte === 255 || index > buffer.length) {
+            break;
+        }
+
+        delta = byte >> 4;
+        length = byte & 15;
+
+        index += 1;
+
+        if (delta === 13) {
+            delta = buffer.readUInt8(index) + 13;
+            index += 1;
+        } else if (delta === 14) {
+            delta = buffer.readUInt16BE(index) + 269;
+            index += 2;
+        } else if (delta === 15) {
+            throw new Error('Wrong option delta');
+        }
+
+        if (length === 13) {
+            length = buffer.readUInt8(index) + 13;
+            index += 1;
+        } else if (length === 14) {
+            length = buffer.readUInt16BE(index) + 269;
+            index += 2;
+        } else if (length === 15) {
+            throw new Error('Wrong option length');
+        }
+
+        number += delta;
+
+        var name = optionNumberToString(number),
+            value = buffer.slice(index, index + length);
+
+        // console.log("name : " + name);
+        // console.log("value : " + value.toString());
+        // console.log();
+
+        options.push({
+            name : name,
+            value : value
+        });
+
+        index += length;
+    }
+
+    return options;
+}
+
+function toCode(code) {
+    var split = code.split && code.split('.'),
+        by = 0;
+
+    if (split && split.length === 2) {
+        by |= parseInt(split[0]) << 5;
+        by |= parseInt(split[1]);
+    } else {
+
+        if (!split)
+            code = parseInt(code);
+
+        by |= (code / 100) << 5;
+        by |= (code % 100);
+    }
+
+    return by;
+}
+
+function fillGenDefaults(packet) {
+    if (!packet)
+        packet = {};
+
+    if (!packet.payload)
+        packet.payload = empty;
+
+    if (!packet.token)
+        packet.token = empty;
+
+    if (packet.token.length > 8)
+        throw new Error('Token too long');
+
+    if (!packet.code)
+        packet.code = '0.01';
+
+    if (!packet.options)
+        packet.options = [];
+
+    return packet;
+}
+
+function calculateBuffer(packet, length) {
+    var extendedLengthValue = calulateExtendedLength(length),
+        extendLength;
+
+    if (extendedLengthValue < 13) {
+        extendLength = 0;
+    } else if (extendedLengthValue == 13) {
+        extendLength = 1;
+    } else if (extendedLengthValue == 14) {
+        extendLength = 2;
+    } else if (extendedLengthValue == 15) {
+        extendLength = 4;
+    } else {
+        console.log('Length must be less than 4GB, current is ' + length);
+    }
+
+    length += (2 + packet.token.length); //+ extendLength);
+
+    return length;
+}
+
+function calculateLength(packet, options) {
+
+    var length = packet.payload.length,
+        i,
+        lengthValue;
+
+    if (packet.payload != '')
+        length += 1;
+
+    for ( i = 0; i < options.length; i++) {
+        length += options[i].length;
+    }
+
+    return length;
+}
+
+function calulateExtendedLength(length) {
+    var lengthValue;
+    if (length < 13) {
+        lengthValue = length;
+    } else if (length < 256 + 13 + 1) {
+        lengthValue = 13;
+    } else if (length < 65535 + 269 + 1) {
+        lengthValue = 14;
+    } else if (length < 4294967295 + 65535 + 1) {
+        lengthValue = 15;
+    } else {
+        console.log('Length must be less than 4GB, current is ' + length);
+    }
+    return lengthValue;
+}
+
+var optionStringToNumber = (function genOptionParser() {
+
+    var code = Object.keys(numMap).reduce(function(acc, key) {
+
+        acc += 'case \'' + numMap[key] + '\':\n';
+        acc += '  return \'' + key + '\'\n';
+
+        return acc;
+    }, 'switch(string) {\n');
+
+    code += 'default:\n';
+    code += 'return parseInt(string)';
+    code += '}\n';
+
+    return new Function('string', code);
+})();
+
+var nameMap = Object.keys(numMap).reduce(function(acc, key) {
+    acc[numMap[key]] = key;
+    return acc;
+}, {});
+
+function optionSorter(a, b) {
+    a = a.name;
+    b = b.name;
+
+    a = parseInt(nameMap[a] || a);
+    b = parseInt(nameMap[b] || b);
+    console.log("a : " + a + ", b : " + b);
+    if (a < b)
+        return -1;
+    if (a > b)
+        return 1;
+
+    return 0;
+}
+
+function prepareOptions(packet) {
+    var options = [],
+        total = 0,
+        delta,
+        buffer,
+        byte,
+        option,
+        i,
+        bufferSize,
+        pos,
+        value;
+
+
+    // packet.options.sort(optionSorter);
+
+    //console.log("path in prepareOptions : " + getUriPath(packet));
+
+    /* add delta, length and value to buffer for each option in order */
+    console.log();
+    console.log('packet.options.length : ' + packet.options.length);
+    console.log();
+
+    for ( i = 0; i < packet.options.length; i++) {
+        pos = 0;
+        option = packet.options[i].name;
+        delta = optionStringToNumber(option) - total;
+        value = packet.options[i].value;
+
+        // console.log('turn : ' + i);
+        // console.log('optionStringToNumber(option) : ' + optionStringToNumber(option));
+        // console.log('delta : ' + delta);
+        // console.log('value.length : ' + value.length);
+        // console.log('value : ' + value);
+
+        // max option length is 1 header, 2 ext numb, 2 ext length
+        buffer = new Buffer(value.length + 5);
+
+        byte = 0;
+
+        if (delta < 13) {
+            byte |= delta << 4;
+        } else if (delta < 255 + 13 + 1) {
+            byte |= 13 << 4;
+        } else if (delta < 65535 + 269 + 1) {
+            byte |= 14 << 4;
+        } else {
+            byte != 15 << 4;
+            console.log("Message Format Error");
+        }
+
+        if (value.length < 13) {
+            byte |= value.length;
+        } else if (value.length < 255 + 13 + 1) {
+            byte |= 13;
+        } else if (value.length < 65535 + 269 + 1) {
+            byte |= 14;
+        } else {
+            byte != 15 << 4;
+            console.log("Message Format Error");
+        }
+
+        // console.log("byte : " + byte);
+        buffer.writeUInt8(byte, pos++);
+
+        if (delta < 13) {
+            ;
+        } else if (delta < 255 + 13 + 1) {
+            buffer.writeUInt8(delta - 13, pos++);
+        } else if (delta < 65535 + 269 + 1) {
+            buffer.writeUInt16BE(delta - 269, pos);
+            pos += 2;
+        } else {
+            console.log("Message Format Error");
+        }
+
+        if (value.length < 13) {
+            ;
+        } else if (value.length < 269) {
+            buffer.writeUInt8(value.length - 13, pos++);
+        } else if (value.length < 65535 + 269 + 1) {
+            buffer.writeUInt16BE(value.length - 269, pos);
+            pos += 2;
+        } else {
+            console.log("Message Format Error");
+        }
+
+        value.copy(buffer, pos);
+        pos += value.length;
+        total += delta;
+        options.push(buffer.slice(0, pos));
+        // console.log('total : ' + total);
+        // console.log('pos : ' + pos);
+        // console.log('buffer : ' + buffer);
+        // console.log();
+    }
+
+    return options;
+}
\ No newline at end of file
diff --git a/cloud/dashboard/src/components/ConnectDialog.js b/cloud/dashboard/src/components/ConnectDialog.js
new file mode 100644 (file)
index 0000000..5f03c46
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import TextField from 'material-ui/TextField';
+import Checkbox from 'material-ui/Checkbox';
+import RaisedButton from 'material-ui/RaisedButton';
+import Dialog from 'material-ui/Dialog';
+
+const Client = require('../Client');
+
+const style = {
+    dialog: {
+        width: '500px'
+    },
+
+    actions: {
+        textAlign: 'center',
+    },
+
+    actionCheckbox: {
+        marginLeft: '100px',
+        width: '200px'
+    },
+
+    error: {
+        textAlign: 'left',
+    }
+}
+
+const errorText = 'This field is required';
+
+class ConnectDialog extends React.Component {
+
+    handleChange = (event) => {
+        if (event.target.value.length !== 0) {
+            this.setState({ error : '' });
+        } else {
+            this.setState({ error : errorText });
+        }
+
+        this.setState({
+          address: event.target.value,
+        });
+    };
+
+    handleSubmit = () => {
+        if (this.state.address.length === 0) {
+            this.setState({ error : errorText });
+            return;
+        }
+
+        Client.writeClientData([['address', this.state.address]]);
+
+        console.log('input address: ' + this.state.address);
+        Client.init(this.state.address);
+    };
+
+    onAutoConnectChecked = (event, isInputChecked) => {
+        this.setState({ autoconnect: isInputChecked });
+
+        Client.writeClientData([['autoconnect', isInputChecked]]);
+    };
+
+    constructor(props, context) {
+        super(props, context);
+        this.state = {
+                   open: true,
+                   autoconnect: props.autoconnect,
+                   error: '',
+                   address: props.address,
+               };
+
+        if(this.state.autoconnect === true) {
+          Client.init(this.state.address);
+        }
+    };
+
+    render() {
+        const actions = [
+            <TextField
+                floatingLabelText="Server address"
+                defaultValue={this.state.address}
+                hintText="127.0.0.1:80"
+                errorText={this.state.error}
+                errorStyle={style.error}
+                onChange={this.handleChange.bind(this)}
+            />, <br />, <br />,
+            <Checkbox
+                label="Auto Connect"
+                defaultChecked={this.state.autoconnect}
+                style={style.actionCheckbox}
+                onCheck={this.onAutoConnectChecked}
+            />, <br />, <br />,
+            <RaisedButton
+                label="Connect"
+                primary={true}
+                onTouchTap={this.handleSubmit.bind(this)}
+            />,
+        ];
+
+        return (
+            <MuiThemeProvider>
+                <div>
+                    <Dialog
+                        title="Connect to cloud"
+                        titleStyle={{ fontWeight: 'bold' }}
+                        actions={actions}
+                        modal={true}
+                        open={this.state.open}
+                        contentStyle={style.dialog}
+                        actionsContainerStyle={style.actions}>
+                    </Dialog>
+                </div>
+            </MuiThemeProvider>
+        );
+    }
+};
+
+export default ConnectDialog;
diff --git a/cloud/dashboard/src/components/ErrorMessage.js b/cloud/dashboard/src/components/ErrorMessage.js
new file mode 100644 (file)
index 0000000..8b8da5b
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import Snackbar from 'material-ui/Snackbar';
+
+class ErrorMessage extends React.Component {
+
+    constructor(props, context) {
+        super(props, context);
+        var strMessage = 'ERROR, ' + props.message;
+        this.state = {
+            open: true,
+            message: strMessage
+        };
+    }
+
+    handleRequestClose = () => {
+        this.setState({
+            open: false,
+        });
+    };
+
+    render() {
+        return (
+            <MuiThemeProvider>
+                <Snackbar
+                    open={this.state.open}
+                    message={this.state.message}
+                    autoHideDuration={3000}
+                    onRequestClose={this.handleRequestClose}
+                />
+            </MuiThemeProvider>
+        );
+    }
+}
+
+export default ErrorMessage;
\ No newline at end of file
diff --git a/cloud/dashboard/src/components/FirmwareManagement.js b/cloud/dashboard/src/components/FirmwareManagement.js
new file mode 100644 (file)
index 0000000..4584831
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import {List, ListItem} from 'material-ui/List';
+import RaisedButton from 'material-ui/RaisedButton';
+import TextField from 'material-ui/TextField';
+
+const Client = require('../Client');
+
+const style = {
+    basic: {
+        padding: 30
+    },
+
+    button: {
+        margin: 7,
+    },
+}
+
+class FirmwareHandler extends React.Component {
+    responseCallback = (packet) => {
+        console.debug("response received from " + this.state.uri);
+        var response = packet.getMethod + '\n';
+        if (packet.getPayloadObject !== null) {
+            response += packet.getPayloadString;
+        }
+        this.setState({ response: response });
+    }
+
+    handleFirmwareChanged = (event, newValue) => {
+        this.setState({ firmwarelink: newValue })
+    };
+
+    handleNewVersionChanged = (event, newValue) => {
+        this.setState({ newversion: newValue })
+    };
+
+    handleButtonClicked = (method) => {
+        var payload = '{"packageuri":"' + this.state.firmwarelink
+        + '", "newversion": "' + this.state.newversion + '"}';
+        Client.sendMessage(this.state.uri, method, JSON.parse(payload), null, this.responseCallback);
+    };
+
+    onFirmware = (packet) => {
+        console.debug("response received from " + this.state.uri);
+        var response = packet.getMethod + '\n';
+        if (packet.getPayloadObject !== null) {
+            response += packet.getPayloadString;
+        }
+        this.setState({ response: response });
+    }
+
+    constructor(props, context) {
+        super(props, context);
+        this.state = {
+            uri: props.uri,
+            firmwarelink: null,
+            newversion: null,
+            currentversion: null,
+            response: '',
+        }
+
+        Client.sendMessage(this.state.uri, "GET", null, null, this.onFirmware);
+    }
+
+    render() {
+        return (
+            <div>
+                <TextField
+                    floatingLabelText="Firmware Link"
+                    fullWidth={true}
+                    multiLine={true}
+                    rows={1}
+                    rowsMax={1}
+                    floatingLabelFixed={true}
+                    style={{ textAlign: 'left'}}
+                    onChange={this.handleFirmwareChanged}
+                />
+                <TextField
+                    floatingLabelText="New version"
+                    fullWidth={true}
+                    multiLine={true}
+                    rows={1}
+                    rowsMax={1}
+                    floatingLabelFixed={true}
+                    style={{ textAlign: 'left'}}
+                    onChange={this.handleNewVersionChanged}
+                />
+                <RaisedButton backgroundColor="#BDBDBD" labelColor="#FFFFFF" style={style.button}
+                    label="Register" onTouchTap={this.handleButtonClicked.bind(this, "POST")}
+                />
+                <TextField
+                    floatingLabelText="response"
+                    fullWidth={true}
+                    multiLine={true}
+                    rows={3}
+                    rowsMax={3}
+                    floatingLabelFixed={true}
+                    style={{ textAlign: 'left'}}
+                    value={this.state.response}
+                />
+            </div>
+        );
+    }
+};
+
+class FirmwareManagement extends React.Component {
+
+  constructor(props, context) {
+      super(props, context);
+      this.state = {
+          devices: props.devices
+      };
+  };
+
+  makeListItem = (uri) => {
+      var handler =
+          <FirmwareHandler uri={uri} />;
+
+      return handler;
+  }
+
+    render() {
+        return (
+          <MuiThemeProvider>
+              <div>
+              <List>
+                  <b>Devices</b>
+                  {this.state.devices.map( (row, index) => {
+                      if (row.size === 0) {
+                          return (<p key='notfound' style={{ fontWeight: 'bold' }}> No Device found </p>);
+                      }
+                      return ( <ListItem key={index} primaryText={index + ' ' + row.n}
+                          secondaryText={ <p> uri: {row.uri} <br /> di: {row.di} </p> }
+                          secondaryTextLines={2}
+                          nestedItems={[<ListItem key={index + '_item'} disabled={true}>
+                          {this.makeListItem(row.uri)} </ListItem>]}
+                          nestedListStyle={{ textAlign:'right' }} primaryTogglesNestedList={true}
+                          innerDivStyle={{ fontWeight: 'bold' }}/>
+                      );})
+                  }
+              </List>
+              </div>
+          </MuiThemeProvider>
+        );
+    }
+}
+
+export default FirmwareManagement;
diff --git a/cloud/dashboard/src/components/MainAppBar.js b/cloud/dashboard/src/components/MainAppBar.js
new file mode 100644 (file)
index 0000000..ede3c3e
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import AppBar from 'material-ui/AppBar';
+import IconButton from 'material-ui/IconButton';
+import IconMenu from 'material-ui/IconMenu';
+import MenuItem from 'material-ui/MenuItem';
+import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert';
+import {orange700, white} from 'material-ui/styles/colors';
+
+const Client = require('../Client');
+
+const style = {
+    basic: {
+        backgroundColor: orange700
+    },
+
+    font: {
+        fontWeight: 'bold'
+    }
+}
+
+const handleSignout = () => {
+    console.debug("sign out clicked");
+    Client.signOut();
+};
+
+const handleDisconnect = () => {
+    console.debug("disconnect clicked");
+    Client.close();
+};
+
+const Menu = (props) => (
+    <IconMenu
+        iconButtonElement={
+            <IconButton><MoreVertIcon color={white}/></IconButton>
+        }
+        targetOrigin={{horizontal: 'right', vertical: 'top'}}
+        anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
+    >
+        <MenuItem primaryText="Help" />
+        {props.signin ? <MenuItem primaryText="Sign out" onTouchTap={handleSignout} />: null}
+        <MenuItem primaryText="Server setting" onTouchTap={handleDisconnect}/>
+    </IconMenu>
+);
+Menu.muiName = 'IconMenu';
+
+class MainAppBar extends React.Component {
+
+    constructor(props, context) {
+        super(props, context);
+
+        this.state = {
+            signin: props.signin
+        }
+    };
+
+    render() {
+        return (
+            <MuiThemeProvider>
+                <div>
+                    <AppBar
+                        /* TODO set title */
+                        title="Resource Manager"
+                        titleStyle={style.font}
+                        /* TODO add icon */
+                        //iconElementLeft={ }
+                        iconElementRight={ <Menu signin={this.state.signin} /> }
+                        showMenuIconButton={false}
+                        style={style.basic}
+                    />
+                </div>
+            </MuiThemeProvider>
+        );
+    }
+};
+
+export default MainAppBar;
diff --git a/cloud/dashboard/src/components/MainMenu.js b/cloud/dashboard/src/components/MainMenu.js
new file mode 100644 (file)
index 0000000..fdfeb7c
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import {List, ListItem} from 'material-ui/List';
+import Divider from 'material-ui/Divider';
+import {orange700, white} from 'material-ui/styles/colors';
+
+import ResourceList from './ResourceList';
+import ResourceListToolbar from './ResourceListToolbar';
+import FirmwareManagement from './FirmwareManagement';
+
+const Client = require('../Client');
+
+const ERROR = 'error';
+
+const style = {
+    list: {
+        backgroundColor: orange700,
+        height: '100%'
+    },
+
+    text: {
+        color: white,
+    }
+}
+
+function appendChildren(id, numChildren) {
+    var parent = document.getElementById(id);
+    while (parent.firstChild) {
+      parent.removeChild(parent.firstChild);
+    }
+
+    for (var i = 0; parent.childElementCount <= numChildren; i++) {
+        parent.appendChild(document.createElement('child' + i));
+    }
+};
+
+class MainMenu extends React.Component {
+
+  onDiscover = (packet) => {
+    if(packet.getCode !== 69)
+    {
+      Client.event.emit(ERROR, "Resource Discover Failed " + packet.getCode);
+      return;
+    }
+
+    var resources = Client.getResourceList(packet.getPayloadObject);
+
+    appendChildren('body', 2);
+    ReactDOM.render(
+        <ResourceListToolbar />,
+        document.getElementById('body').children[0]
+    );
+    ReactDOM.render(
+        <ResourceList data={resources} />,
+        document.getElementById('body').children[1]
+    );
+  };
+
+  onFirmwareResources = (packet) =>{
+    if(packet.getCode !== 69)
+    {
+      Client.event.emit(ERROR, "Firmware Discover Failed " + packet.getCode);
+      return;
+    }
+
+    var resources = Client.getResourceList(packet.getPayloadObject);
+
+    appendChildren('body', 1);
+    ReactDOM.render(
+        <FirmwareManagement devices={resources} />,
+        document.getElementById('body').children[0]
+    );
+  };
+
+    handleDiscovery = () => {
+        Client.discoverResource(null, this.onDiscover);
+    };
+
+    handleFirmware = () => {
+        Client.discoverResource("rt=x.samsung.firmware", this.onFirmwareResources);
+    };
+
+    constructor(props, context) {
+        super(props, context);
+        this.state = {
+            signin: props.signin
+        }
+    };
+
+    render() {
+        return (
+            /* TODO add log page */
+            <MuiThemeProvider>
+                <List style={style.list}>
+                    <ListItem primaryText='Resource list' style={style.text} disabled={!this.state.signin} onTouchTap={this.handleDiscovery} />
+                    <Divider />
+                    <ListItem primaryText='Firmware management' style={style.text} disabled={!this.state.signin} onTouchTap={this.handleFirmware} />
+                    <Divider />
+                    <ListItem primaryText='Log view' style={style.text} />
+                </List>
+            </MuiThemeProvider>
+        );
+    };
+};
+
+export default MainMenu;
diff --git a/cloud/dashboard/src/components/ResourceList.js b/cloud/dashboard/src/components/ResourceList.js
new file mode 100644 (file)
index 0000000..a69b267
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import {List, ListItem} from 'material-ui/List';
+import Subheader from 'material-ui/Subheader';
+import RaisedButton from 'material-ui/RaisedButton';
+import TextField from 'material-ui/TextField';
+
+const Client = require('../Client');
+
+const style = {
+    basic: {
+        padding: 30
+    },
+
+    button: {
+        margin: 7,
+    },
+}
+
+/* TODO handle payload json syntax error */
+// Message handler & react component for each found resource.
+class MessageHandler extends React.Component {
+    responseCallback = (packet) => {
+        console.debug("response received from " + this.state.uri);
+        var response = packet.getMethod + '\n';
+        if (packet.getPayloadObject !== null) {
+            response += packet.getPayloadString;
+        }
+        this.setState({ response: response });
+    }
+
+    handleQueryChanged = (event, newValue) => {
+        this.setState({ queries: newValue })
+    };
+
+    handlePayloadChanged = (event, newValue) => {
+        this.setState({ payload: newValue })
+    };
+
+    handleButtonClicked = (method) => {
+        Client.sendMessage(this.state.uri, method, JSON.parse(this.state.payload), this.state.queries, this.responseCallback);
+        if(method === "GET OBSERVE"){
+          this.setState({observeState : "GET OBSERVE CANCEL"})
+        }
+        else if(method === "GET OBSERVE CANCEL"){
+          this.setState({observeState : "GET OBSERVE"})
+        }
+    };
+
+    constructor(props, context) {
+        super(props, context);
+        this.state = {
+            uri: props.uri,
+            payload: null,
+            queries: null,
+            response: '',
+            observeState: "GET OBSERVE"
+        }
+    }
+
+    render() {
+        return (
+            <div>
+                <TextField
+                    key={this.state.uri + 'queries'}
+                    floatingLabelText="additional query"
+                    fullWidth={true}
+                    multiLine={true}
+                    rows={1}
+                    rowsMax={1}
+                    floatingLabelFixed={true}
+                    style={{ textAlign: 'left'}}
+                    onChange={this.handleQueryChanged}
+                />
+                <TextField
+                    key={this.state.uri + 'payload'}
+                    floatingLabelText="payload"
+                    fullWidth={true}
+                    multiLine={true}
+                    rows={5}
+                    rowsMax={5}
+                    floatingLabelFixed={true}
+                    style={{ textAlign: 'left'}}
+                    onChange={this.handlePayloadChanged}
+                />
+                <RaisedButton backgroundColor="#BDBDBD" labelColor="#FFFFFF" style={style.button}
+                    label="GET" onTouchTap={this.handleButtonClicked.bind(this, "GET")}
+                />
+                <RaisedButton backgroundColor="#BDBDBD" labelColor="#FFFFFF" style={style.button}
+                    label="PUT" onTouchTap={this.handleButtonClicked.bind(this, "PUT")}
+                />
+                <RaisedButton backgroundColor="#BDBDBD" labelColor="#FFFFFF" style={style.button}
+                    label="POST" onTouchTap={this.handleButtonClicked.bind(this, "POST")}
+                />
+                <RaisedButton backgroundColor="#BDBDBD" labelColor="#FFFFFF" style={style.button}
+                    label="DELETE" onTouchTap={this.handleButtonClicked.bind(this, "DELETE")}
+                />
+                <RaisedButton backgroundColor="#BDBDBD" labelColor="#FFFFFF" style={style.button}
+                    label={this.state.observeState} onTouchTap={this.handleButtonClicked.bind(this, this.state.observeState)}
+                />
+                <TextField
+                    floatingLabelText="response"
+                    fullWidth={true}
+                    multiLine={true}
+                    rows={5}
+                    rowsMax={5}
+                    floatingLabelFixed={true}
+                    style={{ textAlign: 'left'}}
+                    value={this.state.response}
+                />
+            </div>
+        );
+    }
+};
+
+// List of found resources.
+class ResourceList extends React.Component {
+
+    state = {
+        inputRT: '',
+        inputDI: '',
+    };
+
+    handleRTTextChanged = (event, newValue) => {
+        this.setState({ inputRT: newValue });
+    }
+
+    handleDITextChanged = (event, newValue) => {
+        this.setState({ inputDI: newValue });
+    }
+
+    constructor(props, context) {
+        super(props, context);
+        this.state = {
+            data: props.data,
+            inputRT: '',
+            inputDI: '',
+        };
+    };
+
+    makeListItem = (uri) => {
+        var handler =
+            <MessageHandler uri={uri} />;
+
+        return handler;
+    }
+
+    render() {
+        return (
+            <MuiThemeProvider>
+                <div>
+                    <List style={style.basic}>
+                        <Subheader style={{ fontWeight: 'bold' }} >Resources</Subheader>
+                        {this.state.data.map( (row, index) => {
+                            if (row.size === 0) {
+                                return (<p key='notfound' style={{ fontWeight: 'bold' }}> No Resource found </p>);
+                            }
+                            return ( <ListItem primaryText={row.uri}
+                                secondaryText={ <p> rt: {row.rts} <br /> if: {row.ifs} </p> }
+                                secondaryTextLines={2}
+                                nestedItems={[<ListItem key={index + '_item'} disabled={true}>
+                                {this.makeListItem(row.uri)} </ListItem>]}
+                                nestedListStyle={{ textAlign:'right' }} primaryTogglesNestedList={true}
+                                innerDivStyle={{ fontWeight: 'bold' }}/>
+                            );})
+                        }
+                    </List>
+                </div>
+            </MuiThemeProvider>
+        );
+    }
+}
+
+export default ResourceList;
diff --git a/cloud/dashboard/src/components/ResourceListToolbar.js b/cloud/dashboard/src/components/ResourceListToolbar.js
new file mode 100644 (file)
index 0000000..50329e7
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import RaisedButton from 'material-ui/RaisedButton';
+import TextField from 'material-ui/TextField';
+import {Toolbar, ToolbarGroup, ToolbarSeparator} from 'material-ui/Toolbar';
+import ResourceList from './ResourceList';
+
+const Client = require('../Client');
+
+const ERROR = 'error';
+
+class ResourceListToolbar extends React.Component {
+
+    state = {
+        inputRT: '',
+        inputDI: '',
+    };
+
+    handleRTTextChanged = (event, newValue) => {
+        this.setState({ inputRT: newValue });
+    }
+
+    handleDITextChanged = (event, newValue) => {
+        this.setState({ inputDI: newValue });
+    }
+
+    onDiscover = (packet) => {
+          if(packet.getCode !== 69)
+          {
+            Client.event.emit(ERROR, "Resource Discover Failed " + packet.getCode);
+            return;
+          }
+
+          var resources = Client.getResourceList(packet.getPayloadObject);
+
+          document.getElementById('body').children[1].innerHTML = "";
+          ReactDOM.render(
+              <ResourceList data={resources} />,
+              document.getElementById('body').children[1]
+          );
+    };
+
+    handleButtonClicked = () => {
+        var queries = '';
+        if (this.state.inputRT.length !== 0) {
+            queries += 'rt='+ this.state.inputRT + ';';
+        }
+        if (this.state.inputDI.length !== 0) {
+            queries += 'di='+ this.state.inputDI;
+        }
+
+        Client.discoverResource(queries, this.onDiscover);
+    }
+
+    render() {
+        return (
+            <MuiThemeProvider>
+                <div>
+                    <Toolbar>
+                        <ToolbarGroup>
+                            <TextField
+                                floatingLabelText="Resource type"
+                                style={{ textAlign: 'left', margin: 7 }}
+                                onChange={this.handleRTTextChanged}
+                                value={this.state.inputRT}
+                            />
+                            <TextField
+                                floatingLabelText="Device id"
+                                style={{ textAlign: 'left', margin: 7 }}
+                                onChange={this.handleDITextChanged}
+                                value={this.state.inputDI}
+                            />
+                            <RaisedButton label="Discover" backgroundColor="#9CCC65" onTouchTap={this.handleButtonClicked}  labelStyle={{ color: '#FFFFFF', fontWeight: 'bold' }}/>
+                        </ToolbarGroup>
+                        <ToolbarGroup lastChild={true}>
+                            <ToolbarSeparator />
+                            <RaisedButton label="Refresh" backgroundColor="#8BC34A" onTouchTap={this.handleButtonClicked}  labelStyle={{ color: '#FFFFFF', fontWeight: 'bold' }}/>
+                        </ToolbarGroup>
+                    </Toolbar>
+                </div>
+            </MuiThemeProvider>
+        );
+    }
+}
+
+export default ResourceListToolbar;
diff --git a/cloud/dashboard/src/components/SigninPage.js b/cloud/dashboard/src/components/SigninPage.js
new file mode 100644 (file)
index 0000000..ee20f53
--- /dev/null
@@ -0,0 +1,187 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import RaisedButton from 'material-ui/RaisedButton';
+import TextField from 'material-ui/TextField';
+import Checkbox from 'material-ui/Checkbox';
+import {white} from 'material-ui/styles/colors';
+import Paper from 'material-ui/Paper';
+
+import SignupButton from './SignupButton';
+
+const Client = require('../Client');
+
+const style = {
+    basic: {
+        margin: 7
+    },
+
+    button: {
+        color: white
+    },
+
+    checkbox: {
+        marginLeft: '100px',
+        width: '200px'
+    },
+
+    error: {
+        textAlign: 'left',
+    },
+
+    font: {
+        fontWeight: 'bold'
+    },
+
+    paper: {
+        textAlign: 'center',
+        margin: 'auto',
+        position: 'absolute',
+        top: '20%',
+        left: '38%',
+    }
+};
+
+const errorText = 'This field is required';
+
+class SigninPage extends React.Component {
+
+    handleChange = (event) => {
+        var currentUidError = this.state.uidError;
+        var currentAccesstokenError = this.state.accesstokenError;
+
+        switch(event.target.id) {
+            case 'uid':
+                this.setState({ uid : event.target.value });
+                if (event.target.value.length !== 0) {
+                    currentUidError = '';
+                } else {
+                    currentUidError = errorText;
+                }
+                break;
+            case 'accesstoken':
+                this.setState({ accesstoken : event.target.value });
+                if (event.target.value.length !== 0) {
+                    currentAccesstokenError = '';
+                } else {
+                    currentAccesstokenError = errorText;
+                }
+                break;
+            default:
+                break;
+        }
+        this.setState({ uidError : currentUidError, accesstokenError : currentAccesstokenError })
+    };
+
+    handleSignin = () => {
+        if (this.state.uid.length === 0 || this.state.accesstoken.length === 0) {
+            return;
+        }
+
+        console.log("di: " + this.state.di);
+        console.log("uid: " + this.state.uid);
+        console.log("accesstoken: " + this.state.accesstoken);
+        Client.writeClientData([["uid", this.state.uid], ["accesstoken", this.state.accesstoken]]);
+        Client.signIn(this.state.di, this.state.uid, this.state.accesstoken);
+    };
+
+    onAutoSigninChecked = (event, isInputChecked) => {
+        this.setState({ autosignin: isInputChecked });
+
+        Client.writeClientData([['autosignin', isInputChecked]]);
+    };
+
+    constructor(props, context) {
+        super(props, context);
+        this.state = {
+                    autosignin: props.autosignin,
+                    di: props.di,
+                    uid: props.uid,
+                    accesstoken: props.accesstoken,
+                    uidError: '',
+                    accesstokenError: '',
+               };
+
+        if(this.state.autosignin === true) {
+         Client.signIn(this.state.di, this.state.uid, this.state.accesstoken);
+        }
+    };
+
+    render() {
+        const actions = (
+            <div style={{ padding: '30px 50px 30px 50px' }}>
+                <p style={{ fontWeight: 'bold', fontSize: 25 }}> Sign In </p>
+                <TextField
+                    floatingLabelText="Device Id"
+                    disabled={true}
+                    defaultValue={this.state.di}
+                    style={{ width: '400px' }}
+                /> <br />
+                <TextField
+                    id='uid'
+                    floatingLabelText="User UUID"
+                    defaultValue={this.state.uid}
+                    style={{ width: '400px' }}
+                    errorText={this.state.uidError}
+                    errorStyle={style.error}
+                    onChange={this.handleChange.bind(this)}
+                /> <br />
+                <TextField
+                    id='accesstoken'
+                    floatingLabelText="Access token"
+                    defaultValue={this.state.accesstoken}
+                    style={{ width: '400px' }}
+                    errorText={this.state.accesstokenError}
+                    errorStyle={style.error}
+                    onChange={this.handleChange.bind(this)}
+                /> <br />
+                <Checkbox
+                    label="Auto Sign-In"
+                    defaultChecked={this.state.autosignin}
+                    style={style.checkbox}
+                    onCheck={this.onAutoSigninChecked}
+                /> <br /> <br />
+                <div style={{ display: 'flex' }}>
+                    <SignupButton di={this.state.di} />
+                    <RaisedButton
+                        label="Sign-In"
+                        style={style.basic}
+                        onTouchTap={this.handleSignin.bind(this)}
+                    />
+                </div>
+            </div>
+        );
+
+        return (
+            <MuiThemeProvider>
+                <div style={{ height: '100%' }}>
+                    <Paper style={style.paper} zDepth={1}>
+                    {actions}
+                    </Paper>
+                </div>
+            </MuiThemeProvider>
+        );
+    }
+};
+
+export default SigninPage;
diff --git a/cloud/dashboard/src/components/SignupButton.js b/cloud/dashboard/src/components/SignupButton.js
new file mode 100644 (file)
index 0000000..7b1b497
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import RaisedButton from 'material-ui/RaisedButton';
+import SelectField from 'material-ui/SelectField';
+import MenuItem from 'material-ui/MenuItem';
+import TextField from 'material-ui/TextField';
+import FlatButton from 'material-ui/FlatButton';
+import Dialog from 'material-ui/Dialog';
+import {white} from 'material-ui/styles/colors';
+
+const Client = require('../Client');
+
+const style = {
+    basic: {
+        margin: 7
+    },
+
+    button: {
+        color: white
+    },
+
+    dialog: {
+        width: '500px'
+    },
+
+    dialogActions: {
+        textAlign: 'center'
+    },
+
+    selectfield: {
+        textAlign: 'left'
+    },
+
+    radiobutton: {
+        textAlign: 'center',
+        maxWidth: '250px',
+        marginBottom: '16px'
+    },
+
+    error: {
+        textAlign: 'left',
+    },
+
+    font: {
+        fontWeight: 'bold'
+    }
+};
+
+const errorText = 'This field is required';
+
+class SignupButton extends React.Component {
+
+    handleOpen = () => {
+        this.setState({ open: true });
+    };
+
+    handleClose = () => {
+        this.setState({ open: false });
+    };
+
+    handleAuthcodeButtonClicked = (value) => {
+        this.setState({ provider : value });
+    };
+
+    handleSelectfieldChange = (event, index, value) => {
+        this.setState({ provider : value });
+    };
+
+    handleTextChange = (event) => {
+        var currentCodeErr = this.state.codeError;
+
+        this.setState({ code : event.target.value });
+        if (event.target.value.length !== 0) {
+            currentCodeErr = '';
+        } else {
+            currentCodeErr = errorText;
+        }
+
+        this.setState({ codeError : currentCodeErr });
+    };
+
+    handleSubmit = () => {
+        if (this.state.code.length === 0) {
+            return;
+        }
+
+        Client.event.on('signup', this.signupCallback);
+        Client.signUp(this.state.di, this.state.provider, this.state.code);
+    };
+
+    signupCallback = (uid, accesstoken) => {
+        this.setState({
+            dialogMessage: 'User UUID: ' + uid + '\n Accesstoken: ' + accesstoken,
+            signup: true,
+        });
+    };
+
+    constructor(props, context) {
+        super(props, context);
+        this.state = {
+            di: props.di,
+            open: false,
+            provider: 'github',
+            code: '',
+            codeError: errorText,
+            dialogMessage: '',
+            signup: false
+        };
+    };
+
+    render() {
+        var actionBeforeSignup = [
+                <RaisedButton label="Github" backgroundColor="#E0E0E0" style={style.basic} onTouchTap={this.handleAuthcodeButtonClicked.bind(this, "github")}
+                    href="https://github.com/login?return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3Dea9c18f540323b0213d0%26redirect_uri%3Dhttp%253A%252F%252Fwww.example.com%252Foauth_callback%252F"
+                    target="_blank" />, <br />,
+                <SelectField
+                    floatingLabelText="OAuth provider"
+                    value={this.state.provider}
+                    onChange={this.handleSelectfieldChange}
+                    style={style.selectfield}
+                >
+                    <MenuItem value={'github'} primaryText="Github" />
+                    <MenuItem value={'samsung'} primaryText="Samsung" />
+                </SelectField>, <br />,
+                <TextField
+                    floatingLabelText="Device Id"
+                    disabled={true}
+                    defaultValue={this.state.di}
+                />, <br />,
+                <TextField
+                    floatingLabelText="Auth Code"
+                    errorText={this.state.codeError}
+                    errorStyle={style.error}
+                    onChange={this.handleTextChange}
+                />, <br />, <br />,
+                <RaisedButton
+                    label="Cancel"
+                    style={style.basic}
+                    onTouchTap={this.handleClose}
+                />,
+                <RaisedButton
+                    label="Submit"
+                    style={style.basic}
+                    primary={true}
+                    onTouchTap={this.handleSubmit}
+                />,
+            ];
+
+        var actionAfterSignup = [
+            <FlatButton
+                label="Close"
+                primary={true}
+                onTouchTap={this.handleClose}
+            />,
+        ];
+
+        return (
+            <MuiThemeProvider>
+                <div>
+                    <RaisedButton label="Sign-up" backgroundColor="#9E9E9E" style={style.basic} labelStyle={style.button} onTouchTap={this.handleOpen}/>
+                    <Dialog
+                        title="Sign-up"
+                        titleStyle={style.font}
+                        actions={this.state.signup ? actionAfterSignup : actionBeforeSignup}
+                        modal={true}
+                        open={this.state.open}
+                        contentStyle={style.dialog}
+                        actionsContainerStyle={style.dialogActions}>
+                    {this.state.dialogMessage}
+                    </Dialog>
+                </div>
+            </MuiThemeProvider>
+        );
+    }
+};
+
+export default SignupButton;
diff --git a/cloud/dashboard/src/index.css b/cloud/dashboard/src/index.css
new file mode 100644 (file)
index 0000000..b4cc725
--- /dev/null
@@ -0,0 +1,5 @@
+body {
+  margin: 0;
+  padding: 0;
+  font-family: sans-serif;
+}
diff --git a/cloud/dashboard/src/index.js b/cloud/dashboard/src/index.js
new file mode 100644 (file)
index 0000000..a3c8cb7
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+ * //******************************************************************
+ * //
+ * // Copyright 2017 Samsung Electronics All Rights Reserved.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ * //
+ * // Licensed under the Apache License, Version 2.0 (the "License");
+ * // you may not use this file except in compliance with the License.
+ * // You may obtain a copy of the License at
+ * //
+ * //      http://www.apache.org/licenses/LICENSE-2.0
+ * //
+ * // Unless required by applicable law or agreed to in writing, software
+ * // distributed under the License is distributed on an "AS IS" BASIS,
+ * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * // See the License for the specific language governing permissions and
+ * // limitations under the License.
+ * //
+ * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ConnectDialog from './components/ConnectDialog';
+import MainAppBar from './components/MainAppBar';
+import MainMenu from './components/MainMenu';
+import SigninPage from './components/SigninPage';
+import ErrorMessage from './components/ErrorMessage';
+
+import './index.css';
+
+import injectTapEventPlugin from 'react-tap-event-plugin';
+injectTapEventPlugin();
+
+const Client = require('./Client');
+
+const CONNECTED = 'connected';
+const DISCONNECTED = 'disconnected';
+const SIGNUP = 'signup';
+const SIGNIN = 'signin';
+const SIGNOUT = 'signout';
+const ERROR = 'error';
+
+// #1 error cases: render error message.
+Client.event.on(ERROR, function(message) {
+    ReactDOM.unmountComponentAtNode(document.getElementById('error'));
+    ReactDOM.render(
+        <ErrorMessage message={message}/>,
+        document.getElementById('error')
+    );
+});
+
+// #2 websocket connected: default page
+Client.event.on(CONNECTED, function() {
+    renderDefaultPage(false);
+
+    ReactDOM.render(
+        <SigninPage di={getDeviceId()} uid={getUid()} accesstoken={getAccesstoken()} autosignin={getAutosignin()} />,
+        document.getElementById('body')
+    );
+});
+
+// #3 websocket disconnected: connect page
+Client.event.on(DISCONNECTED, function() {
+    Client.removeClientData(['autoconnect', 'autosignin']);
+
+    unmountDefaultPage();
+
+    ReactDOM.render(
+        <ConnectDialog address={getAddress()} autoconnect={getAutoconnect()} />,
+        document.getElementById('body')
+    );
+});
+
+// #4 sign-in: request for resource discovery
+Client.event.on(SIGNIN, function() {
+    unmountDefaultPage();
+
+    renderDefaultPage(true);
+
+    ReactDOM.unmountComponentAtNode(document.getElementById('body'));
+});
+
+// #5 sign-out: default page
+Client.event.on(SIGNOUT, function() {
+    Client.removeClientData(['accesstoken']);
+
+    unmountDefaultPage();
+    renderDefaultPage(false);
+
+    ReactDOM.render(
+        <SigninPage di={getDeviceId()} uid={getUid()} accesstoken={getAccesstoken()} autosignin={getAutosignin()} />,
+        document.getElementById('body')
+    );
+});
+
+ReactDOM.render(
+    <ConnectDialog address={getAddress()} autoconnect={getAutoconnect()} />,
+    document.getElementById('body')
+);
+
+function renderDefaultPage(signinStatue) {
+    ReactDOM.render(
+        <MainAppBar signin={signinStatue}/>,
+        document.getElementById('appbar')
+    );
+    ReactDOM.render(
+        <MainMenu signin={signinStatue}/>,
+        document.getElementById('menu')
+    );
+};
+
+function unmountDefaultPage() {
+    ReactDOM.unmountComponentAtNode(document.getElementById('appbar'));
+    ReactDOM.unmountComponentAtNode(document.getElementById('menu'));
+};
+
+/**
+ * Private API.
+ */
+ function generateUUID() {
+     var d = new Date().getTime();
+     var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+         var r = (d + Math.random()*16)%16 | 0;
+         d = Math.floor(d/16);
+         return (c ==='x' ? r : (r&0x3|0x8)).toString(16);
+     });
+     return uuid;
+ };
+
+// get device id from local storage if exists, or generate id.
+function getDeviceId() {
+    var deviceId = Client.readClientData('deviceId');
+
+    if (deviceId === undefined || deviceId === null) {
+        deviceId = generateUUID();
+        Client.writeClientData([['deviceId', deviceId]]);
+    }
+
+    return deviceId;
+};
+
+function getAddress() {
+    var address = Client.readClientData('address');
+
+    return address;
+};
+
+function getUid() {
+    var uid = Client.readClientData('uid');
+
+    return uid;
+};
+
+function getAccesstoken() {
+    var accesstoken = Client.readClientData('accesstoken');
+
+    return accesstoken;
+};
+
+function getAutoconnect() {
+    var autoconnect = (Client.readClientData('autoconnect') === 'true');
+
+    return autoconnect;
+};
+
+function getAutosignin() {
+    var autosignin = (Client.readClientData('autosignin') === 'true');
+
+    return autosignin;
+};
index aef3355..9bf0332 100644 (file)
@@ -43,6 +43,7 @@ public class CoapDecoder extends ByteToMessageDecoder {
     private int          tokenLength         = 0;
     private int          optionPayloadLength = 0;
     private CoapMessage  partialMsg          = null;
+    private int          websocketLength     = -1;
 
     @Override
     protected void decode(ChannelHandlerContext ctx, ByteBuf in,
@@ -121,6 +122,12 @@ public class CoapDecoder extends ByteToMessageDecoder {
                             partialMsg.setToken(token);
                         }
 
+                        if (websocketLength != -1) {
+                            optionPayloadLength = websocketLength
+                                    - (bufferToRead + 1); // shimheader + code +
+                                                          // tokenLength
+                        }
+
                         if (optionPayloadLength > 0) {
                             int optionLen = parseOptions(partialMsg, in,
                                     optionPayloadLength);
@@ -166,7 +173,9 @@ public class CoapDecoder extends ByteToMessageDecoder {
         }
     }
 
-    public void decode(ByteBuf in, List<Object> out) throws Exception {
+    public void decode(ByteBuf in, List<Object> out, int length)
+            throws Exception {
+        websocketLength = length;
         decode(null, in, out);
     }
 
index d65c02c..38c1414 100644 (file)
@@ -30,6 +30,8 @@ import io.netty.handler.codec.MessageToByteEncoder;
 
 public class CoapEncoder extends MessageToByteEncoder<CoapMessage> {
 
+    private boolean isboolWebSocket = false;
+
     @Override
     protected void encode(ChannelHandlerContext ctx, CoapMessage msg,
             ByteBuf out) throws Exception {
@@ -42,6 +44,7 @@ public class CoapEncoder extends MessageToByteEncoder<CoapMessage> {
          * encode options
          */
         encodeOptions(optBuf, coapMessage);
+
         long length = optBuf.readableBytes();
 
         if (coapMessage.getPayloadSize() > 0) {
@@ -49,6 +52,10 @@ public class CoapEncoder extends MessageToByteEncoder<CoapMessage> {
             length += 1 + coapMessage.getPayloadSize();
         }
 
+        if (isboolWebSocket) {
+            length = 0;
+        }
+
         calcShimHeader(coapMessage, out, length);
 
         out.writeByte(coapMessage.getCode());
@@ -70,7 +77,9 @@ public class CoapEncoder extends MessageToByteEncoder<CoapMessage> {
         }
     }
 
-    public void encode(CoapMessage msg, ByteBuf out) throws Exception {
+    public void encode(CoapMessage msg, ByteBuf out, boolean isWebsocket)
+            throws Exception {
+        isboolWebSocket = isWebsocket;
         encode(null, msg, out);
     }
 
index a87b4e0..f10efb2 100644 (file)
  */
 package org.iotivity.cloud.base.protocols.coap.websocket;
 
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
-import io.netty.channel.ChannelDuplexHandler;
-import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelFutureListener;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelPromise;
-import io.netty.handler.codec.http.DefaultFullHttpResponse;
-import io.netty.handler.codec.http.HttpObjectAggregator;
-import io.netty.handler.codec.http.HttpServerCodec;
-import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
-import io.netty.handler.codec.http.websocketx.WebSocketFrame;
-
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -48,6 +35,19 @@ import org.iotivity.cloud.util.Cbor;
 import org.iotivity.cloud.util.JSONUtil;
 import org.iotivity.cloud.util.Log;
 
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
+
 public class WebSocketFrameHandler extends ChannelDuplexHandler {
 
     @Override
@@ -77,7 +77,8 @@ public class WebSocketFrameHandler extends ChannelDuplexHandler {
 
             List<Object> messages = new ArrayList<>();
             new CoapDecoder().decode(((BinaryWebSocketFrame) msg).content(),
-                    messages);
+                    messages,
+                    ((BinaryWebSocketFrame) msg).content().readableBytes());
 
             for (Object message : messages) {
                 if (message instanceof CoapMessage) {
@@ -85,12 +86,12 @@ public class WebSocketFrameHandler extends ChannelDuplexHandler {
 
                     // convert content format to cbor if content format is json.
                     if (coapMessage.getPayloadSize() != 0
-                            && coapMessage.getContentFormat().equals(
-                                    ContentFormat.APPLICATION_JSON)) {
+                            && coapMessage.getContentFormat()
+                                    .equals(ContentFormat.APPLICATION_JSON)) {
                         byte[] payload = coapMessage.getPayload();
                         coapMessage.setPayload(convertJsonToCbor(payload));
-                        coapMessage
-                                .setContentFormat(ContentFormat.APPLICATION_CBOR);
+                        coapMessage.setContentFormat(
+                                ContentFormat.APPLICATION_CBOR);
                     }
                     ctx.fireChannelRead(coapMessage);
                 }
@@ -138,7 +139,7 @@ public class WebSocketFrameHandler extends ChannelDuplexHandler {
             }
 
             ByteBuf encodedBytes = Unpooled.buffer();
-            new CoapEncoder().encode((CoapMessage) msg, encodedBytes);
+            new CoapEncoder().encode((CoapMessage) msg, encodedBytes, true);
             WebSocketFrame frame = new BinaryWebSocketFrame(encodedBytes);
             newMsg = frame;
         } else {