More CMU backend works

This commit is contained in:
140b8f67-ec51-4b64-9606-bff2dffa0170 2016-03-26 00:39:11 -07:00
parent fb1eb10596
commit 846dd7ea78
11 changed files with 830 additions and 55 deletions

View file

@ -0,0 +1,5 @@
{
"id": "app.helloworld",
"name": "Hello World",
"enabled": true
}

1
apps/appdrive.json Normal file
View file

@ -0,0 +1 @@
{"bla": "foo"}

View file

@ -50,7 +50,9 @@ var
del = require('del'), del = require('del'),
fs = require('fs'), fs = require('fs'),
glob = require('glob'), glob = require('glob'),
exec = require('child_process').exec; exec = require('child_process').exec,
using = require('gulp-using'),
flatten = require('gulp-flatten');
/** /**
* @package * @package
@ -109,7 +111,7 @@ var buildJsonVersion = function(output, destination, name, attributes) {
/** /**
* (build) local apps * (build) local apps
* *
* These tasks handle the copy and build of the local apps * These tasks handles the example apps
*/ */
var appsPathInput = "./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/", var systemPathOutput = output + "system/",
runtimePathInput = input + "runtime/", frameworkPathInput = input + "framework/",
runtimePathOutput = systemPathOutput + "runtime/", frameworkPathOutput = systemPathOutput + "framework/",
customPathInput = input + "custom/"; customPathInput = input + "custom/";
// (cleanup) // (cleanup)
gulp.task('system-cleanup', function() { gulp.task('framework-cleanup', function() {
return del( return del(
[systemPathOutput + '**/*'] [systemPathOutput + '**/*']
); );
}); });
// (skeleton) // (skeleton)
gulp.task('system-runtime-skeleton', function() { gulp.task('framework-skeleton', function() {
return gulp.src(runtimePathInput + "skeleton/**/*", { return gulp.src(frameworkPathInput + "skeleton/**/*", {
base: runtimePathInput + "skeleton" base: frameworkPathInput + "skeleton"
}) })
.pipe(gulp.dest(runtimePathOutput)); .pipe(gulp.dest(frameworkPathOutput));
}); });
// (less) // (less)
gulp.task('system-runtime-less', function() { gulp.task('framework-less', function() {
return gulp.src(runtimePathInput + "less/*", { return gulp.src(frameworkPathInput + "less/*", {
base: runtimePathInput + "less" base: frameworkPathInput + "less"
}) })
.pipe(concat('runtime.css')) .pipe(concat('framework.css'))
.pipe(less()) .pipe(less())
.pipe(gulp.dest(runtimePathOutput)); .pipe(gulp.dest(frameworkPathOutput));
}); });
// (Concatenate & Minify) // (Concatenate & Minify)
gulp.task('system-runtime-js', function() { gulp.task('framework-js', function() {
return gulp.src(runtimePathInput + "js/*", { return gulp.src(frameworkPathInput + "js/*", {
base: runtimePathInput + "js" base: frameworkPathInput + "js"
}) })
.pipe(concat('runtime.js')) .pipe(concat('framework.js'))
.pipe(uglify()) .pipe(uglify())
.pipe(concatutil.header(fs.readFileSync(runtimePathInput + "resources/header.txt", "utf8"), { .pipe(concatutil.header(fs.readFileSync(frameworkPathInput + "resources/header.txt", "utf8"), {
pkg: package pkg: package
})) }))
.pipe(gulp.dest(runtimePathOutput)); .pipe(gulp.dest(frameworkPathOutput));
}); });
// (copy custom app) // (copy custom app)
gulp.task('system-custom', function() { gulp.task('framework-custom', function() {
return gulp.src(customPathInput + "**/*", { return gulp.src(customPathInput + "**/*", {
base: customPathInput base: customPathInput
}) })
@ -209,25 +211,25 @@ gulp.task('system-custom', function() {
}); });
/** @job system-version */ /** @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 { return {
runtime: true, framework: true,
} }
}); });
}); });
// (build system) // (build framework)
gulp.task('build-system', function(callback) { gulp.task('build-framework', function(callback) {
runSequence( runSequence(
'system-cleanup', 'framework-cleanup',
'system-runtime-skeleton', 'framework-skeleton',
'system-runtime-less', 'framework-less',
'system-runtime-js', 'framework-js',
'system-custom', 'framework-custom',
'system-version', 'framework-version',
callback callback
); );
}); });
@ -380,6 +382,8 @@ gulp.task('build-sdcard', function(callback) {
}); });
/** /**
* Build documentation * Build documentation
*/ */
@ -597,7 +601,7 @@ gulp.task('clean', function() {
gulp.task('default', function(callback) { gulp.task('default', function(callback) {
runSequence( runSequence(
'clean', 'clean',
'build-system', 'build-framework',
'build-install', 'build-install',
'build-uninstall', 'build-uninstall',
'build-sdcard', 'build-sdcard',
@ -605,3 +609,24 @@ gulp.task('default', function(callback) {
callback 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'));
});

View file

@ -15,6 +15,7 @@
"gulp-concat": "^2.6.0", "gulp-concat": "^2.6.0",
"gulp-concat-util": "^0.5.5", "gulp-concat-util": "^0.5.5",
"gulp-file": "^0.2.0", "gulp-file": "^0.2.0",
"gulp-flatten": "^0.2.0",
"gulp-git": "^1.7.0", "gulp-git": "^1.7.0",
"gulp-jsdoc": "^0.1.5", "gulp-jsdoc": "^0.1.5",
"gulp-less": "^3.0.5", "gulp-less": "^3.0.5",
@ -23,6 +24,7 @@
"gulp-replace": "^0.5.4", "gulp-replace": "^0.5.4",
"gulp-tar": "^1.8.0", "gulp-tar": "^1.8.0",
"gulp-uglify": "^1.5.2", "gulp-uglify": "^1.5.2",
"gulp-using": "^0.1.0",
"gulp-webserver": "^0.9.1", "gulp-webserver": "^0.9.1",
"run-sequence": "^1.1.5" "run-sequence": "^1.1.5"
}, },

51
src/node-cmu/cmu-utils.js Normal file
View file

@ -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;
};

356
src/node-cmu/cmu.js Normal file
View file

@ -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 */

View file

@ -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>

View file

@ -67,6 +67,7 @@ RUN cd ${NODE_SOURCE_PACKAGE}
# Change work dir # Change work dir
WORKDIR /armv7l/${NODE_SOURCE_PACKAGE} WORKDIR /armv7l/${NODE_SOURCE_PACKAGE}
# Configure # Configure
RUN ./configure --without-snapshot --dest-cpu=arm --dest-os=linux --fully-static --without-ssl --tag=CASDK-NODE RUN ./configure --without-snapshot --dest-cpu=arm --dest-os=linux --fully-static --without-ssl --tag=CASDK-NODE

View file

@ -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!";
}

View file

@ -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. * Registers itself between the JCI system and CustomApplication runtime.
*/ */
window.CustomApplicationsProxy = { window.CustomApplications = {
ID: 'system',
/** /**
* (locals) * (locals)
*/ */
debug: false, debug: false,
bootstrapped: false, bootstrapped: false,
@ -59,6 +158,226 @@ window.CustomApplicationsProxy = {
targetAppName: 'custom', targetAppName: 'custom',
targetAppContext: 'Surface', 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) * (bootstrap)
@ -349,7 +668,7 @@ window.CustomApplicationsProxy = {
if(window.opera) { if(window.opera) {
window.opera.addEventListener('AfterEvent.load', function (e) { window.opera.addEventListener('AfterEvent.load', function (e) {
CustomApplicationsProxy.bootstrap(); CustomApplications.initialize();
}); });
} }