diff --git a/tutorial-server/app.js b/tutorial-server/app.js new file mode 100644 index 0000000000..ed4cbc5e05 --- /dev/null +++ b/tutorial-server/app.js @@ -0,0 +1,127 @@ +/*global require,process,console*/ + +var CONFIG = { + port: 8081, + dictionary: "dictionary.json", + interval: 1000 +}; + +(function () { + "use strict"; + + var WebSocketServer = require('ws').Server, + fs = require('fs'), + wss = new WebSocketServer({ port: CONFIG.port }), + dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")), + spacecraft = { + "prop.fuel": 77, + "prop.thrusters": "OFF", + "comms.recd": 0, + "comms.sent": 0, + "pwr.temp": 245, + "pwr.c": 8.15, + "pwr.v": 30 + }, + histories = {}, + listeners = []; + + function updateSpacecraft() { + spacecraft["prop.fuel"] = Math.max( + 0, + spacecraft["prop.fuel"] - + (spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0) + ); + spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985 + + Math.random() * 0.25 + Math.sin(Date.now()); + spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985; + spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3); + } + + function generateTelemetry() { + var timestamp = Date.now(), sent = 0; + Object.keys(spacecraft).forEach(function (id) { + var state = { timestamp: timestamp, value: spacecraft[id] }; + histories[id] = histories[id] || []; // Initialize + histories[id].push(state); + spacecraft["comms.sent"] += JSON.stringify(state).length; + }); + listeners.forEach(function (listener) { + listener(); + }); + } + + function update() { + updateSpacecraft(); + generateTelemetry(); + } + + function handleConnection(ws) { + var subscriptions = {}, // Active subscriptions for this connection + handlers = { // Handlers for specific requests + dictionary: function () { + ws.send(JSON.stringify({ + type: "dictionary", + value: dictionary + })); + }, + subscribe: function (id) { + subscriptions[id] = true; + }, + unsubscribe: function (id) { + delete subscriptions[id]; + }, + history: function (id) { + ws.send(JSON.stringify({ + type: "history", + id: id, + value: histories[id] + })); + } + }; + + function notifySubscribers() { + Object.keys(subscriptions).forEach(function (id) { + var history = histories[id]; + if (history) { + ws.send(JSON.stringify({ + type: "data", + id: id, + value: history[history.length - 1] + })); + } + }); + } + + // Listen for requests + ws.on('message', function (message) { + var parts = message.split(' '), + handler = handlers[parts[0]]; + if (handler) { + handler.apply(handlers, parts.slice(1)); + } + }); + + // Stop sending telemetry updates for this connection when closed + ws.on('close', function () { + listeners = listeners.filter(function (listener) { + return listener !== notifySubscribers; + }); + }); + + // Notify subscribers when telemetry is updated + listeners.push(notifySubscribers); + } + + update(); + setInterval(update, CONFIG.interval); + + wss.on('connection', handleConnection); + + console.log("Example spacecraft running on port "); + console.log("Press Enter to toggle thruster state."); + process.stdin.on('data', function (data) { + spacecraft['prop.thrusters'] = + (spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF"; + console.log("Thrusters " + spacecraft["prop.thrusters"]); + }); +}()); diff --git a/tutorial-server/dictionary.json b/tutorial-server/dictionary.json new file mode 100644 index 0000000000..645e150125 --- /dev/null +++ b/tutorial-server/dictionary.json @@ -0,0 +1,66 @@ +{ + "name": "Example Spacecraft", + "identifier": "sc", + "subsystems": [ + { + "name": "Propulsion", + "identifier": "prop", + "measurements": [ + { + "name": "Fuel", + "identifier": "prop.fuel", + "units": "kilograms", + "type": "float" + }, + { + "name": "Thrusters", + "identifier": "prop.thrusters", + "units": "None", + "type": "string" + } + ] + }, + { + "name": "Communications", + "identifier": "comms", + "measurements": [ + { + "name": "Received", + "identifier": "comms.recd", + "units": "bytes", + "type": "integer" + }, + { + "name": "Sent", + "identifier": "comms.sent", + "units": "bytes", + "type": "integer" + } + ] + }, + { + "name": "Power", + "identifier": "pwr", + "measurements": [ + { + "name": "Generator Temperature", + "identifier": "pwr.temp", + "units": "\u0080C", + "type": "float" + }, + { + "name": "Generator Current", + "identifier": "pwr.c", + "units": "A", + "type": "float" + }, + { + "name": "Generator Voltage", + "identifier": "pwr.v", + "units": "V", + "type": "float" + } + ] + } + ] +} diff --git a/tutorials/bargraph/bundle.js b/tutorials/bargraph/bundle.js new file mode 100644 index 0000000000..5eb16564ce --- /dev/null +++ b/tutorials/bargraph/bundle.js @@ -0,0 +1,66 @@ +define([ + 'legacyRegistry', + './src/controllers/BarGraphController' +], function ( + legacyRegistry, + BarGraphController + ) { + legacyRegistry.register("tutorials/bargraph", { + "name": "Bar Graph", + "description": "Provides the Bar Graph view of telemetry elements.", + "extensions": { + "views": [ + { + "name": "Bar Graph", + "key": "example.bargraph", + "glyph": "H", + "templateUrl": "templates/bargraph.html", + "needs": [ "telemetry" ], + "delegation": true, + "editable": true, + "toolbar": { + "sections": [ + { + "items": [ + { + "name": "Low", + "property": "low", + "required": true, + "control": "textfield", + "size": 4 + }, + { + "name": "Middle", + "property": "middle", + "required": true, + "control": "textfield", + "size": 4 + }, + { + "name": "High", + "property": "high", + "required": true, + "control": "textfield", + "size": 4 + } + ] + } + ] + } + } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/bargraph.css" + } + ], + "controllers": [ + { + "key": "BarGraphController", + "implementation": BarGraphController, + "depends": [ "$scope", "telemetryHandler" ] + } + ] + } + }); +}); diff --git a/tutorials/bargraph/res/templates/bargraph.html b/tutorials/bargraph/res/templates/bargraph.html new file mode 100644 index 0000000000..a5bfbd3e5e --- /dev/null +++ b/tutorials/bargraph/res/templates/bargraph.html @@ -0,0 +1,35 @@ +
+
+
+ {{value}} +
+
+ +
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/tutorials/bargraph/src/controllers/BarGraphController.js b/tutorials/bargraph/src/controllers/BarGraphController.js new file mode 100644 index 0000000000..1d4a3e474c --- /dev/null +++ b/tutorials/bargraph/src/controllers/BarGraphController.js @@ -0,0 +1,75 @@ +define(function () { + function BarGraphController($scope, telemetryHandler) { + var handle; + + // Expose configuration constants directly in scope + function exposeConfiguration() { + $scope.low = $scope.configuration.low; + $scope.middle = $scope.configuration.middle; + $scope.high = $scope.configuration.high; + } + + // Populate a default value in the configuration + function setDefault(key, value) { + if ($scope.configuration[key] === undefined) { + $scope.configuration[key] = value; + } + } + + // Getter-setter for configuration properties (for view proxy) + function getterSetter(property) { + return function (value) { + value = parseFloat(value); + if (!isNaN(value)) { + $scope.configuration[property] = value; + exposeConfiguration(); + } + return $scope.configuration[property]; + }; + } + + // Add min/max defaults + setDefault('low', -1); + setDefault('middle', 0); + setDefault('high', 1); + exposeConfiguration($scope.configuration); + + // Expose view configuration options + if ($scope.selection) { + $scope.selection.proxy({ + low: getterSetter('low'), + middle: getterSetter('middle'), + high: getterSetter('high') + }); + } + + // Convert value to a percent between 0-100 + $scope.toPercent = function (value) { + var pct = 100 * (value - $scope.low) / + ($scope.high - $scope.low); + return Math.min(100, Math.max(0, pct)); + }; + + // Get bottom and top (as percentages) for current value + $scope.getBottom = function (telemetryObject) { + var value = handle.getRangeValue(telemetryObject); + return $scope.toPercent(Math.min($scope.middle, value)); + }; + $scope.getTop = function (telemetryObject) { + var value = handle.getRangeValue(telemetryObject); + return 100 - $scope.toPercent(Math.max($scope.middle, value)); + }; + + // Use the telemetryHandler to get telemetry objects here + handle = telemetryHandler.handle($scope.domainObject, function () { + $scope.telemetryObjects = handle.getTelemetryObjects(); + $scope.barWidth = + 100 / Math.max(($scope.telemetryObjects).length, 1); + }); + + // Release subscriptions when scope is destroyed + $scope.$on('$destroy', handle.unsubscribe); + } + + return BarGraphController; +}); diff --git a/tutorials/telemetry/bundle.js b/tutorials/telemetry/bundle.js new file mode 100644 index 0000000000..306e3737c9 --- /dev/null +++ b/tutorials/telemetry/bundle.js @@ -0,0 +1,90 @@ +define([ + 'legacyRegistry', + './src/ExampleTelemetryServerAdapter', + './src/ExampleTelemetryInitializer', + './src/ExampleTelemetryModelProvider' +], function ( + legacyRegistry, + ExampleTelemetryServerAdapter, + ExampleTelemetryInitializer, + ExampleTelemetryModelProvider +) { + legacyRegistry.register("tutorials/telemetry", { + "name": "Example Telemetry Adapter", + "extensions": { + "types": [ + { + "name": "Spacecraft", + "key": "example.spacecraft", + "glyph": "o" + }, + { + "name": "Subsystem", + "key": "example.subsystem", + "glyph": "o", + "model": { "composition": [] } + }, + { + "name": "Measurement", + "key": "example.measurement", + "glyph": "T", + "model": { "telemetry": {} }, + "telemetry": { + "source": "example.source", + "domains": [ + { + "name": "Time", + "key": "timestamp" + } + ] + } + } + ], + "roots": [ + { + "id": "example:sc", + "priority": "preferred", + "model": { + "type": "example.spacecraft", + "name": "My Spacecraft", + "composition": [] + } + } + ], + "services": [ + { + "key": "example.adapter", + "implementation": "ExampleTelemetryServerAdapter.js", + "depends": [ "$q", "EXAMPLE_WS_URL" ] + } + ], + "constants": [ + { + "key": "EXAMPLE_WS_URL", + "priority": "fallback", + "value": "ws://localhost:8081" + } + ], + "runs": [ + { + "implementation": "ExampleTelemetryInitializer.js", + "depends": [ "example.adapter", "objectService" ] + } + ], + "components": [ + { + "provides": "modelService", + "type": "provider", + "implementation": "ExampleTelemetryModelProvider.js", + "depends": [ "example.adapter", "$q" ] + }, + { + "provides": "telemetryService", + "type": "provider", + "implementation": "ExampleTelemetryProvider.js", + "depends": [ "example.adapter", "$q" ] + } + ] + } + }); +}); diff --git a/tutorials/telemetry/src/ExampleTelemetryInitializer.js b/tutorials/telemetry/src/ExampleTelemetryInitializer.js new file mode 100644 index 0000000000..4d53b41793 --- /dev/null +++ b/tutorials/telemetry/src/ExampleTelemetryInitializer.js @@ -0,0 +1,47 @@ +define( + function () { + "use strict"; + + var TAXONOMY_ID = "example:sc", + PREFIX = "example_tlm:"; + + function ExampleTelemetryInitializer(adapter, objectService) { + // Generate a domain object identifier for a dictionary element + function makeId(element) { + return PREFIX + element.identifier; + } + + // When the dictionary is available, add all subsystems + // to the composition of My Spacecraft + function initializeTaxonomy(dictionary) { + // Get the top-level container for dictionary objects + // from a group of domain objects. + function getTaxonomyObject(domainObjects) { + return domainObjects[TAXONOMY_ID]; + } + + // Populate + function populateModel(taxonomyObject) { + return taxonomyObject.useCapability( + "mutation", + function (model) { + model.name = + dictionary.name; + model.composition = + dictionary.subsystems.map(makeId); + } + ); + } + + // Look up My Spacecraft, and populate it accordingly. + objectService.getObjects([TAXONOMY_ID]) + .then(getTaxonomyObject) + .then(populateModel); + } + + adapter.dictionary().then(initializeTaxonomy); + } + + return ExampleTelemetryInitializer; + } +); diff --git a/tutorials/telemetry/src/ExampleTelemetryModelProvider.js b/tutorials/telemetry/src/ExampleTelemetryModelProvider.js new file mode 100644 index 0000000000..27e4b72deb --- /dev/null +++ b/tutorials/telemetry/src/ExampleTelemetryModelProvider.js @@ -0,0 +1,78 @@ +define( + function () { + "use strict"; + + var PREFIX = "example_tlm:", + FORMAT_MAPPINGS = { + float: "number", + integer: "number", + string: "string" + }; + + function ExampleTelemetryModelProvider(adapter, $q) { + var modelPromise, empty = $q.when({}); + + // Check if this model is in our dictionary (by prefix) + function isRelevant(id) { + return id.indexOf(PREFIX) === 0; + } + + // Build a domain object identifier by adding a prefix + function makeId(element) { + return PREFIX + element.identifier; + } + + // Create domain object models from this dictionary + function buildTaxonomy(dictionary) { + var models = {}; + + // Create & store a domain object model for a measurement + function addMeasurement(measurement) { + var format = FORMAT_MAPPINGS[measurement.type]; + models[makeId(measurement)] = { + type: "example.measurement", + name: measurement.name, + telemetry: { + key: measurement.identifier, + ranges: [{ + key: "value", + name: "Value", + units: measurement.units, + format: format + }] + } + }; + } + + // Create & store a domain object model for a subsystem + function addSubsystem(subsystem) { + var measurements = + (subsystem.measurements || []); + models[makeId(subsystem)] = { + type: "example.subsystem", + name: subsystem.name, + composition: measurements.map(makeId) + }; + measurements.forEach(addMeasurement); + } + + (dictionary.subsystems || []).forEach(addSubsystem); + + return models; + } + + // Begin generating models once the dictionary is available + modelPromise = adapter.dictionary().then(buildTaxonomy); + + return { + getModels: function (ids) { + // Return models for the dictionary only when they + // are relevant to the request. + return ids.some(isRelevant) ? modelPromise : empty; + } + }; + } + + return ExampleTelemetryModelProvider; + } +); diff --git a/tutorials/telemetry/src/ExampleTelemetryProvider.js b/tutorials/telemetry/src/ExampleTelemetryProvider.js new file mode 100644 index 0000000000..e7d71260ec --- /dev/null +++ b/tutorials/telemetry/src/ExampleTelemetryProvider.js @@ -0,0 +1,80 @@ +define( + ['./ExampleTelemetrySeries'], + function (ExampleTelemetrySeries) { + "use strict"; + + var SOURCE = "example.source"; + + function ExampleTelemetryProvider(adapter, $q) { + var subscribers = {}; + + // Used to filter out requests for telemetry + // from some other source + function matchesSource(request) { + return (request.source === SOURCE); + } + + // Listen for data, notify subscribers + adapter.listen(function (message) { + var packaged = {}; + packaged[SOURCE] = {}; + packaged[SOURCE][message.id] = + new ExampleTelemetrySeries([message.value]); + (subscribers[message.id] || []).forEach(function (cb) { + cb(packaged); + }); + }); + + return { + requestTelemetry: function (requests) { + var packaged = {}, + relevantReqs = requests.filter(matchesSource); + + // Package historical telemetry that has been received + function addToPackage(history) { + packaged[SOURCE][history.id] = + new ExampleTelemetrySeries(history.value); + } + + // Retrieve telemetry for a specific measurement + function handleRequest(request) { + var key = request.key; + return adapter.history(key).then(addToPackage); + } + + packaged[SOURCE] = {}; + return $q.all(relevantReqs.map(handleRequest)) + .then(function () { return packaged; }); + }, + subscribe: function (callback, requests) { + var keys = requests.filter(matchesSource) + .map(function (req) { return req.key; }); + + function notCallback(cb) { + return cb !== callback; + } + + function unsubscribe(key) { + subscribers[key] = + (subscribers[key] || []).filter(notCallback); + if (subscribers[key].length < 1) { + adapter.unsubscribe(key); + } + } + + keys.forEach(function (key) { + subscribers[key] = subscribers[key] || []; + adapter.subscribe(key); + subscribers[key].push(callback); + }); + + return function () { + keys.forEach(unsubscribe); + }; + } + }; + } + + return ExampleTelemetryProvider; + } +); diff --git a/tutorials/telemetry/src/ExampleTelemetrySeries.js b/tutorials/telemetry/src/ExampleTelemetrySeries.js new file mode 100644 index 0000000000..b2b1c840de --- /dev/null +++ b/tutorials/telemetry/src/ExampleTelemetrySeries.js @@ -0,0 +1,23 @@ +/*global define*/ + +define( + function () { + "use strict"; + + function ExampleTelemetrySeries(data) { + return { + getPointCount: function () { + return data.length; + }, + getDomainValue: function (index) { + return (data[index] || {}).timestamp; + }, + getRangeValue: function (index) { + return (data[index] || {}).value; + } + }; + } + + return ExampleTelemetrySeries; + } +); diff --git a/tutorials/telemetry/src/ExampleTelemetryServerAdapter.js b/tutorials/telemetry/src/ExampleTelemetryServerAdapter.js new file mode 100644 index 0000000000..83e8372b8d --- /dev/null +++ b/tutorials/telemetry/src/ExampleTelemetryServerAdapter.js @@ -0,0 +1,60 @@ +define( + [], + function () { + "use strict"; + + function ExampleTelemetryServerAdapter($q, wsUrl) { + var ws = new WebSocket(wsUrl), + histories = {}, + listeners = [], + dictionary = $q.defer(); + + // Handle an incoming message from the server + ws.onmessage = function (event) { + var message = JSON.parse(event.data); + + switch (message.type) { + case "dictionary": + dictionary.resolve(message.value); + break; + case "history": + histories[message.id].resolve(message); + delete histories[message.id]; + break; + case "data": + listeners.forEach(function (listener) { + listener(message); + }); + break; + } + }; + + // Request dictionary once connection is established + ws.onopen = function () { + ws.send("dictionary"); + }; + + return { + dictionary: function () { + return dictionary.promise; + }, + history: function (id) { + histories[id] = histories[id] || $q.defer(); + ws.send("history " + id); + return histories[id].promise; + }, + subscribe: function (id) { + ws.send("subscribe " + id); + }, + unsubscribe: function (id) { + ws.send("unsubscribe " + id); + }, + listen: function (callback) { + listeners.push(callback); + } + }; + } + + return ExampleTelemetryServerAdapter; + } +); diff --git a/tutorials/todo/bundle.js b/tutorials/todo/bundle.js index b56d2cdfdf..8313ff6d50 100644 --- a/tutorials/todo/bundle.js +++ b/tutorials/todo/bundle.js @@ -4,7 +4,7 @@ define([ ], function ( legacyRegistry, TodoController - ) { +) { legacyRegistry.register("tutorials/todo", { "name": "To-do Plugin", "description": "Allows creating and editing to-do lists.",