diff --git a/README.md b/README.md index 9f69cc338d1395e216aa6192ff6a93bd47ef3b67..665d878304aabc16489aed16cdb7345bc6f2b0ce 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ To run the program, we launch the main.js file with node, from the command line. cd to the rndmc folder and run: -``node main`` +``node rundmc`` It's handy to keep a terminal window open beside a browser when running the software - it's not perfect yet - I do this: @@ -149,11 +149,11 @@ Besides Inputs, Outputs, a Description and State object, anything else goes. I.E ```javascript // boilerplate rndmc header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button // a constructor, a fn, a javascript mess function uiNum() { @@ -265,27 +265,47 @@ View.assignProgram(program) # Development Notes -## For MW +## Immediately - - want that better-planner - - do axis pullout separately from stepper motor ? +- ui / button - - bug hunting - - dereferenced events / stepper axis vector - - multiple button calls ... - - stepper / planner bug - - is load / save really consistent ? what is the state answer ? +- would like to send board with new buck out to fab +- just do stepper23, bc if that's it, that's it ? -- title is still 'xperiment' -- stepper move.vector is a ref issue? derefed before running calcTrap() ? runs ? in planner ? +- tuning: + - mrobot having PI, PID terms + - having position-set input (also gets output?) + - having position-actual output + - graphing these things + - use UI slider (external) to set PID terms? + - should be able to use the same slider element directly inline -- example modules +- cleaning up reps +- looking for heirarchy demonstration +- imagining microcontrollers +- working on robot to inform desires: i.e. ui charts and graphs + +## For Madison Park + +- want that better-planner, better-stepper, better-inputs and graphic ui w/ 3js +- want network to not blow + - tokens w/ crc bit-level ? + - app does keepalive / green-when-on etc ? + - example of setup-for-consistent-feedback of variable, on a timer? tuning loops ... search ... +- do axis pullout separately from stepper motor ? accel command ? deep jog desire ... architecture still messy though + +- bug hunting + - multiline change input paste doesn't work ... big state problem + - is load / save really consistent ? what is the state answer ? + - expected behavior: pressing the button on the raw move module should result in a move for every button press + - observed behavior: to send another raw move (via the button on the raw move module), we must reset the motor drivers. -- add reset button to hardware - add router for reset, test -## Documentation -- GIFS +## Forever + +- open the door, no cuffs +- option for 'native' multithreading by opening workers on each event ? ## Questionable Moves - module deletion seems unclean @@ -294,8 +314,6 @@ View.assignProgram(program) ## WRT Representations -OK, should write this out properly at some point. - Module have Inputs Outputs @@ -320,6 +338,11 @@ To assemble a representation of these, we want to have a kind of 'netlist' that, - 's' for save program uses hack-asf DOM alert to ask for path ## Desires +- reload / edit individual modules ? +- modules spawn new inputs / outputs ? +- big UI ? + - arrows catch for jogging +- editing ? - heirarchy zoom - architectural clarity betwixt UI and Heap - some auto load / save currently.json file so that we can restart program w/o pain ... maybe just save on new user inputs ? @@ -333,6 +356,7 @@ To assemble a representation of these, we want to have a kind of 'netlist' that, - consistent dereferencing, type checking implementation? ## UI Desires +- scroll / grab events for touchpads etc ... find a mac user and workshop this for one afternoon ? - modules have visual ways to throw errors - i.e. flashing red, popping up... - off-screen divs get pointers-to so that we don't get lost - 'h' or something to zoom-to-extents diff --git a/client/client.js b/client/client.js index 72aba40ee234066846eec0f13cd8496f45a0c304..4d591fd4b2fc3818686921bdd1267a7f1814fad3 100644 --- a/client/client.js +++ b/client/client.js @@ -31,6 +31,8 @@ var lastPos = { x: 10, y: 30 } var wrapper = {} var nav = {} +var verbose = false + /* STARTUP --------------------------------------------------- @@ -64,7 +66,7 @@ window.onload = function() { sckt = this // say hello socketSend('console', 'hello server') - console.log('socket open') + console.log('SCKT: socket open') // ask for the current program socketSend('get current program', '') // main socket entry point @@ -75,10 +77,10 @@ window.onload = function() { this.onerror = (err) => { alert('link to server is broken') location.reload() - console.log('socket error', err) + console.log('SCKT: socket error', err) } this.onclose = (evt) => { - console.log('socket closed', evt) + console.log('SCKT: socket closed', evt) sckt = null } } @@ -110,33 +112,37 @@ function socketRecv(evt) { console.log('RECV CONSOLE:', data) break case 'put module menu': - console.log('RECV MODULE MENU') + if(verbose) console.log('RECV MODULE MENU') heapSendsModuleMenu(data) break case 'put program menu': - console.log('RECV PRG MENU') + if(verbose) console.log('RECV PRG MENU') heapSendsProgramMenu(data) break case 'put program': - console.log('RECV PROGRAM') + if(verbose) console.log('RECV PROGRAM') heapSendsNewProgram(data) break case 'put module': - console.log('RECV NEW MODULE') + if(verbose) console.log('RECV NEW MODULE') heapSendsNewModule(data) break case 'put module change': - console.log('RECV MODULE CHANGE') + if(verbose) console.log('RECV MODULE CHANGE') heapSendsModuleChange(data) break case 'put state change': - console.log('RECV STATE CHANGE') + if(verbose) console.log('RECV STATE CHANGE') heapSendsStateChange(data) break + case 'put ui change': + if(verbose) console.log('RECV UI CHANGE') + heapSendsUiChange(data) + break case 'restart': location.reload() default: - console.log('ERR recv with non recognized type', recv) + console.log('ERR: recv with non recognized type', recv) break } } @@ -157,7 +163,7 @@ HEAP -> SERVER --------------------------------------------------- // always a rep, tho var program = {} -// re-writes the program, adds a description, +// re-writes t program, adds a description, // and loads multiple representations of modules to the view function heapSendsNewProgram(prgm) { @@ -166,7 +172,7 @@ function heapSendsNewProgram(prgm) { program = prgm // 1st we want to git rm old files ... // when adding links, we'll have to add all and then draw links - console.log(program) + if(verbose) console.log('LOAD PROGRAM', program) for (mdlName in program.modules) { addRepToView(program.modules[mdlName]) } @@ -186,11 +192,11 @@ function heapSendsNewModule(mdl) { } // writes DOM elements to represent the module, appends to the wrapper -// and appends to the rep object a .ui object +// and appends to the rep object a .dom object // containing references to those DOM objects function heapSendsModuleChange(data) { - console.log(data) + if(verbose) console.log('HEAP SENDS MODULE CHANGE', data) // data should be rep of changed module var rep = program.modules[data.description.id] // we want a general case, but for now we know we're looking for @@ -206,7 +212,7 @@ function heapSendsModuleChange(data) { var stateItem = rep.state[key] if (stateItem != data.state[key]) { stateItem = data.state[key] - rep.ui.state[key].value = data.state[key] + rep.dom.state[key].value = data.state[key] } } // wreckless or wonderful? @@ -216,16 +222,21 @@ function heapSendsModuleChange(data) { // update state from server to UI function heapSendsStateChange(data) { - console.log('HEAP SENDS CHANGE STATE IN MODULE', data) + if(verbose) console.log('HEAP SENDS CHANGE STATE IN MODULE', data) var rep = program.modules[data.id] rep.state[data.key] = data.val - if (rep.state[data.key].type == 'multiline') { - rep.ui.state[data.key].value = data.val.value + if (typeof data.val == 'boolean') { + rep.dom.state[data.key].innerHTML = data.key + ':\t\t' + rep.state[data.key].toString() } else { - rep.ui.state[data.key].value = data.val + rep.dom.state[data.key].value = data.val } } +function heapSendsUiChange(data){ + if(true) console.log('HEAP SENDS CHANG UI IN MODULE', data) + // +} + /* UI -> HEAP --------------------------------------------------- @@ -298,14 +309,6 @@ UTILITIES --------------------------------------------------- */ -function isStateKey(key) { - if (key.indexOf('_') == 0 || key == 'emitters' || key == 'onChange' || key == 'emitChange') { - return false - } else { - return true - } -} - function redrawLinks() { // probably not a great way to do this, we're removing everything // svg -rm -r @@ -313,14 +316,14 @@ function redrawLinks() { svg.removeChild(svg.firstChild) } // draw origin - var og1 = newLine(-15,0,15,0,5, false) - var og2 = newLine(0,-15,0,15,5, false) + var og1 = newLine(-15, 0, 15, 0, 5, false) + var og2 = newLine(0, -15, 0, 15, 5, false) // find that link var lnkPt var nLnk = 0 for (mdlName in program.modules) { if (program.modules[mdlName].description.isLink) { - lnkPt = getLeftWall(program.modules[mdlName].ui.domElem) + lnkPt = getLeftWall(program.modules[mdlName].dom.domElem) } } // redraw thru all links, just look at reps @@ -328,11 +331,11 @@ function redrawLinks() { var mdlRep = program.modules[mdlName] for (key in mdlRep.outputs) { var output = mdlRep.outputs[key] - var outputUi = mdlRep.ui.outputs[key] + var outputUi = mdlRep.dom.outputs[key] for (input in output.calls) { var toId = output.calls[input].parentId var toKey = output.calls[input].key - var inputUi = program.modules[toId].ui.inputs[toKey] + var inputUi = program.modules[toId].dom.inputs[toKey] var outPos = getOutputArrow(outputUi) var inPos = getInputArrow(inputUi) if (inputUi.isHovering || outputUi.isHovering) { @@ -344,7 +347,7 @@ function redrawLinks() { } if (mdlRep.description.isHardware && !mdlRep.description.isLink) { nLnk++ - var hwPt = getRightWall(mdlRep.ui.domElem) + var hwPt = getRightWall(mdlRep.dom.domElem) lnkPt.y += 5 * nLnk var ln = newLine(hwPt.x, hwPt.y, lnkPt.x, lnkPt.y, 7, true) } @@ -463,7 +466,6 @@ function mouseUpDragListener(evt) { // get json menu item and render // and ask for module at /obj/key oncontextmenu = function(evt) { - console.log(evt.target) if (evt.target.className == 'modname') { var modRep = program.modules[evt.target.innerHTML] if (modRep) { @@ -503,6 +505,9 @@ document.onkeydown = function(evt) { case 'm': socketSend('get module menu', '') break + case 'd': + console.log(program) + break default: break } @@ -511,8 +516,8 @@ document.onkeydown = function(evt) { function writeModuleOptionMenu(modRep) { var menuDom = document.createElement('div') menuDom.id = 'perModuleMenu' - menuDom.style.left = 10 + modRep.ui.domElem.offsetLeft + modRep.ui.domElem.offsetWidth + 'px' - menuDom.style.top = modRep.ui.domElem.offsetTop + 'px' + menuDom.style.left = 10 + modrep.dom.domElem.offsetLeft + modrep.dom.domElem.offsetWidth + 'px' + menuDom.style.top = modRep.dom.domElem.offsetTop + 'px' // future: rm all inputs, rm all outputs, rename, open (heirarchy) var opts = ['delete', 'copy'] for (i in opts) { diff --git a/client/divtools.js b/client/divtools.js index 0a50ee25ecba6cc7bd73928dd4470e52f01cecb7..81c28db3c7adce8aa9aa90edb7cf62be092b8467 100644 --- a/client/divtools.js +++ b/client/divtools.js @@ -48,7 +48,7 @@ function addRepToView(rep) { var uiSetFlag // place in pos if info present - // the rep.ui object will store references to the module's related DOM elements + // the rep.dom object will store references to the module's related DOM elements if (rep.description.position != null) { uiSetFlag = false if (rep.description.position.left != null) { @@ -64,50 +64,61 @@ function addRepToView(rep) { rep.description.position.top = lastPos.y } - if (rep.ui == null) { - rep.ui = {} + if (rep.dom == null) { + rep.dom = {} } - rep.ui.domElem = domElem + rep.dom.domElem = domElem - // WRITE UI STATE ELEMENTS + // WRITE STATE ELEMENTS var stateElem = document.createElement('div') stateElem.className = 'state' - rep.ui.state = {} + rep.dom.state = {} for (st in rep.state) { var inputItem = writeStateRep(stateElem, rep, st) - rep.ui.state[st] = inputItem + rep.dom.state[st] = inputItem } // WRITE INPUTS var inElem = document.createElement('div') inElem.className = 'inputs' - rep.ui.inputs = {} + rep.dom.inputs = {} for (ip in rep.inputs) { var li = writeEventRep(rep, 'input', ip) inElem.appendChild(li) - rep.ui.inputs[ip] = li + rep.dom.inputs[ip] = li } // WRITE OUTPUTS var outElem = document.createElement('div') outElem.className = 'outputs' - rep.ui.outputs = {} + rep.dom.outputs = {} for (op in rep.outputs) { var li = writeEventRep(rep, 'output', op) outElem.appendChild(li) - rep.ui.outputs[op] = li + rep.dom.outputs[op] = li + } + + // HANDLE UNIQUE UIS + if (rep.ui != null) { + var uiElem = document.createElement('div') + uiElem.className = 'uidiv' + rep.dom.ui = {} + for (ui in rep.ui) { + writeUiElement(uiElem, rep, ui) + } } // APPEND TO CONTAINER domElem.appendChild(inElem) domElem.appendChild(outElem) domElem.appendChild(stateElem) + domElem.appendChild(uiElem) var clearElem = document.createElement('div') clearElem.className = 'clear' domElem.appendChild(clearElem) - wrapper.appendChild(rep.ui.domElem) + wrapper.appendChild(rep.dom.domElem) if (uiSetFlag) { putUi(rep) } @@ -124,21 +135,21 @@ function writeEventRep(rep, type, key) { name: key, evt: evt } - console.log('clicked', key) + if (verbose) console.log('EVENT HOOKUP CLK: ', key) evtConnectHandler(ipclk) }) li.addEventListener('mouseover', (evt) => { if (type == 'input') { - rep.ui.inputs[key].isHovering = true + rep.dom.inputs[key].isHovering = true } else if (type == 'output') { - rep.ui.outputs[key].isHovering = true + rep.dom.outputs[key].isHovering = true } redrawLinks() li.addEventListener('mouseout', (evt) => { if (type == 'input') { - rep.ui.inputs[key].isHovering = false + rep.dom.inputs[key].isHovering = false } else if (type == 'output') { - rep.ui.outputs[key].isHovering = false + rep.dom.outputs[key].isHovering = false } redrawLinks() }) @@ -148,70 +159,62 @@ function writeEventRep(rep, type, key) { function writeStateRep(container, rep, key) { var variable = rep.state[key] - switch (variable.type) { - case 'button': - console.log('BUTTON!') + switch (typeof variable) { + case 'string': var li = document.createElement('li') - li.appendChild(document.createTextNode(variable.label)) - li.addEventListener('click', function() { + li.appendChild(document.createTextNode(key)) + var input = document.createElement('input') + input.type = 'text' + input.size = 24 + input.value = variable + input.addEventListener('change', function() { + rep.state[key] = input.value putState(rep, key) }) + li.appendChild(input) container.appendChild(li) - return li + return input break - case 'multiline': - console.log('MULTILINE!') + case 'number': var li = document.createElement('li') - li.appendChild(document.createTextNode(variable.label)) - li.appendChild(document.createElement('br')) - var txtArea = document.createElement('textarea') - txtArea.rows = variable.rows - txtArea.cols = 25 - txtArea.value = variable.value - txtArea.addEventListener('change', function() { - rep.state[key].value = txtArea.value + li.appendChild(document.createTextNode(key)) + var input = document.createElement('input') + input.type = 'text' + input.size = 24 + input.value = variable.toString() + input.addEventListener('change', function(evt) { + rep.state[key] = parseFloat(input.value) putState(rep, key) }) - li.appendChild(txtArea) + li.appendChild(input) container.appendChild(li) - return txtArea + return input break - default: - if (typeof variable == 'string') { - var li = document.createElement('li') - li.appendChild(document.createTextNode(key)) - var input = document.createElement('input') - input.type = 'text' - input.size = 24 - input.value = variable - input.addEventListener('change', function() { - rep.state[key] = input.value - putState(rep, key) - }) - li.appendChild(input) - container.appendChild(li) - return input - } else if (typeof variable == 'number') { + case 'boolean': + var li = document.createElement('li') + li.innerHTML = key + ':\t\t' + variable.toString() + // TODO: tag align-right? + li.addEventListener('click', function() { + if (rep.state[key]) { + rep.state[key] = false + } else { + rep.state[key] = true + } + putState(rep, key) + }) + container.appendChild(li) + return li + // TODO : return what ? + break + case 'object': + // first, handle arrays + if (Array.isArray(variable)) { var li = document.createElement('li') li.appendChild(document.createTextNode(key)) var input = document.createElement('input') input.type = 'text' input.size = 24 - input.value = variable.toString() - input.addEventListener('change', function(evt) { - rep.state[key] = parseFloat(input.value) - putState(rep, key) - }) - li.appendChild(input) - container.appendChild(li) - return input - } else if (typeof variable == 'object') { - if (Array.isArray(variable)) { - var li = document.createElement('li') - li.appendChild(document.createTextNode(key)) - var input = document.createElement('input') - input.type = 'text' - input.size = 24 + if (typeof variable[0] == 'number') { input.value = variable.toString() input.addEventListener('change', function() { var arr = input.value.split(',') @@ -224,12 +227,86 @@ function writeStateRep(container, rep, key) { li.appendChild(input) container.appendChild(li) return input + } else if (typeof variable[0] == 'string') { + input.value = variable.toString() + input.addEventListener('change', function() { + var arr = input.value.split(',') + arr.forEach(function(element, index, array) { + array[index] = element + }) + rep.state[key] = arr + putState(rep, key) + }) + li.appendChild(input) + container.appendChild(li) + return input + } else if (typeof variable[0] == 'object') { + throw 'ERR not going to handle object arrays' } } else { - console.log("unui'd type:", typeof variable) + throw 'ERR not going to handle objects in state' } break + default: + console.log("ERR: state walked and no reciprocal code") + } +} + +function writeUiElement(container, rep, key) { + // pull the representation object from what we were sent + var ui = rep.ui[key] + console.log('write ui', ui) + + // load this thing, + ui.script = document.createElement('script') + ui.script.onerror = function(err){ + console.log('ERR from ui script', err) + } + ui.script.onload = function(msg){ + console.log('script loaded ?') + } + + container.appendChild(ui.script) + + ui.script.src = ui.clientPath + + + + // make an xhttp request for the code + /* + var request = new XMLHttpRequest() + request.open('GET', ui.clientPath) + request.responseType = 'text/javascript' + request.onLoad = function() { + console.log(request.response) + } + request.send() + + + fetch(ui.clientPath).then(function(response) { + response.text().then(function(text) { + console.log(text) + console.log(ui) + ui.script.appendChild(document.createTextNode(text)) + container.apppendChild(ui.script) + }) + }) + */ + + /* + // give it access to the socket, + ui.thing.sendToHeap = function(msg) { + var data = { + id: rep.description.id, + key: key, + msg: msg + } + socketSend('put ui change', data) } + + container.appendChild(ui.thing.domElement) + rep.dom.ui[key] = ui.thing.domElement + */ } @@ -302,11 +379,11 @@ function newLine(x1, y1, x2, y2, stroke, dashed) { var ln = {} ln.elem = document.createElementNS(svgns, 'line') ln.elem.style.stroke = '#1a1a1a' - if(dashed){ + if (dashed) { ln.elem.setAttribute('stroke-dasharray', '21, 7, 7, 7') } ln.elem.style.fill = 'none' - if(stroke){ + if (stroke) { ln.elem.style.strokeWidth = stroke + 'px' } else { ln.elem.style.strokeWidth = '6px' diff --git a/client/index.html b/client/index.html index 7ee3bec947104854f142bac4974b661b053b5088..813bbb18924f601da18a8212991e02c52f182fca 100644 --- a/client/index.html +++ b/client/index.html @@ -7,8 +7,6 @@ <body> <link href="style.css" rel="stylesheet"> - <!-- <script type="text/javascript" src="dat.gui.js"></script> - <script type="text/javascript" src="dummies.js"></script> --> <script type="text/javascript" src="divtools.js"></script> <script type="text/javascript" src="client.js"></script> <div id = "nav"> diff --git a/client/lib/smoothie.js b/client/lib/smoothie.js new file mode 100644 index 0000000000000000000000000000000000000000..60243a60f3ff7f79f84274376cd95538ccf7d6bb --- /dev/null +++ b/client/lib/smoothie.js @@ -0,0 +1,1100 @@ +// MIT License: +// +// Copyright (c) 2010-2013, Joe Walnes +// 2013-2018, Drew Noakes +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * Smoothie Charts - http://smoothiecharts.org/ + * (c) 2010-2013, Joe Walnes + * 2013-2018, Drew Noakes + * + * v1.0: Main charting library, by Joe Walnes + * v1.1: Auto scaling of axis, by Neil Dunn + * v1.2: fps (frames per second) option, by Mathias Petterson + * v1.3: Fix for divide by zero, by Paul Nikitochkin + * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds + * v1.5: Set default frames per second to 50... smoother. + * .start(), .stop() methods for conserving CPU, by Dmitry Vyal + * options.interpolation = 'bezier' or 'line', by Dmitry Vyal + * options.maxValue to fix scale, by Dmitry Vyal + * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla + * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin + * Smooth rescaling, by Kostas Michalopoulos + * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni + * v1.9: Display timestamps along the bottom, by Nick and Stev-io + * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D) + * Refactored by Krishna Narni, to support timestamp formatting function + * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh + * v1.11: options.grid.sharpLines option added, by @drewnoakes + * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes + * v1.12: Support for horizontalLines added, by @drewnoakes + * Support for yRangeFunction callback added, by @drewnoakes + * v1.13: Fixed typo (#32), by @alnikitich + * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano + * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes + * v1.15: Support for npm package (#18), by @dominictarr + * Fixed broken removeTimeSeries function (#24) by @davidgaleano + * Minor performance and tidying, by @drewnoakes + * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes + * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12) + * Documentation and some local variable renaming for clarity, by @drewnoakes + * v1.17: Allow control over font size (#10), by @drewnoakes + * Timestamp text won't overlap, by @drewnoakes + * v1.18: Allow control of max/min label precision, by @drewnoakes + * Added 'borderVisible' chart option, by @drewnoakes + * Allow drawing series with fill but no stroke (line), by @drewnoakes + * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai + * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes + * v1.21: Add 'step' interpolation mode, by @drewnoakes + * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic + * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes + * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf + * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92 + * Draw time labels on top of series, by @comolosabia + * Add TimeSeries.clear function, by @drewnoakes + * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic + * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush + * v1.28: Add 'minValueScale' option, by @megawac + * Fix 'labelPos' for different size of 'minValueString' 'maxValueString', by @henryn + * v1.29: Support responsive sizing, by @drewnoakes + * v1.29.1: Include types in package, and make property optional, by @TrentHouliston + * v1.30: Fix inverted logic in devicePixelRatio support, by @scanlime + * v1.31: Support tooltips, by @Sly1024 and @drewnoakes + * v1.32: Support frame rate limit, by @dpuyosa + * v1.33: Use Date static method instead of instance, by @nnnoel + * Fix bug with tooltips when multiple charts on a page, by @jpmbiz70 + * v1.34: Add disabled option to TimeSeries, by @TechGuard (#91) + * Add nonRealtimeData option, by @annazhelt (#92, #93) + * Add showIntermediateLabels option, by @annazhelt (#94) + * Add displayDataFromPercentile option, by @annazhelt (#95) + * Fix bug when hiding tooltip element, by @ralphwetzel (#96) + * Support intermediate y-axis labels, by @beikeland (#99) + * v1.35: Fix issue with responsive mode at high DPI, by @drewnoakes (#101) + * v1.36: Add tooltipLabel to ITimeSeriesPresentationOptions. + * If tooltipLabel is present, tooltipLabel displays inside tooltip + * next to value, by @jackdesert (#102) + * Fix bug rendering issue in series fill when using scroll backwards, by @olssonfredrik + * Add title option, by @mesca + */ + +;(function(exports) { + + // Date.now polyfill + Date.now = Date.now || function() { return new Date().getTime(); }; + + var Util = { + extend: function() { + arguments[0] = arguments[0] || {}; + for (var i = 1; i < arguments.length; i++) + { + for (var key in arguments[i]) + { + if (arguments[i].hasOwnProperty(key)) + { + if (typeof(arguments[i][key]) === 'object') { + if (arguments[i][key] instanceof Array) { + arguments[0][key] = arguments[i][key]; + } else { + arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]); + } + } else { + arguments[0][key] = arguments[i][key]; + } + } + } + } + return arguments[0]; + }, + binarySearch: function(data, value) { + var low = 0, + high = data.length; + while (low < high) { + var mid = (low + high) >> 1; + if (value < data[mid][0]) + high = mid; + else + low = mid + 1; + } + return low; + } + }; + + /** + * Initialises a new <code>TimeSeries</code> with optional data options. + * + * Options are of the form (defaults shown): + * + * <pre> + * { + * resetBounds: true, // enables/disables automatic scaling of the y-axis + * resetBoundsInterval: 3000 // the period between scaling calculations, in millis + * } + * </pre> + * + * Presentation options for TimeSeries are specified as an argument to <code>SmoothieChart.addTimeSeries</code>. + * + * @constructor + */ + function TimeSeries(options) { + this.options = Util.extend({}, TimeSeries.defaultOptions, options); + this.disabled = false; + this.clear(); + } + + TimeSeries.defaultOptions = { + resetBoundsInterval: 3000, + resetBounds: true + }; + + /** + * Clears all data and state from this TimeSeries object. + */ + TimeSeries.prototype.clear = function() { + this.data = []; + this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries. + this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries. + }; + + /** + * Recalculate the min/max values for this <code>TimeSeries</code> object. + * + * This causes the graph to scale itself in the y-axis. + */ + TimeSeries.prototype.resetBounds = function() { + if (this.data.length) { + // Walk through all data points, finding the min/max value + this.maxValue = this.data[0][1]; + this.minValue = this.data[0][1]; + for (var i = 1; i < this.data.length; i++) { + var value = this.data[i][1]; + if (value > this.maxValue) { + this.maxValue = value; + } + if (value < this.minValue) { + this.minValue = value; + } + } + } else { + // No data exists, so set min/max to NaN + this.maxValue = Number.NaN; + this.minValue = Number.NaN; + } + }; + + /** + * Adds a new data point to the <code>TimeSeries</code>, preserving chronological order. + * + * @param timestamp the position, in time, of this data point + * @param value the value of this data point + * @param sumRepeatedTimeStampValues if <code>timestamp</code> has an exact match in the series, this flag controls + * whether it is replaced, or the values summed (defaults to false.) + */ + TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) { + // Rewind until we hit an older timestamp + var i = this.data.length - 1; + while (i >= 0 && this.data[i][0] > timestamp) { + i--; + } + + if (i === -1) { + // This new item is the oldest data + this.data.splice(0, 0, [timestamp, value]); + } else if (this.data.length > 0 && this.data[i][0] === timestamp) { + // Update existing values in the array + if (sumRepeatedTimeStampValues) { + // Sum this value into the existing 'bucket' + this.data[i][1] += value; + value = this.data[i][1]; + } else { + // Replace the previous value + this.data[i][1] = value; + } + } else if (i < this.data.length - 1) { + // Splice into the correct position to keep timestamps in order + this.data.splice(i + 1, 0, [timestamp, value]); + } else { + // Add to the end of the array + this.data.push([timestamp, value]); + } + + this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); + this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); + }; + + TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) { + // We must always keep one expired data point as we need this to draw the + // line that comes into the chart from the left, but any points prior to that can be removed. + var removeCount = 0; + while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { + removeCount++; + } + if (removeCount !== 0) { + this.data.splice(0, removeCount); + } + }; + + /** + * Initialises a new <code>SmoothieChart</code>. + * + * Options are optional, and should be of the form below. Just specify the values you + * need and the rest will be given sensible defaults as shown: + * + * <pre> + * { + * minValue: undefined, // specify to clamp the lower y-axis to a given value + * maxValue: undefined, // specify to clamp the upper y-axis to a given value + * maxValueScale: 1, // allows proportional padding to be added above the chart. for 10% padding, specify 1.1. + * minValueScale: 1, // allows proportional padding to be added below the chart. for 10% padding, specify 1.1. + * yRangeFunction: undefined, // function({min: , max: }) { return {min: , max: }; } + * scaleSmoothing: 0.125, // controls the rate at which y-value zoom animation occurs + * millisPerPixel: 20, // sets the speed at which the chart pans by + * enableDpiScaling: true, // support rendering at different DPI depending on the device + * yMinFormatter: function(min, precision) { // callback function that formats the min y value label + * return parseFloat(min).toFixed(precision); + * }, + * yMaxFormatter: function(max, precision) { // callback function that formats the max y value label + * return parseFloat(max).toFixed(precision); + * }, + * yIntermediateFormatter: function(intermediate, precision) { // callback function that formats the intermediate y value labels + * return parseFloat(intermediate).toFixed(precision); + * }, + * maxDataSetLength: 2, + * interpolation: 'bezier' // one of 'bezier', 'linear', or 'step' + * timestampFormatter: null, // optional function to format time stamps for bottom of chart + * // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; } + * scrollBackwards: false, // reverse the scroll direction of the chart + * horizontalLines: [], // [ { value: 0, color: '#ffffff', lineWidth: 1 } ] + * grid: + * { + * fillStyle: '#000000', // the background colour of the chart + * lineWidth: 1, // the pixel width of grid lines + * strokeStyle: '#777777', // colour of grid lines + * millisPerLine: 1000, // distance between vertical grid lines + * sharpLines: false, // controls whether grid lines are 1px sharp, or softened + * verticalSections: 2, // number of vertical sections marked out by horizontal grid lines + * borderVisible: true // whether the grid lines trace the border of the chart or not + * }, + * labels + * { + * disabled: false, // enables/disables labels showing the min/max values + * fillStyle: '#ffffff', // colour for text of labels, + * fontSize: 15, + * fontFamily: 'sans-serif', + * precision: 2, + * showIntermediateLabels: false, // shows intermediate labels between min and max values along y axis + * intermediateLabelSameAxis: true, + * }, + * title + * { + * text: '', // the text to display on the left side of the chart + * fillStyle: '#ffffff', // colour for text + * fontSize: 15, + * fontFamily: 'sans-serif', + * verticalAlign: 'middle' // one of 'top', 'middle', or 'bottom' + * }, + * tooltip: false // show tooltip when mouse is over the chart + * tooltipLine: { // properties for a vertical line at the cursor position + * lineWidth: 1, + * strokeStyle: '#BBBBBB' + * }, + * tooltipFormatter: SmoothieChart.tooltipFormatter, // formatter function for tooltip text + * nonRealtimeData: false, // use time of latest data as current time + * displayDataFromPercentile: 1, // display not latest data, but data from the given percentile + * // useful when trying to see old data saved by setting a high value for maxDataSetLength + * // should be a value between 0 and 1 + * responsive: false, // whether the chart should adapt to the size of the canvas + * limitFPS: 0 // maximum frame rate the chart will render at, in FPS (zero means no limit) + * } + * </pre> + * + * @constructor + */ + function SmoothieChart(options) { + this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); + this.seriesSet = []; + this.currentValueRange = 1; + this.currentVisMinValue = 0; + this.lastRenderTimeMillis = 0; + this.lastChartTimestamp = 0; + + this.mousemove = this.mousemove.bind(this); + this.mouseout = this.mouseout.bind(this); + } + + /** Formats the HTML string content of the tooltip. */ + SmoothieChart.tooltipFormatter = function (timestamp, data) { + var timestampFormatter = this.options.timestampFormatter || SmoothieChart.timeFormatter, + lines = [timestampFormatter(new Date(timestamp))], + label; + + for (var i = 0; i < data.length; ++i) { + label = data[i].series.options.tooltipLabel || '' + if (label !== ''){ + label = label + ' '; + } + lines.push('<span style="color:' + data[i].series.options.strokeStyle + '">' + + label + + this.options.yMaxFormatter(data[i].value, this.options.labels.precision) + '</span>'); + } + + return lines.join('<br>'); + }; + + SmoothieChart.defaultChartOptions = { + millisPerPixel: 20, + enableDpiScaling: true, + yMinFormatter: function(min, precision) { + return parseFloat(min).toFixed(precision); + }, + yMaxFormatter: function(max, precision) { + return parseFloat(max).toFixed(precision); + }, + yIntermediateFormatter: function(intermediate, precision) { + return parseFloat(intermediate).toFixed(precision); + }, + maxValueScale: 1, + minValueScale: 1, + interpolation: 'bezier', + scaleSmoothing: 0.125, + maxDataSetLength: 2, + scrollBackwards: false, + displayDataFromPercentile: 1, + grid: { + fillStyle: '#000000', + strokeStyle: '#777777', + lineWidth: 1, + sharpLines: false, + millisPerLine: 1000, + verticalSections: 2, + borderVisible: true + }, + labels: { + fillStyle: '#ffffff', + disabled: false, + fontSize: 10, + fontFamily: 'monospace', + precision: 2, + showIntermediateLabels: false, + intermediateLabelSameAxis: true, + }, + title: { + text: '', + fillStyle: '#ffffff', + fontSize: 15, + fontFamily: 'monospace', + verticalAlign: 'middle' + }, + horizontalLines: [], + tooltip: false, + tooltipLine: { + lineWidth: 1, + strokeStyle: '#BBBBBB' + }, + tooltipFormatter: SmoothieChart.tooltipFormatter, + nonRealtimeData: false, + responsive: false, + limitFPS: 0 + }; + + // Based on http://inspirit.github.com/jsfeat/js/compatibility.js + SmoothieChart.AnimateCompatibility = (function() { + var requestAnimationFrame = function(callback, element) { + var requestAnimationFrame = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(function() { + callback(Date.now()); + }, 16); + }; + return requestAnimationFrame.call(window, callback, element); + }, + cancelAnimationFrame = function(id) { + var cancelAnimationFrame = + window.cancelAnimationFrame || + function(id) { + clearTimeout(id); + }; + return cancelAnimationFrame.call(window, id); + }; + + return { + requestAnimationFrame: requestAnimationFrame, + cancelAnimationFrame: cancelAnimationFrame + }; + })(); + + SmoothieChart.defaultSeriesPresentationOptions = { + lineWidth: 1, + strokeStyle: '#ffffff' + }; + + /** + * Adds a <code>TimeSeries</code> to this chart, with optional presentation options. + * + * Presentation options should be of the form (defaults shown): + * + * <pre> + * { + * lineWidth: 1, + * strokeStyle: '#ffffff', + * fillStyle: undefined, + * tooltipLabel: undefined + * } + * </pre> + */ + SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { + this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)}); + if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { + timeSeries.resetBoundsTimerId = setInterval( + function() { + timeSeries.resetBounds(); + }, + timeSeries.options.resetBoundsInterval + ); + } + }; + + /** + * Removes the specified <code>TimeSeries</code> from the chart. + */ + SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + this.seriesSet.splice(i, 1); + break; + } + } + // If a timer was operating for that timeseries, remove it + if (timeSeries.resetBoundsTimerId) { + // Stop resetting the bounds, if we were + clearInterval(timeSeries.resetBoundsTimerId); + } + }; + + /** + * Gets render options for the specified <code>TimeSeries</code>. + * + * As you may use a single <code>TimeSeries</code> in multiple charts with different formatting in each usage, + * these settings are stored in the chart. + */ + SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + return this.seriesSet[i].options; + } + } + }; + + /** + * Brings the specified <code>TimeSeries</code> to the top of the chart. It will be rendered last. + */ + SmoothieChart.prototype.bringToFront = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + var set = this.seriesSet.splice(i, 1); + this.seriesSet.push(set[0]); + break; + } + } + }; + + /** + * Instructs the <code>SmoothieChart</code> to start rendering to the provided canvas, with specified delay. + * + * @param canvas the target canvas element + * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series + * from appearing on screen, with new values flashing into view, at the expense of some latency. + */ + SmoothieChart.prototype.streamTo = function(canvas, delayMillis) { + this.canvas = canvas; + this.delay = delayMillis; + this.start(); + }; + + SmoothieChart.prototype.getTooltipEl = function () { + // Create the tool tip element lazily + if (!this.tooltipEl) { + this.tooltipEl = document.createElement('div'); + this.tooltipEl.className = 'smoothie-chart-tooltip'; + this.tooltipEl.style.position = 'absolute'; + this.tooltipEl.style.display = 'none'; + document.body.appendChild(this.tooltipEl); + } + return this.tooltipEl; + }; + + SmoothieChart.prototype.updateTooltip = function () { + var el = this.getTooltipEl(); + + if (!this.mouseover || !this.options.tooltip) { + el.style.display = 'none'; + return; + } + + var time = this.lastChartTimestamp; + + // x pixel to time + var t = this.options.scrollBackwards + ? time - this.mouseX * this.options.millisPerPixel + : time - (this.canvas.offsetWidth - this.mouseX) * this.options.millisPerPixel; + + var data = []; + + // For each data set... + for (var d = 0; d < this.seriesSet.length; d++) { + var timeSeries = this.seriesSet[d].timeSeries; + if (timeSeries.disabled) { + continue; + } + + // find datapoint closest to time 't' + var closeIdx = Util.binarySearch(timeSeries.data, t); + if (closeIdx > 0 && closeIdx < timeSeries.data.length) { + data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] }); + } + } + + if (data.length) { + el.innerHTML = this.options.tooltipFormatter.call(this, t, data); + el.style.display = 'block'; + } else { + el.style.display = 'none'; + } + }; + + SmoothieChart.prototype.mousemove = function (evt) { + this.mouseover = true; + this.mouseX = evt.offsetX; + this.mouseY = evt.offsetY; + this.mousePageX = evt.pageX; + this.mousePageY = evt.pageY; + + var el = this.getTooltipEl(); + el.style.top = Math.round(this.mousePageY) + 'px'; + el.style.left = Math.round(this.mousePageX) + 'px'; + this.updateTooltip(); + }; + + SmoothieChart.prototype.mouseout = function () { + this.mouseover = false; + this.mouseX = this.mouseY = -1; + if (this.tooltipEl) + this.tooltipEl.style.display = 'none'; + }; + + /** + * Make sure the canvas has the optimal resolution for the device's pixel ratio. + */ + SmoothieChart.prototype.resize = function () { + var dpr = !this.options.enableDpiScaling || !window ? 1 : window.devicePixelRatio, + width, height; + if (this.options.responsive) { + // Newer behaviour: Use the canvas's size in the layout, and set the internal + // resolution according to that size and the device pixel ratio (eg: high DPI) + width = this.canvas.offsetWidth; + height = this.canvas.offsetHeight; + + if (width !== this.lastWidth) { + this.lastWidth = width; + this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); + this.canvas.getContext('2d').scale(dpr, dpr); + } + if (height !== this.lastHeight) { + this.lastHeight = height; + this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); + this.canvas.getContext('2d').scale(dpr, dpr); + } + } else if (dpr !== 1) { + // Older behaviour: use the canvas's inner dimensions and scale the element's size + // according to that size and the device pixel ratio (eg: high DPI) + width = parseInt(this.canvas.getAttribute('width')); + height = parseInt(this.canvas.getAttribute('height')); + + if (!this.originalWidth || (Math.floor(this.originalWidth * dpr) !== width)) { + this.originalWidth = width; + this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); + this.canvas.style.width = width + 'px'; + this.canvas.getContext('2d').scale(dpr, dpr); + } + + if (!this.originalHeight || (Math.floor(this.originalHeight * dpr) !== height)) { + this.originalHeight = height; + this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); + this.canvas.style.height = height + 'px'; + this.canvas.getContext('2d').scale(dpr, dpr); + } + } + }; + + /** + * Starts the animation of this chart. + */ + SmoothieChart.prototype.start = function() { + if (this.frame) { + // We're already running, so just return + return; + } + + this.canvas.addEventListener('mousemove', this.mousemove); + this.canvas.addEventListener('mouseout', this.mouseout); + + // Renders a frame, and queues the next frame for later rendering + var animate = function() { + this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() { + if(this.options.nonRealtimeData){ + var dateZero = new Date(0); + // find the data point with the latest timestamp + var maxTimeStamp = this.seriesSet.reduce(function(max, series){ + var dataSet = series.timeSeries.data; + var indexToCheck = Math.round(this.options.displayDataFromPercentile * dataSet.length) - 1; + indexToCheck = indexToCheck >= 0 ? indexToCheck : 0; + indexToCheck = indexToCheck <= dataSet.length -1 ? indexToCheck : dataSet.length -1; + if(dataSet && dataSet.length > 0) + { + // timestamp corresponds to element 0 of the data point + var lastDataTimeStamp = dataSet[indexToCheck][0]; + max = max > lastDataTimeStamp ? max : lastDataTimeStamp; + } + return max; + }.bind(this), dateZero); + // use the max timestamp as current time + this.render(this.canvas, maxTimeStamp > dateZero ? maxTimeStamp : null); + } else { + this.render(); + } + animate(); + }.bind(this)); + }.bind(this); + + animate(); + }; + + /** + * Stops the animation of this chart. + */ + SmoothieChart.prototype.stop = function() { + if (this.frame) { + SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); + delete this.frame; + this.canvas.removeEventListener('mousemove', this.mousemove); + this.canvas.removeEventListener('mouseout', this.mouseout); + } + }; + + SmoothieChart.prototype.updateValueRange = function() { + // Calculate the current scale of the chart, from all time series. + var chartOptions = this.options, + chartMaxValue = Number.NaN, + chartMinValue = Number.NaN; + + for (var d = 0; d < this.seriesSet.length; d++) { + // TODO(ndunn): We could calculate / track these values as they stream in. + var timeSeries = this.seriesSet[d].timeSeries; + if (timeSeries.disabled) { + continue; + } + + if (!isNaN(timeSeries.maxValue)) { + chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; + } + + if (!isNaN(timeSeries.minValue)) { + chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; + } + } + + // Scale the chartMaxValue to add padding at the top if required + if (chartOptions.maxValue != null) { + chartMaxValue = chartOptions.maxValue; + } else { + chartMaxValue *= chartOptions.maxValueScale; + } + + // Set the minimum if we've specified one + if (chartOptions.minValue != null) { + chartMinValue = chartOptions.minValue; + } else { + chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue); + } + + // If a custom range function is set, call it + if (this.options.yRangeFunction) { + var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue}); + chartMinValue = range.min; + chartMaxValue = range.max; + } + + if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { + var targetValueRange = chartMaxValue - chartMinValue; + var valueRangeDiff = (targetValueRange - this.currentValueRange); + var minValueDiff = (chartMinValue - this.currentVisMinValue); + this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1; + this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff; + this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff; + } + + this.valueRange = { min: chartMinValue, max: chartMaxValue }; + }; + + SmoothieChart.prototype.render = function(canvas, time) { + var nowMillis = Date.now(); + + // Respect any frame rate limit. + if (this.options.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < (1000/this.options.limitFPS)) + return; + + if (!this.isAnimatingScale) { + // We're not animating. We can use the last render time and the scroll speed to work out whether + // we actually need to paint anything yet. If not, we can return immediately. + + // Render at least every 1/6th of a second. The canvas may be resized, which there is + // no reliable way to detect. + var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel); + + if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) { + return; + } + } + + this.resize(); + this.updateTooltip(); + + this.lastRenderTimeMillis = nowMillis; + + canvas = canvas || this.canvas; + time = time || nowMillis - (this.delay || 0); + + // Round time down to pixel granularity, so motion appears smoother. + time -= time % this.options.millisPerPixel; + + this.lastChartTimestamp = time; + + var context = canvas.getContext('2d'), + chartOptions = this.options, + dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight }, + // Calculate the threshold time for the oldest data points. + oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel), + valueToYPixel = function(value) { + var offset = value - this.currentVisMinValue; + return this.currentValueRange === 0 + ? dimensions.height + : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height)); + }.bind(this), + timeToXPixel = function(t) { + if(chartOptions.scrollBackwards) { + return Math.round((time - t) / chartOptions.millisPerPixel); + } + return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel)); + }; + + this.updateValueRange(); + + context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; + + // Save the state of the canvas context, any transformations applied in this method + // will get removed from the stack at the end of this method when .restore() is called. + context.save(); + + // Move the origin. + context.translate(dimensions.left, dimensions.top); + + // Create a clipped rectangle - anything we draw will be constrained to this rectangle. + // This prevents the occasional pixels from curves near the edges overrunning and creating + // screen cheese (that phrase should need no explanation). + context.beginPath(); + context.rect(0, 0, dimensions.width, dimensions.height); + context.clip(); + + // Clear the working area. + context.save(); + context.fillStyle = chartOptions.grid.fillStyle; + context.clearRect(0, 0, dimensions.width, dimensions.height); + context.fillRect(0, 0, dimensions.width, dimensions.height); + context.restore(); + + // Grid lines... + context.save(); + context.lineWidth = chartOptions.grid.lineWidth; + context.strokeStyle = chartOptions.grid.strokeStyle; + // Vertical (time) dividers. + if (chartOptions.grid.millisPerLine > 0) { + context.beginPath(); + for (var t = time - (time % chartOptions.grid.millisPerLine); + t >= oldestValidTime; + t -= chartOptions.grid.millisPerLine) { + var gx = timeToXPixel(t); + if (chartOptions.grid.sharpLines) { + gx -= 0.5; + } + context.moveTo(gx, 0); + context.lineTo(gx, dimensions.height); + } + context.stroke(); + context.closePath(); + } + + // Horizontal (value) dividers. + for (var v = 1; v < chartOptions.grid.verticalSections; v++) { + var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections); + if (chartOptions.grid.sharpLines) { + gy -= 0.5; + } + context.beginPath(); + context.moveTo(0, gy); + context.lineTo(dimensions.width, gy); + context.stroke(); + context.closePath(); + } + // Bounding rectangle. + if (chartOptions.grid.borderVisible) { + context.beginPath(); + context.strokeRect(0, 0, dimensions.width, dimensions.height); + context.closePath(); + } + context.restore(); + + // Draw any horizontal lines... + if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { + for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { + var line = chartOptions.horizontalLines[hl], + hly = Math.round(valueToYPixel(line.value)) - 0.5; + context.strokeStyle = line.color || '#ffffff'; + context.lineWidth = line.lineWidth || 1; + context.beginPath(); + context.moveTo(0, hly); + context.lineTo(dimensions.width, hly); + context.stroke(); + context.closePath(); + } + } + + // For each data set... + for (var d = 0; d < this.seriesSet.length; d++) { + context.save(); + var timeSeries = this.seriesSet[d].timeSeries; + if (timeSeries.disabled) { + continue; + } + + var dataSet = timeSeries.data, + seriesOptions = this.seriesSet[d].options; + + // Delete old data that's moved off the left of the chart. + timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); + + // Set style for this dataSet. + context.lineWidth = seriesOptions.lineWidth; + context.strokeStyle = seriesOptions.strokeStyle; + // Draw the line... + context.beginPath(); + // Retain lastX, lastY for calculating the control points of bezier curves. + var firstX = 0, firstY = 0, lastX = 0, lastY = 0; + for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) { + var x = timeToXPixel(dataSet[i][0]), + y = valueToYPixel(dataSet[i][1]); + + if (i === 0) { + firstX = x; + firstY = y; + context.moveTo(x, y); + } else { + switch (chartOptions.interpolation) { + case "linear": + case "line": { + context.lineTo(x,y); + break; + } + case "bezier": + default: { + // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves + // + // Assuming A was the last point in the line plotted and B is the new point, + // we draw a curve with control points P and Q as below. + // + // A---P + // | + // | + // | + // Q---B + // + // Importantly, A and P are at the same y coordinate, as are B and Q. This is + // so adjacent curves appear to flow as one. + // + context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop + Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) + Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) + x, y); // endPoint (B) + break; + } + case "step": { + context.lineTo(x,lastY); + context.lineTo(x,y); + break; + } + } + } + + lastX = x; lastY = y; + } + + if (dataSet.length > 1) { + if (seriesOptions.fillStyle) { + // Close up the fill region. + if (chartOptions.scrollBackwards) { + context.lineTo(lastX, dimensions.height + seriesOptions.lineWidth); + context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); + context.lineTo(firstX, firstY); + } else { + context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); + context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); + context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); + } + context.fillStyle = seriesOptions.fillStyle; + context.fill(); + } + + if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') { + context.stroke(); + } + context.closePath(); + } + context.restore(); + } + + if (chartOptions.tooltip && this.mouseX >= 0) { + // Draw vertical bar to show tooltip position + context.lineWidth = chartOptions.tooltipLine.lineWidth; + context.strokeStyle = chartOptions.tooltipLine.strokeStyle; + context.beginPath(); + context.moveTo(this.mouseX, 0); + context.lineTo(this.mouseX, dimensions.height); + context.closePath(); + context.stroke(); + this.updateTooltip(); + } + + // Draw the axis values on the chart. + if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { + var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision), + minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision), + maxLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2, + minLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(minValueString).width - 2; + context.fillStyle = chartOptions.labels.fillStyle; + context.fillText(maxValueString, maxLabelPos, chartOptions.labels.fontSize); + context.fillText(minValueString, minLabelPos, dimensions.height - 2); + } + + // Display intermediate y axis labels along y-axis to the left of the chart + if ( chartOptions.labels.showIntermediateLabels + && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max) + && chartOptions.grid.verticalSections > 0) { + // show a label above every vertical section divider + var step = (this.valueRange.max - this.valueRange.min) / chartOptions.grid.verticalSections; + var stepPixels = dimensions.height / chartOptions.grid.verticalSections; + for (var v = 1; v < chartOptions.grid.verticalSections; v++) { + var gy = dimensions.height - Math.round(v * stepPixels); + if (chartOptions.grid.sharpLines) { + gy -= 0.5; + } + var yValue = chartOptions.yIntermediateFormatter(this.valueRange.min + (v * step), chartOptions.labels.precision); + //left of right axis? + intermediateLabelPos = + chartOptions.labels.intermediateLabelSameAxis + ? (chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(yValue).width - 2) + : (chartOptions.scrollBackwards ? dimensions.width - context.measureText(yValue).width - 2 : 0); + + context.fillText(yValue, intermediateLabelPos, gy - chartOptions.grid.lineWidth); + } + } + + // Display timestamps along x-axis at the bottom of the chart. + if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) { + var textUntilX = chartOptions.scrollBackwards + ? context.measureText(minValueString).width + : dimensions.width - context.measureText(minValueString).width + 4; + for (var t = time - (time % chartOptions.grid.millisPerLine); + t >= oldestValidTime; + t -= chartOptions.grid.millisPerLine) { + var gx = timeToXPixel(t); + // Only draw the timestamp if it won't overlap with the previously drawn one. + if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) { + // Formats the timestamp based on user specified formatting function + // SmoothieChart.timeFormatter function above is one such formatting option + var tx = new Date(t), + ts = chartOptions.timestampFormatter(tx), + tsWidth = context.measureText(ts).width; + + textUntilX = chartOptions.scrollBackwards + ? gx + tsWidth + 2 + : gx - tsWidth - 2; + + context.fillStyle = chartOptions.labels.fillStyle; + if(chartOptions.scrollBackwards) { + context.fillText(ts, gx, dimensions.height - 2); + } else { + context.fillText(ts, gx - tsWidth, dimensions.height - 2); + } + } + } + } + + // Display title. + if (chartOptions.title.text !== '') { + context.font = chartOptions.title.fontSize + 'px ' + chartOptions.title.fontFamily; + var titleXPos = chartOptions.scrollBackwards ? dimensions.width - context.measureText(chartOptions.title.text).width - 2 : 2; + if (chartOptions.title.verticalAlign == 'bottom') { + context.textBaseline = 'bottom'; + var titleYPos = dimensions.height; + } else if (chartOptions.title.verticalAlign == 'middle') { + context.textBaseline = 'middle'; + var titleYPos = dimensions.height / 2; + } else { + context.textBaseline = 'top'; + var titleYPos = 0; + } + context.fillStyle = chartOptions.title.fillStyle; + context.fillText(chartOptions.title.text, titleXPos, titleYPos); + } + + context.restore(); // See .save() above. + }; + + // Sample timestamp formatting function + SmoothieChart.timeFormatter = function(date) { + function pad2(number) { return (number < 10 ? '0' : '') + number } + return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); + }; + + exports.TimeSeries = TimeSeries; + exports.SmoothieChart = SmoothieChart; + +})(typeof exports === 'undefined' ? this : exports); + diff --git a/client/ui/uiButton.js b/client/ui/uiButton.js new file mode 100644 index 0000000000000000000000000000000000000000..dada8bdafad5c846919af0d97981907a9709263e --- /dev/null +++ b/client/ui/uiButton.js @@ -0,0 +1 @@ +console.log('hello lib load') \ No newline at end of file diff --git a/files/dogbone.gcode b/files/dogbone.gcode index 8eada8236f36aefc55308843150274ba79dfa562..fa1b9172c05fb0022b4349ce82f2e52ea03fd92e 100644 --- a/files/dogbone.gcode +++ b/files/dogbone.gcode @@ -1,6 +1,6 @@ G0 F25 Z5 G0 X27.5Y111.5 -G0 Z-2 +G0 Z3 G0 Y83.5 G0 X14.5 G0 Y27.5 @@ -13,7 +13,7 @@ G0 Y83.5 G0 X-1.5 G0 Y111.5 G0 X27.5 -G0 Z-4 +G0 Z2 G0 Y83.5 G0 X14.5 G0 Y27.5 @@ -26,7 +26,7 @@ G0 Y83.5 G0 X-1.5 G0 Y111.5 G0 X27.5 -G0 Z-6 +G0 Z1 G0 Y83.5 G0 X14.5 G0 Y27.5 @@ -39,5 +39,5 @@ G0 Y83.5 G0 X-1.5 G0 Y111.5 G0 X27.5 -G0 Z10 -G0 X-40Y140 \ No newline at end of file +G0 Z5 +G0 X10Y100 \ No newline at end of file diff --git a/modules/flowcontrol/and.js b/modules/flowcontrol/and.js index e3825b19187349b19dbc2348e101b5be3590f308..d299e60aab6b8cb57dd1d593c7b15dfea72ef8b4 100644 --- a/modules/flowcontrol/and.js +++ b/modules/flowcontrol/and.js @@ -1,9 +1,9 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button // the 'andflow' flowcontrol unit only lets events through once both have occurred ... diff --git a/modules/hardware/atkbreadboard.js b/modules/hardware/atkbreadboardboard.js similarity index 89% rename from modules/hardware/atkbreadboard.js rename to modules/hardware/atkbreadboardboard.js index e240dd84ebe86902d3b3401b386b035daa5953e4..98bfd272ff571f9e4d4823d4e5c9e566d31ff29a 100644 --- a/modules/hardware/atkbreadboard.js +++ b/modules/hardware/atkbreadboardboard.js @@ -1,12 +1,12 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button - -const Hardware = require('../../lib/atkunit.js') -const PCKT = require('../../lib/packets.js') +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button + +const Hardware = require('../../src/atkunit.js') +const PCKT = require('../../src/packets.js') // a constructor, a fn, a javascript mess function ATKBreadBoardBoard() { diff --git a/modules/hardware/atkmrobot.js b/modules/hardware/atkmrobot.js index de127aed0222dfff2dab61c91444201846b65733..70cf5d71a957d54bb5184502cea5ed75cafb64f8 100644 --- a/modules/hardware/atkmrobot.js +++ b/modules/hardware/atkmrobot.js @@ -1,12 +1,12 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button - -const Hardware = require('../../lib/atkunit.js') -const PCKT = require('../../lib/packets.js') +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button + +const Hardware = require('../../src/atkunit.js') +const PCKT = require('../../src/packets.js') // a constructor, a fn, a javascript mess function ATKMathRobot() { diff --git a/modules/hardware/atkseriallink.js b/modules/hardware/atkseriallink.js index f97d49311b297e7d0f9393aab0d5527b18bb3b55..2c2e6729382f219d88c17f4317d3d9fede22159f 100644 --- a/modules/hardware/atkseriallink.js +++ b/modules/hardware/atkseriallink.js @@ -1,10 +1,10 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output -let State = InOut.State -let Button = InOut.Button +let State = JSUnit.State +let Button = JSUnit.Button const SerialPort = require('serialport') diff --git a/modules/hardware/atkstepper.js b/modules/hardware/atkstepper.js index 1a9fdf7a29ec01dbe8e86f574ba6a2df251dd615..d819b3482f1bc16faefb214e26bf72d4df09fe2a 100644 --- a/modules/hardware/atkstepper.js +++ b/modules/hardware/atkstepper.js @@ -1,16 +1,16 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output -let State = InOut.State -let Button = InOut.Button +let State = JSUnit.State +let Button = JSUnit.Button const MJS = require('mathjs') -const DCRT = require('../../lib/cartesian.js') +const DCRT = require('../../src/cartesian.js') -const Hardware = require('../../lib/atkunit.js') -const PCKT = require('../../lib/packets.js') +const Hardware = require('../../src/atkunit.js') +const PCKT = require('../../src/packets.js') function Stepper() { diff --git a/modules/hardware/webcam.js b/modules/hardware/webcam.js index bce6df3c338ba9c1d361e87ba18cbb64b6282e58..3ac82a73e2d2246598afae35348bf69d0b002537 100644 --- a/modules/hardware/webcam.js +++ b/modules/hardware/webcam.js @@ -1,9 +1,9 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button var NodeWebcam = require("node-webcam") diff --git a/modules/motion/planner.js b/modules/motion/planner.js index 8f2f19cd5a66e582fc04401bfc48ab20a5d0d37b..04e088a6077bfa0321eee733682601eb24832564 100644 --- a/modules/motion/planner.js +++ b/modules/motion/planner.js @@ -1,12 +1,12 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button // descartes, to you -const DCRT = require('../../lib/cartesian.js') +const DCRT = require('../../src/cartesian.js') const MJS = require('mathjs') // planner consumes target moves (i.e. segments having uniform speed throughout) diff --git a/modules/motion/rawmove.js b/modules/motion/rawmove.js index 18d244c7a2d4347fed33105d01cd3002609092ce..3a25b1d3015651a8e500d7216499507945b7d808 100644 --- a/modules/motion/rawmove.js +++ b/modules/motion/rawmove.js @@ -1,9 +1,9 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button function RawMove() { var rawmove = { diff --git a/modules/parsing/gcode.js b/modules/parsing/gcode.js index ae2ecb824e1847b9d848b92e11e4f08b04884392..ac118fb9fad30312054152a45a1bb4a984f32e0f 100644 --- a/modules/parsing/gcode.js +++ b/modules/parsing/gcode.js @@ -1,8 +1,8 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State function Gcode() { diff --git a/modules/ui/button.js b/modules/ui/button.js index d8435aa67b7f57e49eec959f99d81851e199c6d9..a86e9acd4156e88bfd25e44d579f28f90285c2b5 100644 --- a/modules/ui/button.js +++ b/modules/ui/button.js @@ -1,9 +1,9 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button // a constructor, a fn, a javascript mess function uiButton() { diff --git a/modules/ui/multiline.js b/modules/ui/multiline.js index 376f7ae1aaecce61503eaea056c58ae52ee93b78..da519c5bee14ce19c20ba068227011cf251949af 100644 --- a/modules/ui/multiline.js +++ b/modules/ui/multiline.js @@ -1,11 +1,9 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output -let MultiLine = InOut.MultiLine -let Button = InOut.Button +let State = JSUnit.State const fs = require('fs') @@ -22,9 +20,8 @@ function MultiLineIn() { multilinein.state = State() // alias ! - var state = multilinein.state - state.load = Button('LOAD', onLoadFile) - state.thru = Button('WHAM', lineThru) + state.load = State.newButton('--', onLoadFile) + state.thru = Button('THRU', lineThru) state.previously = MultiLine('lines complete', 11) state.now = MultiLine('line just out', 1) state.incoming = MultiLine('future lines', 36) @@ -94,6 +91,8 @@ function MultiLineIn() { multilinein.load = onLoadFile + onLoadFile('./files/dogbone.gcode') + function onExternalLine(str) { // push new str to bottom of queue } diff --git a/modules/ui/number.js b/modules/ui/number.js index bff0d2b803f7c2679e24b65c121afdfef1367de7..c0c17a1b3d79893765689c4af58354b3942a406f 100644 --- a/modules/ui/number.js +++ b/modules/ui/number.js @@ -1,9 +1,9 @@ // boilerplate rndmc header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button // a constructor, a fn, a javascript mess function uiNum() { diff --git a/modules/ui/stest.js b/modules/ui/stest.js new file mode 100644 index 0000000000000000000000000000000000000000..d44751aed2443012477f71a98ed020271dc74fed --- /dev/null +++ b/modules/ui/stest.js @@ -0,0 +1,61 @@ +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State + +// interface elements +const JSUI = require('../../src/jsui.js') +let UI = JSUI.UI + +function STest() { + var stest = { + description:{ + name: 'test-for-state', + alt: 'a description' + } + } + + // state is numbers, strings, arrays of either, booleans + // booleans are buttons + stest.state = State() + var state = stest.state + state.num = 12 + state.str = 'one string' + + state.arr = new Array() + state.arr.push(12) + state.arr.push(24) + + state.strarr = new Array() + state.strarr.push('str1') + state.strarr.push('str2') + + state.boolean = false + + stest.inputs = { + inp: Input('any', onInp) + } + + function onInp(evt){ + console.log('ex input', evt) + } + + stest.outputs = { + outp: Output('any') + } + + + // other items for interaction are explicitly UI + stest.ui = UI() + var ui = stest.ui + ui.addElement('btnex', './ui/uiButton.js', onButtonData) + + function onButtonData(evt){ + console.log('button change evt', evt) + ui.btnex.isPressed = true + } + + return stest +} + +module.exports = STest \ No newline at end of file diff --git a/modules/ui/terminal.js b/modules/ui/terminal.js index 62a3a404869d440ab8e6467aaf3ba8f4e664d1a3..c153aa9a59fd4e994ec1c9ea0d569dcf26be736a 100644 --- a/modules/ui/terminal.js +++ b/modules/ui/terminal.js @@ -1,8 +1,8 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State // a constructor, a fn, a javascript mess function Terminal() { diff --git a/modules/util/andgate.js b/modules/util/andgate.js index 6f1b4965db0a755a659652df0ba71276a62c8418..0ad34185ab53418225c516f8fb6fca72378fc598 100644 --- a/modules/util/andgate.js +++ b/modules/util/andgate.js @@ -1,9 +1,9 @@ // boilerplate atkapi header -const InOut = require('../../lib/jsunit.js') -let Input = InOut.Input -let Output = InOut.Output -let State = InOut.State -let Button = InOut.Button +const JSUnit = require('../../src/jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button // a constructor, a fn, a javascript mess function AndFlowControl() { diff --git a/modules/util/delay.js b/modules/util/delay.js index fd7b9f06b7cd7b7ce3088b74baae2a7c28f0a2dc..b20c87c1b4da5063d03c35185625863b41311951 100644 --- a/modules/util/delay.js +++ b/modules/util/delay.js @@ -1,6 +1,6 @@ // boilerplate atkapi header // boilerplate header -const JSUnit = require('../../lib/jsunit.js') +const JSUnit = require('../../src/jsunit.js') let Input = JSUnit.Input let Output = JSUnit.Output let State = JSUnit.State diff --git a/modules/util/gate.js b/modules/util/gate.js index ea6e1c9e53a34afef3472d1e87895935dc347b90..cada67a4316aea7b16c783645bcf31b8b05ef37c 100644 --- a/modules/util/gate.js +++ b/modules/util/gate.js @@ -1,5 +1,5 @@ // boilerplate header -const JSUnit = require('../../lib/jsunit.js') +const JSUnit = require('../../src/jsunit.js') let Input = JSUnit.Input let Output = JSUnit.Output let State = JSUnit.State diff --git a/modules/util/log.js b/modules/util/log.js index 69c4f60f2306c043a131e8ab86c2e087ae4bf4a7..eb9ad295a1f96feb084b940eab3769be72ddec5b 100644 --- a/modules/util/log.js +++ b/modules/util/log.js @@ -1,5 +1,5 @@ // boilerplate header -const JSUnit = require('../../lib/jsunit.js') +const JSUnit = require('../../src/jsunit.js') let Input = JSUnit.Input let Output = JSUnit.Output let State = JSUnit.State diff --git a/programs.js b/programs.js index 9582a6ec53a98a1b3612f50191257d6460b443b9..97121d8c5402aef1558711ecc3eb8925138ec48a 100644 --- a/programs.js +++ b/programs.js @@ -2,9 +2,11 @@ const fs = require('fs') const Reps = require('./reps.js') -const JSUnit = require('./lib/jsunit.js') +const JSUnit = require('./src/jsunit.js') let isStateKey = JSUnit.isStateKey +var socket = {} + function newProgram(name) { var program = { description: { @@ -20,6 +22,7 @@ function newProgram(name) { function loadModuleFromSource(program, path, id) { // source -> heap if (fs.existsSync(path)) { + // compile a new object based on definition in path var src = require(path) var mod = new src() @@ -41,47 +44,41 @@ function loadModuleFromSource(program, path, id) { // add to program object program.modules[mod.description.id] = mod - /* ---------------------------------- */ - // WARN! Corner Case should Go Away or Improve at next spiral - if (mod.description.isLink) { - for (mdlName in program.modules) { - if (program.modules[mdlName].description.isLink) { - console.log("PRGMEM ONLY BIG ENOUGH FOR ONE LINK") - //process.exit() - } - } - } - // end corner case code - /* ---------------------------------- */ - // input need references for later hookup for (key in mod.inputs) { mod.inputs[key].parentId = mod.description.id mod.inputs[key].key = key } - // state updating, begs for update + // state items get wrapped in a getter / setter + // so that changes from internal modules can + // push to UI + mod.state.init(mod.description.id, socket) + + mod.ui.init(mod.description.id, socket) + + /* for (key in mod.state) { - if (key == 'onChange' | key == 'emitChange' | key == 'emitters') { - //console.log('rolling past change fn') - } else { - mod.state['_' + key] = mod.state[key] - mod.state[key] = {} + if(isStateKey(key)){ writeStateObject(mod, key) } } + */ + + console.log('ADDING', mod.description.id, 'to', program.description.id) - if (program.description.id == null) { - if (program.description.name == null) { - if (program.description == null) { - program.description = {} + /* ---------------------------------- */ + // WARN! Corner Case should Go Away or Improve at next spiral + if (mod.description.isLink) { + for (mdlName in program.modules) { + if (program.modules[mdlName].description.isLink) { + console.log("PRGMEM ONLY BIG ENOUGH FOR ONE LINK") + //process.exit() } - program.description.name = 'unnamed program' } - program.description.id = program.description.name + '-' + 0 } - - console.log('ADDING', mod.description.id, 'to', program.description.id) + // end corner case code + /* ---------------------------------- */ /* ---------------------------------- */ // WARN! Corner Case should Go Away or Improve at next spiral @@ -115,17 +112,16 @@ function loadModuleFromSource(program, path, id) { } } +/* function writeStateObject(mod, key) { + mod.state['_' + key] = mod.state[key] + mod.state[key] = {} + Object.defineProperty(mod.state, key, { set: function(x) { // update internal value this['_' + key] = x // console.log('SET', key, this['_' + key]) - // push to internal state change handler - // let's call emitChange from the server-side ... - // so that we don't get into any heavy VIR - // when we change it within the module - // this.emitChange(key) // push to external view if (socket) { pushState(mod, key) @@ -134,11 +130,14 @@ function writeStateObject(mod, key) { }) Object.defineProperty(mod.state, key, { get: function() { + console.log('KEY', key) + console.log("IS", this['_' + key]) //console.log('GET', key, this['_' + key]) return this['_' + key] } }) } +*/ function removeModuleFromProgram(program, id) { // this simple? @@ -159,19 +158,10 @@ EXTERNAL HOOKS */ function assignSocket(sckt) { - socket = sckt + // we can pass this object 'down' by reference, once it loads + socket.send = sckt.send } -function pushState(mod, key) { - var data = { - id: mod.description.id, - key: key, - val: mod.state[key] - } - socket.send('put state change', data) -} - - function saveProgram(prgmem, path) { // ok, and we're interested in just copying the relevant things ... var svprgmem = { @@ -195,7 +185,6 @@ function saveProgram(prgmem, path) { console.log('PROGRAM SAVED AT', path) } - function openProgram(path) { var program = {} @@ -263,7 +252,7 @@ function openProgram(path) { // TODO: states: sometimes we load, we want to run the change emitter ... sometimes we don't // what choice ? mdl.state['_' + key] = mdlRep.state[key] - mdl.state.emitChange('route') + mdl.state.emitUIChange('route') } else { mdl.state['_' + key] = mdlRep.state[key] } diff --git a/reps.js b/reps.js index 853de088575b62d3e43b9575dbf45448cf687e16..9b7f466705c222a5d7ac3df5e49dec0daf1bfe44 100644 --- a/reps.js +++ b/reps.js @@ -1,29 +1,16 @@ -const JSUnit = require('./lib/jsunit.js') +const JSUnit = require('./src/jsunit.js') let isStateKey = JSUnit.isStateKey +const JSUI = require('./src/jsui.js') +let isUiKey = JSUI.isUiKey + function makeRepFromModule(mdl) { // rep != mdl // rep is rep of mdl - var rep = { - description: { - id: mdl.description.id, - name: mdl.description.name, - alt: mdl.description.alt, - path: mdl.description.path - } - } - - // yikes tho, corner cases should be easier ... - if(mdl.description.isHardware){ - rep.description.isHardware = true - } - if(mdl.description.isLink){ - rep.description.isLink = true - } - - if(mdl.description.position){ - rep.description.position = mdl.description.position + // deep copy description + var rep = { + description: JSON.parse(JSON.stringify(mdl.description)) } // TODO: making rep. of input / output should be a f'n of that object ... @@ -56,6 +43,15 @@ function makeRepFromModule(mdl) { } } + rep.ui = {} + for(key in mdl.ui){ + if(isUiKey(key)){ + rep.ui[key] = {} + rep.ui[key].type = mdl.ui[key].type + rep.ui[key].clientPath = mdl.ui[key].clientPath + } + } + return rep } diff --git a/main.js b/rundmc.js similarity index 88% rename from main.js rename to rundmc.js index 20d41bd1a242e9f4cdc0c3f30332cb0eeca21971..e9c38aaf1ec31d16ea9e6a8efd977a7b124d52a7 100644 --- a/main.js +++ b/rundmc.js @@ -1,6 +1,7 @@ // // // new node controller / HEAP +// reconfigurable numeric dataflow machine controller / RNDMC // // main.js // @@ -30,6 +31,11 @@ const Programs = require('./programs.js') // the program object: real simple, just has a description, and a 'modules' var program = Programs.new('new program') +var stest = Programs.loadModuleFromSource(program, './modules/ui/stest.js') + +var rep = Reps.makeFromModule(stest) +console.log('rep', rep) + /* example program-like-an-api // load some modules var multiline = Programs.loadModuleFromSource(program, './modules/ui/multiline.js') diff --git a/src/atkroute.js b/src/atkroute.js new file mode 100644 index 0000000000000000000000000000000000000000..02c2ba4f2b66c4651fb6de66b3b243f2b1a2483d --- /dev/null +++ b/src/atkroute.js @@ -0,0 +1,37 @@ +// object to extend for things-that-are-hardware + +// and pass f'n to call on received messages +function ATKRoute(route, calls) { + var atkroute = { + isAtkRoute: true, + link: null, // pls, deliver 2 me a hw outlet + route: route, + calls: {} + } + + atkroute.send = function(msg) { + if (this.link != null) { + // CHECKS and append route, then send + this.link.send(msg, this) + } else { + console.log("NO LINK NO SEND") + } + } + + atkroute.subscribe = function(key, callback){ + this.calls[key.toString()] = callback + } + + atkroute.onMessage = function(msg){ + // one key at a time, for now + var key = msg[0].toString() + // *could* slice the key out at this point, but nah + if(this.calls[key] != null){ + this.calls[key](msg) + } + } + + return atkroute +} + +module.exports = ATKRoute \ No newline at end of file diff --git a/src/atkunit.js b/src/atkunit.js new file mode 100644 index 0000000000000000000000000000000000000000..529bc8b1a6bff81c8d102111299ebf555a97b875 --- /dev/null +++ b/src/atkunit.js @@ -0,0 +1,59 @@ +// software object for reciprocal hardware object + +// boilerplate atkapi header +const JSUnit = require('./jsunit.js') +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State +let Button = JSUnit.Button + +let ATKRoute = require('./atkroute.js') + +function Hardware(){ + var hardware = { + description:{ + name: 'hardwareUnit', + alt: 'software representation of networked hardware object', + isHardware: true + }, + route: ATKRoute('0,0') + } + + hardware.state = State() + var state = hardware.state + + state.reset = Button('reset hardware', onReset) + + state.test = Button('test network', onNetworkTest) + state.message = 'click above to test network' + state.route = '0,0' // default + + state.onChange('route', function(){ + hardware.route.route = state.route + }) + + function onReset(){ + var rstpck = new Array() + rstpck.push(128) + state.message = 'reset command issued' + hardware.route.send(rstpck) + } + + function onNetworkTest(){ + var tstpck = new Array() + tstpck.push(127) + state.message = 'test packet out' + hardware.route.send(tstpck) + } + + hardware.route.subscribe(127, testReturn) + + function testReturn(msg){ + state.message = 'test OK' + console.log('test returns with msg', msg) + } + + return hardware +} + +module.exports = Hardware \ No newline at end of file diff --git a/src/cartesian.js b/src/cartesian.js new file mode 100644 index 0000000000000000000000000000000000000000..c6255fedb01e0b9f73971aa493d300c1dd69a484 --- /dev/null +++ b/src/cartesian.js @@ -0,0 +1,28 @@ +function distance(p1, p2) { + // takes p1, p2 to be arrays of same length + // computes cartesian distance + var sum = 0 + for (var i = 0; i < p1.length; i++) { + sum += Math.pow((p1[i] - p2[i]), 2) + } + return Math.sqrt(sum) +} + +function length(v) { + // length of vector + var sum = 0 + for (var i = 0; i < v.length; i++) { + sum += Math.pow(v[i], 2) + } + return Math.sqrt(sum) +} + +function degrees(rad){ + return rad * (180 / Math.PI) +} + +module.exports = { + distance: distance, + length: length, + degrees: degrees +} \ No newline at end of file diff --git a/src/jsui.js b/src/jsui.js new file mode 100644 index 0000000000000000000000000000000000000000..46f78466e8770ca0f9aada2cafe82e66e75c1395 --- /dev/null +++ b/src/jsui.js @@ -0,0 +1,40 @@ +function UI() { + var ui = {} + + ui.addElement = function(keyName, srcPath, callback){ + var src = require(srcPath) + ui[keyName] = new src() + ui[keyName].callback = callback + } + + ui.init = function(parentModId, socket){ + // get hookups from top level program + this.parentId = parentModId + this.socket = socket + // and wrap state objects in getters / setters + for (key in this) { + if (isUiKey(key)) { + // see if they have init functions? + } + } + } + + return ui +} + +function isUiKey(key) { + if (key == 'parentId' || key == 'socket' || key == 'init' || key == 'addElement' || key == 'pushToUI' || key == 'emitters') { + return false + } else { + return true + } +} + +module.exports = { + UI: UI, + isUiKey: isUiKey +} + +/* bless u mdn +https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement +*/ \ No newline at end of file diff --git a/src/jsunit.js b/src/jsunit.js new file mode 100644 index 0000000000000000000000000000000000000000..90cbec4f5996afd90739b7fd2775eb51bf786e14 --- /dev/null +++ b/src/jsunit.js @@ -0,0 +1,151 @@ +// event system to include type-checking etc +// dataflow types for javascript objects ... + +function Input(type, fn) { + var input = { + accepts: type, + fn: fn + } + + return input +} + +function Output(type) { + var output = { + emits: type + } + + output.calls = new Array() + + output.attach = function(input) { + this.calls.push(input) + } + + output.isLinked = function(input) { + // return true if already hooked up + if (this.calls.includes(input)) { + return true + } else { + return false + } + } + + output.remove = function(input) { + if (!this.isLinked(input)) { + console.log('attempt to rm input that is not attached') + return false + } else { + this.calls.splice(this.calls.indexOf(input), 1) + } + } + + output.removeAllLinks = function() { + this.calls = [] + } + + output.checkLinks = function(id) { + console.log('checking links', this) + for (index in this.calls) { + if (this.calls[index].parentId == id) { + console.log('popping null entry from', this.calls) + this.calls.splice(index, 1) + console.log('new record', this.calls) + } else { + // all good + } + } + } + + output.emit = function(data) { + if (this.calls.length == 0) { + console.log('no inputs bound to this output') + } else { + for (index in this.calls) { + this.calls[index].fn(JSON.parse(JSON.stringify(data))) + } + } + } + + return output +} + +function State() { + var state = {} + + state.emitters = {} + state.parentId = null + state.socket = null + + // called when change from UI + state.onUIChange = function(item, fn) { + this.emitters[item] = fn + } + + state.emitUIChange = function(item) { + if (this.emitters[item] != null) { + this.emitters[item]() + } + } + + state.pushToUI = function(key) { + if (this.socket) { + var data = { + id: this.parentId, + key: key, + val: this[key] + } + this.socket.send('put state change', data) + } else { + console.log("ERR on state update to UI, socket is", this.socket) + } + } + + state.init = function(parentModId, socket) { + // get hookups from top level program + this.parentId = parentModId + this.socket = socket + // and wrap state objects in getters / setters + for (key in this) { + if (isStateKey(key)) { + this['_' + key] = this[key] + this[key] = {} + writeStateGetterSetter(this, key) + } + } + } + + return state +} + +function writeStateGetterSetter(state, key) { + Object.defineProperty(state, key, { + set: function(x) { + // update internal value + state['_' + key] = x + // console.log('SET', key, this['_' + key]) + // push to external view + state.pushToUI(key) + } + }) + Object.defineProperty(state, key, { + get: function() { + //console.log('GET', key, this['_' + key]) + return state['_' + key] + } + }) +} + +function isStateKey(key) { + if (key.indexOf('_') == 0 || key == 'parentId' || key == 'socket' || key == 'init' || key == 'pushToUI' || key == 'emitters' || key == 'onUIChange' || key == 'emitUIChange' ) { + return false + } else { + return true + } +} + +module.exports = { + Input: Input, + Output: Output, + State: State, + isStateKey: isStateKey +} \ No newline at end of file diff --git a/src/packets.js b/src/packets.js new file mode 100644 index 0000000000000000000000000000000000000000..ad63359112614e31333d930278a84e9f2ee26a45 --- /dev/null +++ b/src/packets.js @@ -0,0 +1,24 @@ +function pack32(val) { + var pack = new Array(); + pack[0] = (val >> 24) & 255; + pack[1] = (val >> 16) & 255; + pack[2] = (val >> 8) & 255; + pack[3] = val & 255; + + return pack; +} + +function unPack32(arr){ + if(arr.length == 4){ + var unPacked = arr[0] << 24 | arr[1] << 16 | arr[2] << 8 | arr[3] + return unPacked + } else { + console.log("ERR: arr > 4 at unPack32", arr) + } + +} + +module.exports = { + pack32: pack32, + unPack32: unPack32 +} \ No newline at end of file diff --git a/src/ui/uiButton.js b/src/ui/uiButton.js new file mode 100644 index 0000000000000000000000000000000000000000..60c9e2e9a4ba5ca5fd745b47ff1e251e6dfc7df5 --- /dev/null +++ b/src/ui/uiButton.js @@ -0,0 +1,11 @@ +function UIButton() { + var uiButton = { + type: 'button', + clientPath: 'ui/uiButton.js', + isPressed: false + } + + return uiButton +} + +module.exports = UIButton \ No newline at end of file diff --git a/views.js b/views.js index 32d1138a1c6072d6673738eed3b7d3cea20e223a..d77ea9dcd0b7730ee75de6ad00ded0c840be619a 100644 --- a/views.js +++ b/views.js @@ -32,6 +32,11 @@ function startHttp() { res.sendFile(__dirname + '/client/' + req.params.file) }) + app.get('/ui/:file', (req, res) => { + console.log('client req ui', req.params.file) + res.sendFile(__dirname + '/client/ui/' + req.params.file) + }) + // through this window http.listen(8080, () => { console.log('RNDMC is listening on localhost:8080') @@ -100,8 +105,8 @@ function socketRecv(evt) { uiRequestNewModule(data) break case 'remove module': - uiRequestRemoveModule(data) - break + uiRequestRemoveModule(data) + break case 'put state change': uiRequestStateChange(data) break @@ -227,19 +232,19 @@ function uiRequestNewModule(data) { // bit of a mess to pick out the last entered module var keys = Object.keys(program.modules) var latest = keys[keys.length - 1] - if(program.modules[latest].description.isLink && data != './modules/hardware/atkseriallink.js'){ + if (program.modules[latest].description.isLink && data != './modules/hardware/atkseriallink.js') { // we just added hardware, so added a link, so we've added two // just burn it down socketSend('restart', '') } // TODO: questionable init - here and in loadProgram, should be better handled across board - if(program.modules[latest].init != null){ + if (program.modules[latest].init != null) { program.modules[latest].init() } socketSend('put module', Reps.makeFromModule(program.modules[keys[keys.length - 1]])) } -function uiRequestRemoveModule(data){ +function uiRequestRemoveModule(data) { console.log('UI REQUEST TO REMOVE MODULE', data.id) Programs.removeModule(program, data.id) socketSend('restart', '') @@ -250,22 +255,14 @@ function uiRequestStateChange(data) { var mdlState = program.modules[data.id].state - if (mdlState[data.key]) { - switch (mdlState[data.key].type) { - case 'button': - mdlState[data.key].onClick() - break - case 'multiline': - mdlState[data.key].value = data.val - mdlState.emitChange(data.key) - break - default: - mdlState[data.key] = data.val - mdlState.emitChange(data.key) - break - } + if (mdlState[data.key] != null) { + mdlState[data.key] = data.val + mdlState.emitUIChange(data.key) } else { - console.log("ERR no state key,", key, "found here", data) + console.log("ERR no state key,", data.key, "found here", data) + console.log("THE KEY", data.key) + console.log("THE MDL", mdlState) + console.log("THE STATE", mdlState[data.key]) } } @@ -280,7 +277,7 @@ function uiRequestLinkChange(data) { var toMdl = program.modules[toId] // if it's already attached, we rm - if(fromMdl.outputs[outputName].isLinked(toMdl.inputs[inputName])){ + if (fromMdl.outputs[outputName].isLinked(toMdl.inputs[inputName])) { fromMdl.outputs[outputName].remove(toMdl.inputs[inputName]) } else { fromMdl.outputs[outputName].attach(toMdl.inputs[inputName])