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 @@ + + + + + + + + + + CMU Protocol Test + + \ 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(); }); }