From 846dd7ea78b17de1ce80fb9f3517b85baa3bb95d Mon Sep 17 00:00:00 2001
From: 140b8f67-ec51-4b64-9606-bff2dffa0170 <flyandi@yahoo.com>
Date: Sat, 26 Mar 2016 00:39:11 -0700
Subject: [PATCH] More CMU backend works

---
 apps/app.helloworld/app.json                  |   5 +
 apps/appdrive.json                            |   1 +
 gulpfile.js                                   |  93 +++--
 package.json                                  |   2 +
 src/node-cmu/cmu-utils.js                     |  51 +++
 src/node-cmu/cmu.js                           | 356 ++++++++++++++++++
 src/{runtime => node-cmu}/package.json        |   0
 src/node-cmu/test/index.html                  |  25 ++
 src/node/sys/Dockerfile                       |   1 +
 src/runtime/runtime.js                        |  10 -
 .../CustomApplications.js}                    | 341 ++++++++++++++++-
 11 files changed, 830 insertions(+), 55 deletions(-)
 create mode 100644 apps/app.helloworld/app.json
 create mode 100644 apps/appdrive.json
 create mode 100644 src/node-cmu/cmu-utils.js
 create mode 100644 src/node-cmu/cmu.js
 rename src/{runtime => node-cmu}/package.json (100%)
 create mode 100644 src/node-cmu/test/index.html
 delete mode 100644 src/runtime/runtime.js
 rename src/{proxy/CustomApplicationsProxy.js => system/CustomApplications.js} (61%)

diff --git a/apps/app.helloworld/app.json b/apps/app.helloworld/app.json
new file mode 100644
index 0000000..cf6eb95
--- /dev/null
+++ b/apps/app.helloworld/app.json
@@ -0,0 +1,5 @@
+{
+    "id": "app.helloworld",
+    "name": "Hello World",
+    "enabled": true
+}
\ No newline at end of file
diff --git a/apps/appdrive.json b/apps/appdrive.json
new file mode 100644
index 0000000..b1854ae
--- /dev/null
+++ b/apps/appdrive.json
@@ -0,0 +1 @@
+{"bla": "foo"}
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
index 2121cf4..7278fe0 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -50,7 +50,9 @@ var
     del = require('del'),
     fs = require('fs'),
     glob = require('glob'),
-    exec = require('child_process').exec;
+    exec = require('child_process').exec,
+    using = require('gulp-using'),
+    flatten = require('gulp-flatten');
 
 /**
  * @package
@@ -109,7 +111,7 @@ var buildJsonVersion = function(output, destination, name, attributes) {
 /**
  * (build) local apps
  *
- * These tasks handle the copy and build of the local apps
+ * These tasks handles the example apps
  */
 
 var appsPathInput = "./apps/",
