diff --git a/canvas.js b/canvas.js index bdbc6d5e23fa94dcfe389bede2d0e74e8c8f1a72..14b1a0af440550af2d91b60cf4b03703edb246f0 100644 --- a/canvas.js +++ b/canvas.js @@ -6,7 +6,6 @@ const Programs = require('./programs.js') // the program object: real simple, just has a description, and a 'modules' var program = Programs.new('new program') - var canvas = Programs.loadModuleFromSource(program, './modules/ui/threeCanvas.js') Programs.setUI(canvas, 200, 200) diff --git a/littlerascal.js b/littlerascal.js new file mode 100644 index 0000000000000000000000000000000000000000..769634c0951e483ea0e62da2b7675b2509dd71ee --- /dev/null +++ b/littlerascal.js @@ -0,0 +1,148 @@ +// business +const Reps = require('./reps.js') +const Programs = require('./programs.js') + +// the program object: real simple, just has a description, and a 'modules' +var program = Programs.new('new program') + +/* ok +- new plotter +- ack four +- go to pos block +- neil:mm adjust +- git push +- add y axis network wait ? +*/ + +// das link +var link = Programs.loadModuleFromSource(program, './modules/hardware/atkseriallink.js') +link.startUp() +link.state.log = true + +let steps = 10 + +// x motor +var xm = Programs.loadModuleFromSource(program, './modules/hardware/atkstepper.js') +xm.route.route = '0,0' +xm.state.route = '0,0' +xm.state.spu = steps +xm.state.axis = 'X' + +// y left and right +var ylm = Programs.loadModuleFromSource(program, './modules/hardware/atkstepper.js') +ylm.route.route = '0,1' +ylm.state.route = '0,1' +ylm.state.spu = steps +ylm.state.axis = 'Y' + +var ylr = Programs.loadModuleFromSource(program, './modules/hardware/atkstepper.js') +ylr.route.route = '0,2' +ylr.state.route = '0,2' +ylr.state.spu = -steps +ylr.state.axis = 'Y' + +// z motor +var zm = Programs.loadModuleFromSource(program, './modules/hardware/atkstepper.js') +zm.route.route = '0,3' +zm.state.route = '0,3' +zm.state.spu = -steps +zm.state.axis = 'Z' + +var planner = Programs.loadModuleFromSource(program, './modules/motion/simplanner.js') +planner.state.minSpeed = 1 +planner.state.accel = 300 +planner.netWindow = 1 + +/* + +var simplanner = Programs.loadModuelFromSource(program, './modules/motion/simplanner.js') + +let ahMove = { + axes: ['X', 'Y', 'Z'], + p1: [10, 10, 10], + p2: [20, 20, 20], + vector: [10, 10, 10], + cruise: 100, + entry: 10, + exit: 10, + accel: 100 +} + +*/ + +// what doth planner output ? +/* +var newMove = { + axes: axes, + p1: p1, + p2: p2, + cruise: move.speed, + entry: 0, + exit: 0, + accel: state.accel +} +*/ + +// can we write something that just steps through moves one by one ? + +// HOOKUP +planner.outputs.moves.attach(xm.inputs.trapezoid) +planner.outputs.moves.attach(ylm.inputs.trapezoid) +planner.outputs.moves.attach(ylr.inputs.trapezoid) +planner.outputs.moves.attach(zm.inputs.trapezoid) +xm.outputs.ack.attach(planner.inputs.acks) +ylm.outputs.ack.attach(planner.inputs.acks) +zm.outputs.ack.attach(planner.inputs.acks) + +let moveToPlanner = { + position: { + X: 20, + Y: 10, + Z: 5 + }, + speed: 150 +} + +link.onOpen = function(){ + // planner.inputs.instruction.fn(moveToPlanner) + //xm.inputs.trapezoid.fn(ahMove) +} + +// ws input + +var sockit = Programs.loadModuleFromSource(program, './modules/pipes/websocket.js') +var np = Programs.loadModuleFromSource(program, './modules/motion/neilparse.js') + +sockit.outputs.data.attach(np.inputs.array) +np.outputs.move.attach(planner.inputs.instruction) +planner.outputs.moveComplete.attach(np.inputs.shift) + +// UI HOOKUP + +Programs.setView(program, { + scale: 0.5, + translate: [-160, -30], + origin: [200, 120] +}) + +let top = 100 +let motorbar = 1200 +let motorspace = 400 + +Programs.setUI(sockit, top, top) +Programs.setUI(np, 600, top) + +Programs.setUI(link, 1800, top) +Programs.setUI(xm, motorbar, top) +Programs.setUI(ylm, motorbar, top + motorspace) +Programs.setUI(ylr, motorbar, top + 2 * motorspace) +Programs.setUI(zm, motorbar, top + 3 * motorspace) +Programs.setUI(planner, 600, 450) + +// UI +const View = require('./views.js') +View.startHttp() +View.startWs() + +Programs.assignSocket(View.uiSocket) +View.assignProgram(program) \ No newline at end of file diff --git a/modules/hardware/atkseriallink.js b/modules/hardware/atkseriallink.js index 6427385b7d289f0c4ad08ca99d7934de95cc2009..49cfc2811eaaa099204c38958dec14c878be391a 100644 --- a/modules/hardware/atkseriallink.js +++ b/modules/hardware/atkseriallink.js @@ -94,6 +94,7 @@ function ATKSerialLink() { //state.portStatus = 'opening' serialport.on('open', function() { state.portStatus = 'open' + atkSerialLink.onOpen() }) serialport.on('error', function(err) { state.portStatus = err.message @@ -103,6 +104,10 @@ function ATKSerialLink() { } } + atkSerialLink.onOpen = function() { + // null 4 now ... + } + /* ------------------------------------------------------ PACKETS TO HARDWARE diff --git a/modules/hardware/atkstepper.js b/modules/hardware/atkstepper.js index 22e86c5efe64d249a8f6482bae34651fdd215420..09636182e4f0180560ee3ee3fbdaaa5b641e5288 100644 --- a/modules/hardware/atkstepper.js +++ b/modules/hardware/atkstepper.js @@ -47,7 +47,7 @@ function Stepper() { }) state.axis = 'X' - state.spu = 200 // steps per unit + state.spu = 80 // steps per unit state.rawMove = -10 var ui = stepper.ui @@ -191,7 +191,7 @@ function Stepper() { console.log(dMove) } - if(verbose) console.log('------------------- DMOVE', state.axis, dMove.steps) + if(true) console.log('------------------- DMOVE', state.axis, dMove) var packet = new Array() // step blocks are #131 diff --git a/modules/motion/neilparse.js b/modules/motion/neilparse.js new file mode 100644 index 0000000000000000000000000000000000000000..0e3157489b9016dead282ac09d8672e85feb4a1f --- /dev/null +++ b/modules/motion/neilparse.js @@ -0,0 +1,132 @@ +// boilerplate atkapi header +const JSUnit = require('../../src/jsunit.js') + +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State + +// unit to convert neil's mods-output arrays into human readable move objects +// probably do flow control here also ? + +function NeilParser() { + var neilParser = { + description: { + name: 'neilParser Parser', + alt: 'line of neilParser -> points' + } + } + + // log more + var verbose = false + + let npList = null + + // one caveat here is that we can't dynamically add objects to this, + // or they don't get getter / settered when we do + neilParser.state = State() + var state = neilParser.state + state.lift = 50 + state.moveSpeed = 200 + state.plungeSpeed = 50 + state.jogSpeed = 600 + state.listLength = 0 + + neilParser.inputs = { + array: Input('array', (arr) => { + let list = writeMoveObjects(arr) + console.log('have list', list) + npList = list + state.listLength = npList.length + // throw it out + neilParser.outputs.newList.emit(1) + onShift() + + }), + shift: Input('event', (arg) => { + onShift() + }) + } + + neilParser.outputs = { + move: Output('move'), + newList: Output('event') + } + + function writeMoveObjects(arr) { + // arr[i] are segments, + // between arr[i] instances, we put a z-lift and then + // a z-drop + + if (Array.isArray(arr)) { + let flat = new Array() + for (var i = 0; i < arr.length; i++) { + flat.push(arr[i][0]) + flat.push('z down move') + flat = flat.concat(arr[i].slice(1)) + flat.push('z up move') + } + // console.log('FLATTENED TO', flat) + // add header + flat.splice(0, 0, 'z up move') + // and return home + flat.push([0, 0]) + // make a list of objs + let list = new Array() + // this is actually stateful ... jogging or not + let upState = false + for (var j = 0; j < flat.length; j++) { + if (Array.isArray(flat[j])) { + let move = { + position: { + X: flat[j][0], + Y: flat[j][1] + } + } + if (upState) { + move.speed = state.jogSpeed + } else { + move.speed = state.moveSpeed + } + list.push(move) + } else if (flat[j] === 'z up move') { + upState = true + list.push({ + position: { + Z: state.lift + }, + speed: state.plungeSpeed + }) + } else if (flat[j] === 'z down move') { + upState = false + list.push({ + position: { + Z: 0 + }, + speed: state.plungeSpeed + }) + } + } + + return list + + } else { + console.log('NEIL PARSE: err: not an array') + } + + } + + function onShift() { + if (npList != null && npList.length > 0) { + neilParser.outputs.move.emit(npList.shift()) + state.listLength = npList.length + } else { + console.log('NP Completes Moves') + } + } + + return neilParser +} + + +// export the module +module.exports = NeilParser \ No newline at end of file diff --git a/modules/motion/simplanner.js b/modules/motion/simplanner.js new file mode 100644 index 0000000000000000000000000000000000000000..05cb5f8cd69edf976095ca24e4649b9c9d880d3c --- /dev/null +++ b/modules/motion/simplanner.js @@ -0,0 +1,374 @@ +// boilerplate atkapi header +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 + +// descartes, to you +const DCRT = require('../../src/cartesian.js') +const MJS = require('mathjs') + +// planner consumes target moves (i.e. segments having uniform speed throughout) +// and subjects them to acceleration constraints + +function Planner() { + var planner = { + description: { + name: 'Lookahead-Motion-Planner', + alt: 'movements -> acceleration planned moves' + } + } + + // log more + var verbose = false + + planner.state = State() + var state = planner.state // reference pass attempt? + + state.axisIDs = 'X,Y,Z' + state.onUiChange('axisIDs', axisIDUpdate) + + state.accel = 200 // units/s/s + state.minSpeed = 1 // units/s + + state.position = [0, 0, 0] + + // should be grn / red button ... + state.isRunning = 1 + state.onUiChange('isRunning', netStateRefresh) + + state.netWindow = 1 + state.netState = [0, 0, 0] + + planner.ui = UI() + var ui = planner.ui + + ui.addElement('startStopButton', 'ui/uiButton.js') + ui.startStopButton.subscribe('onload', function(msg) { + ui.startStopButton.send({ + calls: 'setText', + argument: 'start / stop planner' + }) + }) + ui.startStopButton.subscribe('onclick', onStartStop) + + planner.inputs = { + instruction: Input('move instruction', onNewInstruction), + acks: Input('move acknowledgement', onAck), + run: Input('boolean', onRunInstruction) + } + + planner.outputs = { + moves: Output('move instruction'), + moveComplete: Output('number') + } + + // we'll use one of these to assert / do things + // after the module is loaded, and state is copied etc + // i.e. one thing we can do is assert a starting value + planner.init = function() { + state.isRunning = 0 + state.netState = [0, 0, 0] + } + + /* + ------------------------------------------------------ + UPDATING / SETUP + ------------------------------------------------------ + */ + + function onAck(msg) { + //console.log("Planner onAck:", msg) + // update position, net states, run netCheck + var axes = state.axisIDs.split(',') + var match = axes.indexOf(msg.axis) + if (match !== -1) { + // HERE is a hack, for our state-updating system doesn't yet accomodate arrays + var newPos = state.position.slice(0) + newPos[match] += msg.increment + state.position = newPos + var newNetState = state.netState.slice(0) + newNetState[match] -= 1 + state.netState = newNetState + if (verbose) console.log('NEW NET STATE', newNetState) + } else { + console.log('ERR in PLANNER: missed axis on ack from stepper') + } + netStateRefresh() + } + + function netStateRefresh() { + // check if received all four, + // send a packet + var ns = state.netState + var i = 0 + // check equality + while (ns[i] == ns[i + 1]) { + i++ + // console.log('EQUALITY LOOP') + if (i > ns.length - 1) { + break + } + } + if (i == ns.length - 1) { + console.log('Planner confirms Move Complete') + planner.outputs.moveComplete.emit(1) + } + } + + function onRunInstruction(boolean) { + if (boolean) { + if (state.isRunning) { + // already running, do nothing + } else { + state.isRunning = 1 + netStateRefresh() + } + } else { + if (state.isRunning) { + state.isRunning = 0 + } else { + // oy + } + } + } + + function axisIDUpdate() { + var axes = state.axisIDs.split(',') + var position = new Array(axes.length) + position.fill(0) + var packetState = new Array(axes.length) + for (i in axes) { + position.push(0) + packetState.push(0) + // could do + //planner.outputs[axes[i]] = Output('move instruction') + } + state.position = position + state.packetState = packetState + console.log(planner) + } + + function onStartStop() { + if (state.isRunning) { + state.isRunning = 0 + } else { + state.isRunning = 1 + netStateRefresh() + } + } + + function sendMoveToNetwork(move) { + for (i in state.netState) { + state.netState[i]++ + } + state.netState = state.netState + planner.outputs.moves.emit(move) + } + + /* + ------------------------------------------------------ + ENTRY POINTS + ------------------------------------------------------ + */ + + function onNewInstruction(move) { + // our axis + var axes = state.axisIDs.split(',') + + // we'll make a new move object + // start and end points, and axes to track + var p1 = [] + var p2 = new Array(axes.length) + p2.fill(0) + // start point is current pos ... one at a time in simplanner + p1 = state.position + // now pick out new deltas + for (i in axes) { + var key = axes[i] + if (move.position[key] != null) { + // some new pos, + p2[i] = move.position[key] + } else { + // or none, this axis stays + p2[i] = p1[i] + } + } + // check for zero-length vector + if (MJS.distance(p1, p2) == 0) { + console.log('------------------ !ACHTUNG! -------------------') + console.log('------------------ !ACHTUNG! -------------------') + console.log('planner throwing zero length vector') + zlcounter++ + console.log(zlcounter) + // this means we need another one from the queue + planner.outputs.moveComplete.emit(1) + } else { + // starting with basics + let mv = { + axes: axes, + p1: p1, + p2: p2, + cruise: move.speed, + entry: state.minSpeed, + exit: state.minSpeed, + accel: state.accel + } + // run it once + runSimpleAccel(mv) + sendMoveToNetwork(mv) + } + } + + /* + ------------------------------------------------------ + JUNCTION DEVIATION + walk mq to generate permissible entry / exit speeds + by 'junction deviation' algorithm + ------------------------------------------------------ + */ + + function runSimpleAccel(move) { + // useful + var accel = state.accel + var ms = state.minSpeed + // should reference axis ids, not these moves ? + var numAxis = move.p1.length + + // have entry and exit speeds already because we're simplanner + // now that we know permissible cornering speeds, + // we'll calculate the trapezoid shapes + // this becomes very useful for turning moves over to stepper motors + // and will give us a time estimate for each move as well, + // we need / want that so that we can set a network buffer length + calcTrap(move, accel, false) + let time = moveTime(move) + if (true) console.log('PLANNER: trapezoid time to', move.p2, time) + if (time < 0.1) { + console.log('------------------ !ACHTUNG! -------------------') + console.log('------------------ !ACHTUNG! -------------------') + console.log("WARN! move time here", time) + } + } + /* + ------------------------------------------------------ + TRAPEZOIDS FROM JUNCTION DEVIATION ENTRY / EXITS + ------------------------------------------------------ + */ + + function calcTrap(move, a, debug) { + // have p1, p2, cruise, entry, exit + // add length, figure if full-accel, full-deccel, triangle, trapezoid, cruise ... + move.vector = MJS.subtract(move.p2, move.p1) + var d = DCRT.length(move.vector) + var vi = move.entry + var v = move.cruise + var vf = move.exit + // limits + var maxExit = Math.sqrt(Math.pow(vi, 2) + 2 * a * d) + var maxEntry = Math.sqrt(Math.pow(vf, 2) + 2 * a * d) + // seven possible cases + // HERE set move with type, accel / deccel lengths, calculate time for each, and total time + // then get on with writing a larger gcode window, bringing in some 'real' moves, and getting these + // out / back from steppers + if (d <= 0) { + console.log('ZERO LENGTH MOVE') + } else if (maxExit <= vf) { + if (debug) console.log('TRAP: full ramp up'); + move.type = 'ramp up' + // full ramp up + // accel, cruise, and deccel time + move.t1 = (vf - vi) / a + move.d1 = d + move.t2 = 0 + move.d2 = 0 + move.t3 = 0 + move.d3 = 0 + // check we won't break + if (maxExit < vf) console.log('max ramp up, misses by', vf - maxExit) + } else if (maxEntry <= vi) { + if (debug) console.log('TRAP: full ramp down'); + move.type = 'ramp down' + move.t1 = 0 + move.d1 = 0 + move.t2 = 0 + move.d2 = 0 + move.t3 = (vi - vf) / a + move.d3 = d + // check we're not going to break / not make + if (maxEntry < vi) console.log('max ramp down, misses by', vi - maxEntry) + } else if (vi == v && vf == v) { + if (debug) console.log('TRAP: full cruise'); + move.type = 'cruise' + move.t1 = 0 + move.d1 = 0 + move.t2 = d / v + move.d2 = d + move.t3 = 0 + move.d3 = 0 + } else if (vi == v) { + if (debug) console.log('TRAP: cruise, decelerate'); + move.type = 'cruise and deccel' + var deccelDistance = (Math.pow(v, 2) - Math.pow(vf, 2)) / (2 * a) + move.t1 = 0 + move.d1 = 0 + move.t2 = (d - deccelDistance) / v + move.d2 = d - deccelDistance + move.t3 = (v - vf) / a + move.d3 = deccelDistance + } else if (vf == v) { + if (debug) console.log('TRAP: accel, cruise'); + move.type = 'accel and cruise' + var accelDistance = (Math.pow(v, 2) - Math.pow(vi, 2)) / (2 * a) + move.t1 = (v - vi) / a + move.d1 = accelDistance + move.t2 = (d - accelDistance) / v + move.d2 = d - accelDistance + move.t3 = 0 + move.d3 = 0 + } else { + // either full trapezoid, or a triangle + // distances to / from cruise from / to start / end + var accelDistance = (Math.pow(v, 2) - Math.pow(vi, 2)) / (2 * a) + var deccelDistance = (Math.pow(v, 2) - Math.pow(vf, 2)) / (2 * a) + if (accelDistance + deccelDistance < d) { + if (debug) console.log('TRAP: complete trapezoid'); + move.type = 'complete trapezoid' + move.t1 = (v - vi) / a + move.d1 = accelDistance + move.t2 = (d - accelDistance - deccelDistance) / v + move.d2 = d - accelDistance - deccelDistance + move.t3 = (v - vf) / a + move.d3 = deccelDistance + } else { + if (debug) console.log('TRAP: triangular'); + // find the peak + var vPeak = Math.sqrt(((2 * a * d + Math.pow(vi, 2) + Math.pow(vf, 2)) / 2)) + var midPoint = (Math.pow(vPeak, 2) - Math.pow(vi, 2)) / (2 * a) + move.type = 'triangular' + move.t1 = (v - vi) / a + move.d1 = midPoint + move.t2 = 0 + move.d2 = 0 + move.t3 = (v - vf) / a + move.d3 = d - midPoint + } + } + } + + function moveTime(move) { + if (move.t1 != null) { + return move.t1 + move.t2 + move.t3 + } else { + console.log('move with no time') + } + } + + return planner +} + +module.exports = Planner \ No newline at end of file diff --git a/modules/pipes/websocket.js b/modules/pipes/websocket.js new file mode 100644 index 0000000000000000000000000000000000000000..420965b7973704e199642088838670f17e737d2b --- /dev/null +++ b/modules/pipes/websocket.js @@ -0,0 +1,60 @@ +// boilerplate atkapi header +const JSUnit = require('../../src/jsunit.js') + +let Input = JSUnit.Input +let Output = JSUnit.Output +let State = JSUnit.State + +// websocket, to share program representations with the client (and back) +const ws = require('ws') + +function WebSocket() { + var webSocket = { + description: { + name: 'webSocket', + alt: 'webseket' + } + } + + // log more + var verbose = false + + let sckt = null + + // one caveat here is that we can't dynamically add objects to this, + // or they don't get getter / settered when we do + webSocket.state = State() + var state = webSocket.state + state.address = '127.0.0.1' + state.port = 1234 + state.status = 'closed' + + webSocket.inputs = { + in: Input('any', (thing) => { + // some fn + }) + } + + webSocket.outputs = { + data: Output('any') + } + + function openSocket(){ + const wss = new ws.Server({port: state.port}) + wss.on('connection', (pipe) => { + sckt = pipe + console.log('WebSocket Opened') + sckt.on('message', (msg) => { + webSocket.outputs.data.emit(JSON.parse(msg)) + }) + }) + } + + openSocket() + + return webSocket +} + + +// export the module +module.exports = WebSocket \ No newline at end of file diff --git a/rundmc.js b/rundmc.js index 189ac6897a24d3c8942e73d82cab851d5b5af59b..4d75709a22de0b497dbc906118e7c49c72dec3b2 100644 --- a/rundmc.js +++ b/rundmc.js @@ -29,9 +29,9 @@ const Reps = require('./reps.js') const Programs = require('./programs.js') // the program object: real simple, just has a description, and a 'modules' -// var program = Programs.new('new program') +var program = Programs.new('new program') -var program = Programs.open('./programs/temp.json') +//var program = Programs.open('./programs/temp.json') /* var stest = Programs.loadModuleFromSource(program, './modules/ui/stest.js') diff --git a/src/atkunit.js b/src/atkunit.js index 2b328d5ff14b16df3866f96a3373885ae03715d8..5a673aa9bb680f8b6548362adad058b12ac5ab61 100644 --- a/src/atkunit.js +++ b/src/atkunit.js @@ -39,6 +39,7 @@ function Hardware() { argument: 'reset hardware' }) }) + ui.resetButton.subscribe('onclick', onReset) ui.addElement('testButton', 'ui/uiButton.js') ui.testButton.subscribe('onload', function(msg) { @@ -47,8 +48,10 @@ function Hardware() { argument: 'test network' }) }) + ui.testButton.subscribe('onclick', onNetworkTest) state.onUiChange('route', function() { + console.log('logging state / route change') hardware.route.route = state.route }) diff --git a/views.js b/views.js index 43b447d01de379b71187c70368c5fda0366a6f56..fb211bf15932815963b1276999ba489bc173f60e 100644 --- a/views.js +++ b/views.js @@ -141,7 +141,7 @@ function socketSend(type, data) { //console.log('SEND', msg) sckt.send(JSON.stringify(msg)) } else { - console.log('on socketSend, ws not ready') + // console.log('on socketSend, ws not ready') } } @@ -306,7 +306,7 @@ function uiRequestUiChange(data){ } function uiRequestMdlPositionChange(data) { - if(verbose) console.log('UI REQUEST ADD / CHANGE UI INFO TO MODULE', data) + if(true) console.log('UI REQUEST ADD / CHANGE UI INFO TO MODULE', data) var mod = program.modules[data.description.id] mod.description.position = data.description.position }