Commit b0218025 authored by Jake Read's avatar Jake Read
Browse files

add js controller

parent 20f6b453
......@@ -15,4 +15,6 @@ jake/
**/ASF/
**.3dmbak
\ No newline at end of file
**.3dmbak
node_modules/
## Known Bugs
- the remote 'set-currents' command seems to drop Z current levels beneath stall, other motors appear OK, not sure. large currents cause trouble, perhaps not enough bypass caps.
- I believe this is a bug on the b-channel transmit, which was never debugged: likely in byte-delineation
\ No newline at end of file
......@@ -19,16 +19,10 @@ union chunk_float64 {
double f;
};
// C_SCALE
// 1: DACs go 0->512 (of 4096, peak current is 1.6A at 4096): 0.2A
// 2: DACs go 0->1024,
// ...
// 8: DACs go full width
#define BUS_DROP 1 // Z: 1, YL: 2, X: 3, YR: 4
#define AXIS_PICK 2 // Z: 2, Y: 1, X: 0
#define AXIS_INVERT false // Z: false, YL: true, YR: false, X: false
#define SPU 3200.0F // always posiive! Z: 3200, XY: 400
#define SPU 3200.0F // always positive! Z: 3200, XY: 400
#define C_SCALE 0.2F // 0-1, floating: initial holding current to motor, 0-2.5A
#define TICKS_PER_PACKET 20.0F // always 20.0F
......@@ -76,6 +70,12 @@ void loop() {
currentChunks[2] = { .bytes = { bChPck[ptr ++], bChPck[ptr ++], bChPck[ptr ++], bChPck[ptr ++] } };
// set current
stepper_hw->setCurrent(currentChunks[AXIS_PICK].f);
// bug here
/*
I noticed that setcurrent commands to the z motor seem to fail: the motor drops to a low current no matter the value transmitted
I've checked (by inserting a flag to pick y-values) here, and those values work well, so it's nothing with the motor driver itself
so, I suspect the b-channel bus packets are coming out of those structures one byte short - makes sense there would be some bug in/around delineation
*/
break;
}
case AK_SETPOS: {
......
/*
clank controller init
serves client modules, bootstraps local scripts from client ui.
Jake Read at the Center for Bits and Atoms
(c) Massachusetts Institute of Technology 2020
This work may be reproduced, modified, distributed, performed, and
displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
Copyright is retained and must be preserved. The work is provided as is;
no warranty is provided, and users accept all liability.
*/
// new year new bootstrap
const express = require('express')
const app = express()
// this include lets us read data out of put requests,
const bodyparser = require('body-parser')
// our fs tools,
const fs = require('fs')
//const filesys = require('./filesys.js')
// and we occasionally spawn local pipes (workers)
const child_process = require('child_process')
// will use these to figure where tf we are
let ownIp = ''
const os = require('os')
// serve everything: https://expressjs.com/en/resources/middleware/serve-static.html
app.use(express.static(__dirname))
// accept post bodies as json,
app.use(bodyparser.json())
app.use(bodyparser.urlencoded({extended: true}))
// redirect traffic to /client,
app.get('/', (req, res) => {
res.redirect('/client')
})
// we also want to institute some pipes: this is a holdover for a better system
// more akin to nautilus, where server-side graphs are manipulated
// for now, we just want to dive down to a usb port, probably, so this shallow link is OK
let processes = []
app.get('/startLocal/:file', (req, res) => {
// launches another node instance at this file w/ these args,
let args = ''
if(req.query.args){
args = req.query.args.split(',')
}
console.log(`attempt to start ${req.params.file} with args ${args}`)
// startup, let's spawn,
const process = child_process.spawn('node', ['-r', 'esm', `local/${req.params.file}`])
// add our own tag,
process.fileName = req.params.file
let replied = false
let pack = ''
process.stdout.on('data', (buf) => {
// these emerge as buffers,
let msg = buf.toString()
// can only reply once to xhr req
if(msg.includes('OSAP-wss-addr:') && !replied){
res.send(msg)
replied = true
}
// ok, dealing w/ newlines
pack = pack.concat(msg)
let index = pack.indexOf('\n')
while(index >= 0){
console.log(`${process.fileName} ${process.pid}: ${pack.substring(0, index)}`)
pack = pack.slice(index + 1)
index = pack.indexOf('\n')
}
})
process.stderr.on('data', (err) => {
if(!replied){
res.send('err in local script')
replied = true
}
console.log(`${process.fileName} ${process.pid} err:`, err.toString())
})
process.on('close', (code) => {
console.log(`${process.fileName} ${process.pid} closes:`, code)
if(!replied){
res.send('local process closed')
replied = true
}
})
console.log(`started ${process.fileName} w/ pid ${process.pid}`)
})
// finally, we tell the express server to listen here:
let port = 8080
app.listen(port)
// once we're listening, report our IP:
let ifaces = os.networkInterfaces()
// this just logs the processes IP's to the termina
Object.keys(ifaces).forEach(function(ifname) {
var alias = 0;
ifaces[ifname].forEach(function(iface) {
if ('IPv4' !== iface.family){//} || iface.internal !== false) {
// skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
return;
}
ownIp = iface.address
if (alias >= 1) {
console.log('clank-tool available on: \t' /*ifname + ':' + alias,*/ + iface.address + `:${port}`);
// this single interface has multiple ipv4 addresses
// console.log('serving at: ' ifname + ':' + alias + iface.address + `:${port}`);
} else {
console.log('clank-tool available on:\t' /*ifname + ':' + alias,*/ + iface.address + `:${port}`);
// this interface has only one ipv4 adress
//console.log(ifname, iface.address);
}
++alias;
});
});
/*
domtools.js
osap tool drawing utility
Jake Read at the Center for Bits and Atoms
(c) Massachusetts Institute of Technology 2020
This work may be reproduced, modified, distributed, performed, and
displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
Copyright is retained and must be preserved. The work is provided as is;
no warranty is provided, and users accept all liability.
*/
// -------------------------------------------------------- TRANSFORM
// move things,
let writeTransform = (div, tf) => {
//console.log('vname, div, tf', view.name, div, tf)
if (tf.s) {
div.style.transform = `scale(${tf.s})`
} else {
div.style.transform = `scale(1)`
}
//div.style.transformOrigin = `${tf.ox}px ${tf.oy}px`
div.style.left = `${parseInt(tf.x)}px`
div.style.top = `${parseInt(tf.y)}px`
}
// a utility to do the same, for the background, for *the illusion of movement*,
// as a note: something is wrongo with this, background doth not zoom at the same rate...
let writeBackgroundTransform = (div, tf) => {
div.style.backgroundSize = `${tf.s * 10}px ${tf.s * 10}px`
div.style.backgroundPosition = `${tf.x + 50*(1-tf.s)}px ${tf.y + 50*(1-tf.s)}px`
}
// a uility to read those transforms out of elements,
// herein lays ancient mods code, probably a better implementation exists
let readTransform = (div) => {
// transform, for scale
let transform = div.style.transform
let index = transform.indexOf('scale')
let left = transform.indexOf('(', index)
let right = transform.indexOf(')', index)
let s = parseFloat(transform.slice(left + 1, right))
// left and right, position
let x = parseFloat(div.style.left)
let y = parseFloat(div.style.top)
return ({
s: s,
x: x,
y: y
})
}
// -------------------------------------------------------- DRAG Attach / Detach
let dragTool = (dragHandler, upHandler) => {
let onUp = (evt) => {
if (upHandler) upHandler(evt)
window.removeEventListener('mousemove', dragHandler)
window.removeEventListener('mouseup', onUp)
}
window.addEventListener('mousemove', dragHandler)
window.addEventListener('mouseup', onUp)
}
// -------------------------------------------------------- SVG
// return in an absolute-positioned wrapper at ax, ay, with dx / dy endpoint
let svgLine = (ax, ay, dx, dy, width) => {
let cont = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
$(cont).addClass('svgcont').css('left', ax).css('top', ay)
let path = document.createElementNS('http://www.w3.org/2000/svg', 'line')
$(cont).append(path)
path.style.stroke = '#1a1a1a'
path.style.fill = 'none'
path.style.strokeWidth = `${width}px`
path.setAttribute('x1', 0)
path.setAttribute('y1', 0)
path.setAttribute('x2', dx)
path.setAttribute('y2', dy)
return cont
}
export default {
writeTransform,
writeBackgroundTransform,
readTransform,
dragTool,
svgLine
}
/*
gridsquid.js
osap tool drawing set
Jake Read at the Center for Bits and Atoms
(c) Massachusetts Institute of Technology 2020
This work may be reproduced, modified, distributed, performed, and
displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
Copyright is retained and must be preserved. The work is provided as is;
no warranty is provided, and users accept all liability.
*/
'use strict'
import dt from './domtools.js'
import * as dat from '../libs/dat.gui.module.js'
import { TS, PK, DK, AK, EP } from '../../core/ts.js'
export default function GRIDSQUID(osap, xPlace, yPlace) {
// ------------------------------------------------------ PLANE / ZOOM / PAN
let plane = $('<div>').addClass('plane').get(0)
let wrapper = $('#wrapper').get(0)
// odd, but w/o this, scaling the plane & the background together introduces some numerical errs,
// probably because the DOM is scaling a zero-size plane, or somesuch.
$(plane).css('background', 'url("/client/drawing/bg.png")').css('width', '100px').css('height', '100px')
let cs = 1 // current scale,
let dft = { s: cs, x: 0, y: 0, ox: 0, oy: 0 } // default transform
// zoom on wheel
wrapper.addEventListener('wheel', (evt) => {
if($(evt.target).is('input, textarea')) return
evt.preventDefault()
evt.stopPropagation()
let ox = evt.clientX
let oy = evt.clientY
let ds
if (evt.deltaY > 0) {
ds = 0.025
} else {
ds = -0.025
}
let ct = dt.readTransform(plane)
ct.s *= 1 + ds
ct.x += (ct.x - ox) * ds
ct.y += (ct.y - oy) * ds
// max zoom pls thx
if (ct.s > 1.5) ct.s = 1.5
if (ct.s < 0.05) ct.s = 0.05
cs = ct.s
dt.writeTransform(plane, ct)
dt.writeBackgroundTransform(wrapper, ct)
})
// pan on drag,
wrapper.addEventListener('mousedown', (evt) => {
if($(evt.target).is('input, textarea')) return
evt.preventDefault()
evt.stopPropagation()
dt.dragTool((drag) => {
drag.preventDefault()
drag.stopPropagation()
let ct = dt.readTransform(plane)
ct.x += drag.movementX
ct.y += drag.movementY
dt.writeTransform(plane, ct)
dt.writeBackgroundTransform(wrapper, ct)
})
})
// init w/ defaults,
dt.writeTransform(plane, dft)
dt.writeBackgroundTransform(wrapper, dft)
$(wrapper).append(plane)
// ------------------------------------------------------ RENDER / RERENDER
// all nodes render into the plane, for now into the wrapper
// once ready to render, heights etc should be set,
let renderNode = (node) => {
let nel = $(`<div>`).addClass('node').get(0) // node class is position:absolute
nel.style.width = `${parseInt(node.width)}px`
nel.style.height = `${parseInt(node.height)}px`
nel.style.left = `${parseInt(node.pos.x)}px`
nel.style.top = `${parseInt(node.pos.y)}px`
$(nel).append($(`<div>${node.name}</div>`).addClass('nodename'))
if (node.el) $(node.el).remove()
node.el = nel
$(plane).append(node.el)
}
let renderVPort = (vPort, outgoing) => {
let nel = $('<div>').addClass('vPort').get(0)
nel.style.width = `${parseInt(vPort.parent.width) - 4}px`
let ph = perPortHeight - heightPadding
nel.style.height = `${parseInt(ph)}px`
nel.style.left = `${parseInt(vPort.parent.pos.x)}px`
let ptop = vPort.parent.pos.y + heightPadding + vPort.indice * perPortHeight + heightPadding / 2
nel.style.top = `${parseInt(ptop)}px`
$(nel).append($(`<div>${vPort.name}</div>`).addClass('vPortname'))
// draw outgoing svg,
if(outgoing){
// anchor position (absolute, within attached-to), delx, dely
let line = dt.svgLine(perNodeWidth - 2, ph / 2, linkWidth, 0, 2)
$(nel).append(line) // appended, so that can be rm'd w/ the .remove() call
}
if (vPort.el) $(vPort.el).remove()
vPort.el = nel
$(plane).append(vPort.el)
}
// draw vals,
let perNodeWidth = 60
let linkWidth = 30
let perPortHeight = 120
let heightPadding = 10
// for now, this'll look a lot like thar recursor,
// and we'll just do it once, assuming nice and easy trees ...
this.draw = (root) => {
let start = performance.now()
$('.node').remove()
$('.vPort').remove()
// alright binches lets recurse,
// node-to-draw, vPort-entered-on, depth of recursion
let recursor = (node, entry, entryTop, depth) => {
node.width = perNodeWidth // time being, we are all this wide
node.height = heightPadding * 2 + node.vPorts.length * perPortHeight // 10px top / bottom, 50 per port
node.pos = {}
node.pos.x = depth * (perNodeWidth + linkWidth) + xPlace // our x-pos is related to our depth,
// and the 'top' - now, if entry has an .el / etc data - if ports have this data as well, less calc. here
if (entry) {
node.pos.y = entryTop - entry.indice * perPortHeight - heightPadding
} else {
node.pos.y = yPlace
}
// draw ready?
renderNode(node)
// traverse,
for (let vp of node.vPorts) {
if (vp == entry) {
renderVPort(vp)
continue
} else if (vp.reciprocal) {
renderVPort(vp, true)
recursor(vp.reciprocal.parent, vp.reciprocal, node.pos.y + heightPadding + vp.indice * perPortHeight, depth + 1)
} else {
renderVPort(vp)
}
}
}
recursor(root, null, 0, 0)
//console.warn('draw was', performance.now() - start)
// root.pos = {
// x: 10,
// y: 10
// }
// root.width = 100
// root.height = 100
// renderNode(root)
// console.warn('next call')
// root.pos.x = 50
// renderNode(root)
} // end draw
// ------------------------------------------------------ SWEEP ROUTINES
// node is an element in the tree,
// path is the route to it, in uint8array
let sweepRecurse = async (node) => {
for (let p in node.vPorts) {
// don't try closed ports,
if (node.vPorts[p].portStatus != EP.PORTSTATUS.OPEN) continue
// can't traverse busses yet,
if (node.vPorts[p].portTypeKey != PK.PORTF.KEY) {
console.error('cannot traverse busses yet')
continue // BUSDIFFERENCE
}
// don't want to traverse back up,
// at this point, only the layer above has ahn reciprocal established, we are traversing down
if (node.vPorts[p].reciprocal) continue
// ok, we're set to dive,
let nextRoute = {
path: new Uint8Array(node.routeTo.path.length + 3),
segsize: 128
}
nextRoute.path.set(node.routeTo.path)
nextRoute.path[node.routeTo.path.length] = PK.PORTF.KEY
TS.write('uint16', parseInt(p), nextRoute.path, node.routeTo.path.length + 1, true)
//console.log('next path', nextRoute)
try {
// get num vPorts at next node, and use route back to discover exit port
let nodeRes = await osap.query(nextRoute, 'name', 'numVPorts')
//console.warn('noderes route', nodeRes.route)
// if this works, a node exists on the other side of this port,
let nextNode = {
routeTo: nextRoute,
name: nodeRes.data.name,
vPorts: []
}
// the port that the above query entered on,
let entryPort = TS.read('uint16', nodeRes.route.path, 1, true)
// for each next in line,
for (let np = 0; np < nodeRes.data.numVPorts; np++) {
try {
let portRes = await osap.query(nextRoute, 'vport', np, 'name', 'portTypeKey', 'portStatus', 'maxSegLength')
if(portRes.data.portStatus == EP.PORTSTATUS.CLOSED){
// try open,
try {
// console.warn('req open')
let writeRes = await osap.write(nextRoute, 'vport', np, 'portStatus', true)
} catch (err) {
console.warn('write-open err', err)
}
}
//console.log("PORT", portRes.data)
let vPort = {
parent: nextNode,
indice: parseInt(np),
name: portRes.data.name,
portTypeKey: portRes.data.portTypeKey,
portStatus: portRes.data.portStatus,
maxSegLength: portRes.data.maxSegLength,
}
nextNode.vPorts.push(vPort)
if(np == entryPort){
// circular linking
node.vPorts[p].reciprocal = nextNode.vPorts[np] // p (node-port) np (next-node-port)
nextNode.vPorts[np].reciprocal = node.vPorts[p]
}
} catch (err) {
console.error(err)
console.error('sweep / draw error at port', p, ',', nextNode.name)
nextNode.vPorts.push(null)
}
} // close query on next ports,
// continue
await sweepRecurse(nextNode)
} catch (err) {
throw err // ?? doth this pass up the layer ?
}
}
}
let sweeper = async () => {
// start from nil,
let root = {} // home node,
root.vPorts = [] // our ports,
root.name = osap.name
root.routeTo = {
path: new Uint8Array(0),
segsize: 128
}
// make definitions of our local ports,
for (let p in osap.vPorts) {
let pOut = {
indice: parseInt(p),
portTypeKey: osap.vPorts[p].portTypeKey,
portStatus: osap.vPorts[p].phy.status,
maxSegLength: osap.vPorts[p].phy.maxSegLength,
name: osap.vPorts[p].name
}
root.vPorts.push(pOut)
pOut.parent = root
// kick closed ports: this is different then the remainder of recurse, because we have
// direct access to it,
if(pOut.portStatus == EP.PORTSTATUS.CLOSED){
osap.vPorts[p].phy.open()
}
}
// now we can start here, to recurse through
try {
await sweepRecurse(root)
} catch (err) {
console.error('err during sweep', err)
}
// return the structure
return root
}
// hmm ...
let depthAnalysis = (root) => {
let depths = [0]
let recursor = (vPort, depth) => {
if (depth > 6) return // depth limit
if (vPort.reciprocal) { // places to go,
for (let vp of vPort.reciprocal.parent.vPorts) {
depths.push(depth + 1)
console.log(vPort.reciprocal.parent.name)
if (vp == vPort.reciprocal) continue // skip entry // TODO circular graphs would stil f us here
recursor(vp, depth + 1)
}
}
} // end recursor,
for (let vp of root.vPorts) {
recursor(vp, 0)
}
return Math.max(...depths)
}
// ------------------------------------------------------ DAT Control
let ctrl = {
polling: true,
interval: 600,
timer: undefined
}
let setPollingStatus = (val) =>{
if(val){
runSweepRoutine()
} else {
if(ctrl.timer){
clearTimeout(ctrl.timer)
ctrl.timer = undefined
}
}
}
let runSweepRoutine = async () => {
try {
let res = await sweeper()
this.draw(res)
} catch (err) {
console.error('sweeper err', err)
}
if(ctrl.polling){
ctrl.timer = setTimeout(runSweepRoutine, ctrl.interval)
}