@@ -149,59 +151,59 @@ gulp.task('build-apps', function(callback) {
 
 
 /**
- * tasks to build the runtime
+ * tasks to build the framework
  */
 
 var systemPathOutput = output + "system/",
-    runtimePathInput = input + "runtime/",
-    runtimePathOutput = systemPathOutput + "runtime/",
+    frameworkPathInput = input + "framework/",
+    frameworkPathOutput = systemPathOutput + "framework/",
     customPathInput = input + "custom/";
 
 // (cleanup)
-gulp.task('system-cleanup', function() {
+gulp.task('framework-cleanup', function() {
     return del(
         [systemPathOutput + '**/*']
     );
 });
 
 // (skeleton)
-gulp.task('system-runtime-skeleton', function() {
+gulp.task('framework-skeleton', function() {
 
-    return gulp.src(runtimePathInput + "skeleton/**/*", {
-            base: runtimePathInput + "skeleton"
+    return gulp.src(frameworkPathInput + "skeleton/**/*", {
+            base: frameworkPathInput + "skeleton"
         })
-        .pipe(gulp.dest(runtimePathOutput));
+        .pipe(gulp.dest(frameworkPathOutput));
 });
 
 
 // (less)
-gulp.task('system-runtime-less', function() {
+gulp.task('framework-less', function() {
 
-    return gulp.src(runtimePathInput + "less/*", {
-            base: runtimePathInput + "less"
+    return gulp.src(frameworkPathInput + "less/*", {
+            base: frameworkPathInput + "less"
         })
-        .pipe(concat('runtime.css'))
+        .pipe(concat('framework.css'))
         .pipe(less())
-        .pipe(gulp.dest(runtimePathOutput));
+        .pipe(gulp.dest(frameworkPathOutput));
 });
 
 
 // (Concatenate & Minify)
-gulp.task('system-runtime-js', function() {
+gulp.task('framework-js', function() {
 
-    return gulp.src(runtimePathInput + "js/*", {
-            base: runtimePathInput + "js"
+    return gulp.src(frameworkPathInput + "js/*", {
+            base: frameworkPathInput + "js"
         })
-        .pipe(concat('runtime.js'))
+        .pipe(concat('framework.js'))
         .pipe(uglify())
-        .pipe(concatutil.header(fs.readFileSync(runtimePathInput + "resources/header.txt", "utf8"), {
+        .pipe(concatutil.header(fs.readFileSync(frameworkPathInput + "resources/header.txt", "utf8"), {
             pkg: package
         }))
-        .pipe(gulp.dest(runtimePathOutput));
+        .pipe(gulp.dest(frameworkPathOutput));
 });
 
 // (copy custom app)
-gulp.task('system-custom', function() {
+gulp.task('framework-custom', function() {
     return gulp.src(customPathInput + "**/*", {
             base: customPathInput
         })
@@ -209,25 +211,25 @@ gulp.task('system-custom', function() {
 });
 
 /** @job system-version */
-gulp.task('system-version', function() {
+gulp.task('framework-version', function() {
 
-    buildJsonVersion("runtime.json", runtimePathOutput, "runtime-package", function(package) {
+    buildJsonVersion("framework.json", frameworkPathOutput, "framework-package", function(package) {
         return {
-            runtime: true,
+            framework: true,
         }
     });
 });
 
 
-// (build system)
-gulp.task('build-system', function(callback) {
+// (build framework)
+gulp.task('build-framework', function(callback) {
     runSequence(
-        'system-cleanup',
-        'system-runtime-skeleton',
-        'system-runtime-less',
-        'system-runtime-js',
-        'system-custom',
-        'system-version',
+        'framework-cleanup',
+        'framework-skeleton',
+        'framework-less',
+        'framework-js',
+        'framework-custom',
+        'framework-version',
         callback
     );
 });
@@ -380,6 +382,8 @@ gulp.task('build-sdcard', function(callback) {
 });
 
 
+
+
 /**
  * Build documentation
  */
@@ -597,11 +601,32 @@ gulp.task('clean', function() {
 gulp.task('default', function(callback) {
     runSequence(
         'clean',
-        'build-system',
+        'build-framework',
         'build-install',
         'build-uninstall',
         'build-sdcard',
         //'build-docs',
         callback
     );
+});
+
+
+/**
+ * Node
+ */
+
+var nodePathInput = input + 'node-cmu/',
+    nodePathSource = input + 'node/latest/',
+    nodePathOutput = output + 'node/';
+
+
+// (cleanup)
+gulp.task('node', function() {
+
+    // copy embeeded files
+    gulp.src(nodePathInput + "**/*.js", {
+        base: nodePathInput
+    }).pipe(flatten()).pipe(gulp.dest(nodePathOutput + 'cmu'));
+
+
 });
\ No newline at end of file
diff --git a/package.json b/package.json
index 4c6d849..8ecf449 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
     "gulp-concat": "^2.6.0",
     "gulp-concat-util": "^0.5.5",
     "gulp-file": "^0.2.0",
+    "gulp-flatten": "^0.2.0",
     "gulp-git": "^1.7.0",
     "gulp-jsdoc": "^0.1.5",
     "gulp-less": "^3.0.5",
@@ -23,6 +24,7 @@
     "gulp-replace": "^0.5.4",
     "gulp-tar": "^1.8.0",
     "gulp-uglify": "^1.5.2",
+    "gulp-using": "^0.1.0",
     "gulp-webserver": "^0.9.1",
     "run-sequence": "^1.1.5"
   },
diff --git a/src/node-cmu/cmu-utils.js b/src/node-cmu/cmu-utils.js
new file mode 100644
index 0000000..d366d45
--- /dev/null
+++ b/src/node-cmu/cmu-utils.js
@@ -0,0 +1,51 @@
+/**
+ * Custom Application SDK for Mazda Connect Infotainment System
+ *
+ * A micro framework that allows to write custom applications for the Mazda Connect Infotainment System
+ * that includes an easy to use abstraction layer to the JCI system.
+ *
+ * Written by Andreas Schwarz (http://github.com/flyandi/mazda-custom-applications-sdk)
+ * Copyright (c) 2016. All rights reserved.
+ *
+ * WARNING: The installation of this application requires modifications to your Mazda Connect system.
+ * If you don't feel comfortable performing these changes, please do not attempt to install this. You might
+ * be ending up with an unusuable system that requires reset by your Dealer. You were warned!
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see http://www.gnu.org/licenses/
+ *
+ */
+
+
+'use strict';
+
+
+/**
+ * A minimal implementation of the extend algorithm
+ * @param  Any object
+ * @return object           Returns an objec
+ */
+exports.extend = function(out) {
+
+  out = out || {};
+
+  for (var i = 1; i < arguments.length; i++) {
+    if (!arguments[i])
+      continue;
+
+    for (var key in arguments[i]) {
+      if (arguments[i].hasOwnProperty(key))
+        out[key] = arguments[i][key];
+    }
+  }
+
+  return out;
+};
\ No newline at end of file
diff --git a/src/node-cmu/cmu.js b/src/node-cmu/cmu.js
new file mode 100644
index 0000000..a87c9a1
--- /dev/null
+++ b/src/node-cmu/cmu.js
@@ -0,0 +1,356 @@
+/**
+ * Custom Application SDK for Mazda Connect Infotainment System
+ *
+ * A micro framework that allows to write custom applications for the Mazda Connect Infotainment System
+ * that includes an easy to use abstraction layer to the JCI system.
+ *
+ * Written by Andreas Schwarz (http://github.com/flyandi/mazda-custom-applications-sdk)
+ * Copyright (c) 2016. All rights reserved.
+ *
+ * WARNING: The installation of this application requires modifications to your Mazda Connect system.
+ * If you don't feel comfortable performing these changes, please do not attempt to install this. You might
+ * be ending up with an unusuable system that requires reset by your Dealer. You were warned!
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see http://www.gnu.org/licenses/
+ *
+ */
+
+
+'use strict';
+
+const VERSION = "0.0.1";
+
+/**
+ * Definitions
+ * @const
+ */
+
+const _webSocketServer = require("ws").Server;
+const _extend = require("./cmu-utils").extend;
+const fs = require("fs");
+
+/**
+ * Minimal Command
+ * @constants
+ */
+
+const REQUEST_VERSION = "version";
+const REQUEST_PING = "ping";
+const REQUEST_APPLICATIONS = "applications";
+
+const RESULT_OK = 200;
+const RESULT_PONG = 201;
+const RESULT_NOTFOUND = 404;
+const RESULT_ERROR = 500;
+
+
+const MOUNTROOT_PATH = "/tmp/mnt/";
+const APPLICATIONS_PATH = "/apps/";
+
+const APPLICATION_JSON = "app.json";
+const APPLICATION_CSS = "app.css";
+const APPLICATION_JS = "app.js";
+const APPLICATION_WORKER = "worker.js";
+
+const APPDRIVE_JSON = "appdrive.json";
+
+/**
+ * This is the CMU that is compiled into the node binary and runs the actual link between the
+ * custom applications and the CMU.
+ * @node-cmu
+ */
+
+function cmu() {
+
+    this.__construct();
+
+}
+
+/**
+ * Prototypes
+ * @type The prototype object
+ */
+cmu.prototype = {
+
+    /**
+     * The version number
+     * @var string
+     */
+    version: VERSION,
+
+    /**
+     * The network object
+     * @var array
+     */
+    network: {
+
+        /**
+         * The network port
+         */
+        port: 9700,
+    },
+
+
+
+    /**
+     * Constructs the CMU object
+     * @constructor
+     */
+    __construct: function()
+    {
+        this.applications = {};
+
+        this.__socket = new _webSocketServer({
+            port: this.network.port
+        });
+
+        this.__socket.on('connection', function(client) {
+
+            this.attachClient(client);
+
+        }.bind(this));
+
+    },
+
+
+    /**
+     * Preps a client
+     * @param  object   client      The client instance
+     * @return void
+     */
+    attachClient: function(client) {
+
+        client.on('message', function(message) {
+
+            this.handleClientData(client, message);
+
+        }.bind(this));
+
+        client.on('close', function() {
+
+            // do nothing
+        });
+
+        client.on('error', function(e) {
+
+            // do nothing
+        });
+    },
+
+    /**
+     * Handles a client message
+     * @param  object   client      The client instance
+     * @param  string   message     The message payload
+     * @return void
+     */
+    handleClientData: function(client, message, flags) {
+
+        try {
+            var payload = JSON.parse(message);
+
+            // process minimal command interfae
+            if(payload.request) {
+
+                switch(payload.request) {
+
+                    /**
+                     * Returns the current CMU backend version
+                     * @type REQUEST_VERSION
+                     */
+                    case REQUEST_VERSION:
+
+                        return this.sendFromPayload(client, payload, {
+                            version: this.getVersion()
+                        });
+                        break;
+
+                    /**
+                     * Heartbeat
+                     * @type REQUEST_PING
+                     */
+                    case REQUEST_PING:
+
+                        return this.sendFromPayload(client, payload, {
+                            outboundStamp: (new Date()).getTime()
+                        }, RESULT_PONG);
+                        break;
+
+                    /**
+                     * Returns the current registered applications
+                     * @type REQUEST_APPLICATIONS
+                     */
+                    case REQUEST_APPLICATIONS:
+
+                        // find applications
+                        this.findApplications(function(applications, appdrive) {
+
+                            this.sendFromPayload(client, payload, {
+                                applications: applications,
+                                appdrive: appdrive,
+                            });
+
+                        }.bind(this));
+
+                        break;
+
+                    /**
+                     * Default - Pass to application handler
+                     * @type default
+                     */
+                    default:
+
+                        break;
+
+                }
+
+            }
+
+        } catch(e) {
+
+        }
+    },
+
+    /**
+     * Sends a payload back to the client
+     * @param  object   client      The client
+     * @param  object   payload     The payload in object format
+     * @return boolean              Returns the status of the operation
+     */
+    sendFromPayload: function(client, payload, data, resultCode) {
+
+        var final = JSON.stringify(_extend({}, payload, data, {
+            result: resultCode || RESULT_OK
+        }));
+
+        client.send(final);
+    },
+
+    /**
+     * Returns the version
+     * @getter
+     * @return  string              The version number
+     */
+    getVersion: function() {
+        return this.version;
+    },
+
+
+    /**
+     * Finds all applications in known locations
+     * @return {[type]} [description]
+     */
+    findApplications: function(callback) {
+
+        this.applications = {};
+
+        this.appdrive = false;
+
+        var result = [],
+            mountPoints = ['sd_nav', 'sda', 'sdb', 'sdc', 'sdd', 'sde'];
+
+        mountPoints.forEach(function(mountPoint) {
+
+            var path = [MOUNTROOT_PATH, mountPoint, APPLICATIONS_PATH].join(""),
+
+                appdriveFilename = [path, APPDRIVE_JSON].join("");
+
+            if(this._isFile(appdriveFilename)) {
+
+                this.appdrive = require(appdriveFilename);
+            }
+
+            if(this._isDir(path)) {
+
+                var files = fs.readdirSync(path);
+
+                if(files.length) files.forEach(function(appId) {
+
+                    /**
+                     * currently we only allow the first application to be registered
+                     * otherwise you would need to restart the CMU
+                     */
+
+                    if(!this.applications[appId]) {
+
+                        var applicationPath = [path, appId, "/"].join("");
+
+                        if(this._isDir(applicationPath)) {
+
+                            var profile = {
+                                    appId: appId,
+                                    appPath: applicationPath,
+                                    files: {},
+                                },
+                                parts = [APPLICATION_JS, APPLICATION_JSON, APPLICATION_CSS, APPLICATION_WORKER],
+                                found = 0;
+
+                            parts.forEach(function(filename) {
+
+                                var fullFilename = [applicationPath, filename].join("");
+
+                                if(this._isFile(fullFilename)) {
+
+                                    profile.files[filename] = fullFilename;
+                                    found++;
+                                }
+                            }.bind(this));
+
+
+                            if(found >= 1) {
+                                this.applications[appId] = profile;
+                            }
+                        }
+                    }
+
+                }.bind(this));
+            }
+
+        }.bind(this));
+
+        if(callback) callback(this.applications, this.appdrive);
+
+    },
+
+    /**
+     * __fileExists
+     */
+
+    _isFile: function(path) {
+        try {
+            return fs.lstatSync(path).isFile();
+        } catch(e) {}
+
+        return false;
+    },
+
+    /**
+     * Checks if the directory exists
+     * @param  {[type]}  path [description]
+     * @return {Boolean}      [description]
+     */
+    _isDir: function(path) {
+        try {
+            return fs.lstatSync(path).isDirectory();
+        } catch(e) {}
+
+        return false;
+    },
+};
+
+/**
+ *
+ */
+
+exports = new cmu();
+
+/**
+/** eof */
diff --git a/src/runtime/package.json b/src/node-cmu/package.json
similarity index 100%
rename from src/runtime/package.json
rename to src/node-cmu/package.json
diff --git a/src/node-cmu/test/index.html b/src/node-cmu/test/index.html
new file mode 100644
index 0000000..1668eea
--- /dev/null
+++ b/src/node-cmu/test/index.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src="../../system/CustomApplications.js"></script>
+
+    <script>
+
+        CustomApplications.initialize();
+
+
+        Logger.subscribe(function(level, name, msg, color) {
+            console.log(
+                '%c[' + level + '] %c[' + name + '] ' + msg,
+                'color:' + color,
+                'color:black'
+            );
+        });
+
+    </script>
+
+  </head>
+  <body>
+    <strong>CMU Protocol Test</strong>
+  </body>
+</html>
\ No newline at end of file
diff --git a/src/node/sys/Dockerfile b/src/node/sys/Dockerfile
index d670b30..11eb8bd 100644
--- a/src/node/sys/Dockerfile
+++ b/src/node/sys/Dockerfile
@@ -67,6 +67,7 @@ RUN cd ${NODE_SOURCE_PACKAGE}
 # Change work dir
 WORKDIR /armv7l/${NODE_SOURCE_PACKAGE}
 
+
 # Configure
 RUN ./configure --without-snapshot --dest-cpu=arm --dest-os=linux --fully-static --without-ssl --tag=CASDK-NODE
 
diff --git a/src/runtime/runtime.js b/src/runtime/runtime.js
deleted file mode 100644
index 335653e..0000000
--- a/src/runtime/runtime.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable require-buffer */
-'use strict';
-
-//const binding = process.binding('cmu');
-const cmu = exports;
-
-
-cmu.version = function() {
-    return "It a worked!";
-}
\ No newline at end of file
diff --git a/src/proxy/CustomApplicationsProxy.js b/src/system/CustomApplications.js
similarity index 61%
rename from src/proxy/CustomApplicationsProxy.js
rename to src/system/CustomApplications.js
index 76bb097..0e0e82a 100644
--- a/src/proxy/CustomApplicationsProxy.js
+++ b/src/system/CustomApplications.js
@@ -24,28 +24,127 @@
  *
  */
 
-
 /**
- * (GlobalError)
+ * This is the main system file that manages everything between the CMU backend server and the frontend.
  */
 
-window.onerror = function() {
-	console.error(arguments);
-}
+/**
+ * (Logger)
+ */
+
+
+window.Logger = {
+
+	levels: {
+		debug: 'DEBUG',
+		info: 'INFO',
+		error: 'ERROR',
+	},
+
+	/**
+	 * Subscriptions
+	 * @array
+	 */
+	subscriptions: [],
+
+	/**
+	 * (debug) debug message
+	 */
+
+	debug: function() {
+		this.__message(this.levels.debug, "#006600", Array.apply(null, arguments));
+	},
+
+	/**
+	 * (error) error message
+	 */
+
+	error: function() {
+		this.__message(this.levels.error, "#FF0000", Array.apply(null, arguments));
+	},
+
+	/**
+	 * (info) info message
+	 */
+
+	info: function() {
+		this.__message(this.levels.info, "#0000FF", Array.apply(null, arguments));
+	},
+
+	/**
+	 * Subscribe
+	 * @return {[type]} [description]
+	 */
+	subscribe: function(callback) {
+
+		if(typeof(callback) == "function") {
+			this.subscriptions.push(callback);
+		}
+	},
+
+	/**
+	 * [__message description]
+	 * @param  {[type]} level  [description]
+	 * @param  {[type]} color  [description]
+	 * @param  {[type]} values [description]
+	 * @return {[type]}        [description]
+	 */
+	__message: function(level, color, values) {
+
+		var msg = [];
+
+		if(values.length > 1) {
+			values.forEach(function(value, index) {
+
+				if(index > 0) {
+
+					if(typeof(value) == "object") {
+
+						var keys = value, o = false;
+
+						if(Object.prototype.toString.call(value) == "[object Object]") {
+							var keys = Object.keys(value),
+								o = true;
+						}
+
+						keys.forEach(function(v, index) {
+							msg.push(o ? '[' + v + '=' + value[v]+ ']' : '[' + v + ']');
+						});
+
+					} else {
+						msg.push(value);
+					}
+				}
+			});
+		}
+
+		msg = msg.join(" ");
+
+		this.subscriptions.forEach(function(subscription) {
+
+			try {
+				subscription(level, values[0], msg, color);
+			} catch(e) {
+
+			}
+		});
+	}
+};
 
 
 /**
- * (CustomApplicationsProxy)
+ * (CustomApplications)
  *
  * Registers itself between the JCI system and CustomApplication runtime.
  */
 
-window.CustomApplicationsProxy = {
+window.CustomApplications = {
+
+	ID: 'system',
 
 	/**
 	 * (locals)
 	 */
-
 	debug: false,
 	bootstrapped: false,
 
@@ -59,6 +158,226 @@ window.CustomApplicationsProxy = {
 	targetAppName: 'custom',
 	targetAppContext: 'Surface',
 
+	/**
+	 * Configuration
+	 */
+	configuration: {
+
+		networkHost: '127.0.0.1',
+		networkPort: 9700,
+
+	},
+
+	/**
+	 * Commands
+	 */
+	commands: {
+
+		REQUEST_PING: 'ping',
+		REQUEST_APPLICATIONS: 'applications',
+	},
+
+	/**
+	 * Results
+	 */
+	results: {
+
+		RESULT_OK: 200,
+		RESULT_PONG: 201,
+		RESULT_NOTFOUND: 404,
+		RESULT_ERROR: 500,
+	},
+
+	/**
+	 * Initializes the proxy
+	 * @return void
+	 */
+	initialize: function() {
+
+		this.requests = {};
+
+		this.obtainConnection();
+
+	},
+
+
+	/**
+	 * Establishes a connection between the front and backend
+	 * @return void
+	 */
+	obtainConnection: function() {
+
+		try {
+
+			this.client = new WebSocket('ws://' + this.configuration.networkHost + ':' + this.configuration.networkPort);
+
+			/**
+			 * Ping
+			 */
+			this.client.ping = function() {
+
+				this.request(this.commands.REQUEST_PING, {
+					inboundStamp: (new Date()).getTime()
+				}, function(error, result) {
+
+					Logger.info(this.ID, "ping", {
+						lost: error,
+						time: !error ? result.outboundStamp - result.inboundStamp : 0,
+					});
+
+				}.bind(this));
+
+			}.bind(this);
+
+			/**
+			 * onOpen
+			 * @event
+			 */
+			this.client.onopen = function() {
+
+				Logger.info(this.ID, "connection open");
+
+				this.client.ping();
+
+				this.requestApplications();
+
+			}.bind(this);
+
+			/**
+			 * onMessage
+			 * @event
+			 */
+			this.client.onmessage = function(message) {
+
+				this.handleReturnRequest(message);
+
+			}.bind(this);
+
+			/**
+			 * onError
+			 * @event
+			 */
+			this.client.onerror = function(error) {
+
+				Logger.error(CustomApplications.ID, 'ClientError', error);
+
+			}.bind(this);
+
+			/**
+			 * onClose
+			 * @event
+			 */
+			this.client.onclose = function(event) {
+
+				this.client = null;
+
+				if(event.code == 3110) {
+
+				} else {
+
+					setTimeout(function() {
+
+						CustomApplications.obtainConnection();
+
+					}, 5000); // retry later
+
+				}
+
+			}.bind(this);
+
+		} catch(e) {
+
+			this.client = null;
+		}
+	},
+
+	/**
+	 * [request description]
+	 * @return {[type]} [description]
+	 */
+	request: function(request, payload, callback) {
+
+		// check connection state
+		if(!this.client || this.client.readyState != 1) return callback(true, {});
+
+		// prepare id
+		var id = false;
+		while(!id || this.requests[id]) {
+			id = (new Date()).getTime();
+		}
+
+		// register request
+		this.requests[id] = callback;
+
+		// sanity check
+		payload = payload || {};
+
+		// add request id
+		payload.requestId = id;
+
+		payload.request = request;
+
+		// execute
+		return this.client.send(JSON.stringify(payload));
+	},
+
+
+	/**
+	 * Processes a request
+	 * @param  {[type]} data [description]
+	 * @return {[type]}      [description]
+	 */
+	handleReturnRequest: function(message) {
+
+		try {
+			// parse message
+			var payload = JSON.parse(message.data);
+
+			// check against active requests
+			if(payload.requestId && this.requests[payload.requestId]) {
+
+				var callback = this.requests[payload.requestId];
+
+				if(typeof(callback) == "function") {
+
+					callback(payload.result == this.results.RESULT_ERROR, payload);
+				}
+
+				delete this.requests[payload.requestId];
+
+				return; // all done
+			}
+
+		} catch(e) {
+
+			Logger.error(CustomApplications.ID, 'handleReturnRequest', e);
+		}
+	},
+
+
+	/**
+	 * Trys to load the Custom Applications
+	 * @return void
+	 */
+	requestApplications: function() {
+
+		if(typeof(CustomApplicationsHandler) != "undefined") return false;
+
+		if(!this.request(this.commands.REQUEST_APPLICATIONS, false, function(error, result) {
+
+			if(error) {
+
+				return setTimeout(function() {
+
+					this.requestApplications();
+
+				}.bind(this), 100);
+			}
+
+			console.log(result);
+
+		}.bind(this)));
+	},
 
 	/**
 	 * (bootstrap)
@@ -66,7 +385,7 @@ window.CustomApplicationsProxy = {
 	 * Bootstraps the JCI system
 	 */
 
-	 bootstrap: function() {
+	bootstrap: function() {
 
 		// verify that core objects are available
 		if(typeof framework === 'object' && framework._currentAppUiaId === this.systemAppId && this.bootstrapped === false) {
@@ -184,7 +503,7 @@ window.CustomApplicationsProxy = {
 	routeMmuiMsg: function(jsObject) {
 
 		if(typeof(CustomApplicationsHandler) === 'object') {
-		
+
 			try {
 
 				var proxy = CustomApplicationsProxy;
@@ -349,7 +668,7 @@ window.CustomApplicationsProxy = {
 
 if(window.opera) {
 	window.opera.addEventListener('AfterEvent.load', function (e) {
-		CustomApplicationsProxy.bootstrap();
+		CustomApplications.initialize();
 	});
 }