Skip to content
Snippets Groups Projects
main.js 14.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jake Read's avatar
    Jake Read committed
    //
    
    Jake Read's avatar
    Jake Read committed
    // nnc.js
    //
    
    Jake Read's avatar
    Jake Read committed
    //
    // Jake Read at the Center for Bits and Atoms
    // (c) Massachusetts Institute of Technology 2018
    //
    // This work may be reproduced, modified, distributed, performed, and
    // displayed for any purpose, but must acknowledge the mods
    // project. Copyright is retained and must be preserved. The work is
    // provided as is; no warranty is provided, and users accept all
    // liability.
    
    
    RULES:
    
    modules have Ouputs() and Inputs() that connect to other software modules running on the same CPU. It's a directed graph.
    
    modules can have ATKPort()s that use a Link() to get to remote hardware, per message-passing network. It's a physical graph of duplex connections.
    
    this is nice because we might have two Link()s to double up on bandwidth. 
    
    alternately, Link()s could be TCP connections to other pieces of hardware, including something like us ! 
    
    */
    
    /*
    
    
    SYSTEM REQUIRES
    
    */
    
    
    Jake Read's avatar
    Jake Read committed
    // file system to load / look at our available modules / edit them 
    const fs = require('fs')
    
    Jake Read's avatar
    Jake Read committed
    // express serves files, http makes the connection
    const app = require('express')()
    const http = require('http').Server(app)
    
    // websocket, to share program representations with the client (and back)
    
    Jake Read's avatar
    Jake Read committed
    const WebSocket = require('ws')
    
    /*
    
    SERVER AND WS SETUP
    
    */
    
    
    Jake Read's avatar
    Jake Read committed
    // serving this handful of static files
    app.get('/', (req, res) => {
        console.log('client req /')
        res.sendFile(__dirname + '/client/index.html')
    })
    
    app.get('/:file', (req, res) => {
        console.log('client req', req.params.file)
        res.sendFile(__dirname + '/client/' + req.params.file)
    })
    
    
    Jake Read's avatar
    Jake Read committed
    // and listening for requests here
    const wss = new WebSocket.Server({ port: 8081 })
    
    wss.on('connection', (ws) => {
        sckt = ws
    
    Jake Read's avatar
    Jake Read committed
        socketSend('console', 'hello client')
    
    Jake Read's avatar
    Jake Read committed
        // send current config as list of all modules
        putReps()
    
    Jake Read's avatar
    Jake Read committed
        console.log('socket open on 8081')
        ws.on('message', (evt) => {
            socketRecv(evt)
        })
    })
    
    // through this window
    http.listen(8080, () => {
        console.log('listening on 8080 for static files')
    })
    
    
    Jake Read's avatar
    Jake Read committed
    // server globals 
    
    
    Jake Read's avatar
    Jake Read committed
    function socketSend(type, data) {
    
        if (sckt) {
            var msg = {
                type: type,
                data: data
            }
    
            //console.log('SEND', msg)
    
            sckt.send(JSON.stringify(msg))
    
    Jake Read's avatar
    Jake Read committed
    function socketRecv(evt) {
    
    Jake Read's avatar
    Jake Read committed
        var recv = JSON.parse(evt)
        var type = recv.type
        var data = recv.data
    
        //console.log('RECV', recv)
    
    Jake Read's avatar
    Jake Read committed
        // bang thru
    
    Jake Read's avatar
    Jake Read committed
        switch (type) {
    
    Jake Read's avatar
    Jake Read committed
            case 'console':
    
                console.log('RECV CONSOLE:', data)
    
    Jake Read's avatar
    Jake Read committed
                break
            case 'get menu':
                var tree = buildMenu()
    
    Jake Read's avatar
    Jake Read committed
                socketSend('put menu', tree)
                break
            case 'add module':
                addModule(data)
                break
    
    Jake Read's avatar
    Jake Read committed
            case 'put state':
                changeState(data)
    
    Jake Read's avatar
    Jake Read committed
                break
            case 'put link':
    
    Jake Read's avatar
    Jake Read committed
                // id:output > id:input
                break
    
            case 'put ui':
                changeUi(data)
                break
    
    Jake Read's avatar
    Jake Read committed
            case 'rm link':
                // id:output > id:input
    
    Jake Read's avatar
    Jake Read committed
                break
            default:
    
                console.log('ERR server recv with non recognized type', recv)
    
    Jake Read's avatar
    Jake Read committed
                break
    
    Jake Read's avatar
    Jake Read committed
    ------------------------------------------------------
    
    PROGRAM AS API
    
    Jake Read's avatar
    Jake Read committed
    ------------------------------------------------------
    
    Jake Read's avatar
    Jake Read committed
    // nodes are objects having inputs, outputs, and state
    // nodes can be made up of other nodes 
    // a node originates somewhere, 
    
    Jake Read's avatar
    Jake Read committed
    // is it a JSON, or a txt .js file ? loadProgram and loadModule ...
    // it wants to be an object that gets 'read' in ... 
    
    Jake Read's avatar
    Jake Read committed
    var program = {
        description: {
            name: 'tstprgmem',
    
    Jake Read's avatar
    Jake Read committed
        },
    
    Jake Read's avatar
    Jake Read committed
    
    
    Jake Read's avatar
    Jake Read committed
    // add things
    
    Jake Read's avatar
    Jake Read committed
    var gate = addModuleToProgram(program, './src/util/gate.js')
    var delay = addModuleToProgram(program, './src/util/delay.js')
    var log = addModuleToProgram(program, './src/util/log.js')
    
    Jake Read's avatar
    Jake Read committed
    
    
    Jake Read's avatar
    Jake Read committed
    // hookup as usual 
    
    gate.outputs.out.attach(delay.inputs.thru)
    delay.outputs.out.attach(log.inputs.thru)
    gate.outputs.out.attach(log.inputs.thru)
    
    
    console.log('saving onesave.json')
    
    Jake Read's avatar
    Jake Read committed
    saveProgram(program, 'save/onesave.json')
    
    program = null 
    delete gate 
    delete delay
    delete log 
    
    
    console.log('opening onesave.json')
    
    Jake Read's avatar
    Jake Read committed
    var prgmem = openProgram('save/onesave.json')
    
    saveProgram(prgmem, 'save/twosave.json')
    
    // working through:
    /*
    
     - add load-from-thing-connect-outputs-to-inputs 
     - add new state loading / structure?
     - UI / letters 
    
    */
    
    
    Jake Read's avatar
    Jake Read committed
    /*
    ------------------------------------------------------
    
    Jake Read's avatar
    Jake Read committed
    PROGRAM ASSEMBLY 
    
    Jake Read's avatar
    Jake Read committed
    ------------------------------------------------------
    
    Jake Read's avatar
    Jake Read committed
    function addModuleToProgram(program, path) {
    
    Jake Read's avatar
    Jake Read committed
        // program is an object seeking heirarchy, has program.modules 
    
    Jake Read's avatar
    Jake Read committed
        if (fs.existsSync(path)) {
    
    Jake Read's avatar
    Jake Read committed
            var src = require(path)
            var mod = new src()
    
    
            // wants unique module id's 
            if(program.description.counter == null){
                program.description.counter = 0
            } else {
                program.description.counter ++ 
            }
    
    
    Jake Read's avatar
    Jake Read committed
            // make unique name 
    
            mod.description.id = mod.description.id = mod.description.name + '-' + program.description.counter
    
    Jake Read's avatar
    Jake Read committed
            mod.description.path = path
    
            
            program.modules[mod.description.id] = mod
    
    Jake Read's avatar
    Jake Read committed
    
            // input need references for later hookup
            for (key in mod.inputs) {
                mod.inputs[key].parentId = mod.description.id
                mod.inputs[key].key = key
    
    Jake Read's avatar
    Jake Read committed
            // state updating, begs for update 
            for (key in mod.state) {
                if (key == 'onChange' | key == 'emitChange' | key == 'emitters') {
    
                    //console.log('rolling past change fn')
                } else {
    
    Jake Read's avatar
    Jake Read committed
                    mod.state['_' + key] = mod.state[key]
                    mod.state[key] = {}
                    writeStateObject(mod, key)
    
    Jake Read's avatar
    Jake Read committed
            if (program.description.id == null) {
                if (program.description.name == null) {
                    if (program.description == null) {
                        program.description = {}
                    }
                    program.description.name = 'unnamed program'
                }
                program.description.id = program.description.name + '-' + 0
            }
    
    Jake Read's avatar
    Jake Read committed
            console.log('ADDING', mod.description.id, 'to', program.description.id)
            // also return it so that we can write programs without the UI 
    
    Jake Read's avatar
    Jake Read committed
        } else {
    
    Jake Read's avatar
    Jake Read committed
            console.log('ERR no module found at ', path)
    
    Jake Read's avatar
    Jake Read committed
    function writeStateObject(mod, key) {
        Object.defineProperty(mod.state, key, {
    
    Jake Read's avatar
    Jake Read committed
                // update internal value 
    
    Jake Read's avatar
    Jake Read committed
                this['_' + key] = x
                //console.log('SET', key, this['_' + key])
    
    Jake Read's avatar
    Jake Read committed
                // push to internal state change handler
    
    Jake Read's avatar
    Jake Read committed
                this.emitChange(key)
    
    Jake Read's avatar
    Jake Read committed
                // update server (or in some cases, a confirmation)
                putState(mod)
    
    Jake Read's avatar
    Jake Read committed
        Object.defineProperty(mod.state, key, {
    
    Jake Read's avatar
    Jake Read committed
                //console.log('GET', key, this['_' + key])
                return this['_' + key]
    
    Jake Read's avatar
    Jake Read committed
    function makeRepFromModule(mdl) {
        var rep = {
            description: {
                id: mdl.description.id,
                name: mdl.description.name,
                alt: mdl.description.alt,
                path: mdl.description.path
            }
        }
    
        // TODO: making rep. of input / output should be a f'n of that object ...
        // input, outputs, state objs should be known /sysobjects 
        // everything else is free play 
        rep.inputs = {}
        for (key in mdl.inputs) {
            rep.inputs[key] = {}
            rep.inputs[key].accepts = mdl.inputs[key].accepts
        }
    
        rep.outputs = {}
        for (key in mdl.outputs) {
            rep.outputs[key] = {}
            rep.outputs[key].emits = mdl.outputs[key].emits
            rep.outputs[key].calls = new Array()
            mdl.outputs[key].calls.forEach(function(inp) {
                var input = {
                    parentId: inp.parentId,
                    key: inp.key
                }
                rep.outputs[key].calls.push(input)
            })
        }
    
        rep.state = {}
        for(key in mdl.state){
            if(isStateKey(key)){
                rep.state[key] = mdl.state[key]
            }
        }
    
        return rep
    }
    
    function saveProgram(prgmem, path) {
        // ok, and we're interested in just copying the relevant things ... 
        var svprgmem = {
            description: {
    
                name: prgmem.description.name,
                counter: prgmem.description.counter
    
    Jake Read's avatar
    Jake Read committed
            },
    
    Jake Read's avatar
    Jake Read committed
        }
    
        var mdls = prgmem.modules
    
    
        for(key in mdls){
            var mdl = mdls[key]
    
    Jake Read's avatar
    Jake Read committed
            var og = makeRepFromModule(mdl)
    
            svprgmem.modules[mdl.description.id] = og 
        }
    
    Jake Read's avatar
    Jake Read committed
    
        fs.writeFileSync(path, JSON.stringify(svprgmem, null, 2), 'utf8')
    }
    
    function openProgram(path){
        var program = {}
    
        // get the .json file as an object 
        var prgRep = JSON.parse(fs.readFileSync(path, 'utf8'))
    
        console.log('OPENING THIS PRGRAM REP', prgRep)
    
    Jake Read's avatar
    Jake Read committed
    
    
        // copy-pasta the program descro, 
    
    Jake Read's avatar
    Jake Read committed
        program.description = {
            name: prgRep.description.name,
    
            counter: 0,
            id: prgRep.description.name + 1, // in another world, we count
    
    Jake Read's avatar
    Jake Read committed
            path: path
        }
    
    
        // gonna get those modules from source
        program.modules = {}
    
    Jake Read's avatar
    Jake Read committed
    
    
        for(key in prgRep.modules){
            var mdlRep = prgRep.modules[key]
            addModuleToProgram(program, mdlRep.description.path)
        }
    
        // gonna hook 'em up
        for(modName in prgRep.modules){
            // keys should be identical for rep and heap
            var mdlRep = prgRep.modules[modName]
            var mdl = program.modules[modName]
    
            for(outName in mdlRep.outputs){
                var outRep = mdlRep.outputs[outName]
                // each has some caller ids 
                for(nestedInputRep in outRep.calls){
                    // conn from tl program -> this hookup 
                    var nIRParent = outRep.calls[nestedInputRep].parentId
                    var nIRKey = outRep.calls[nestedInputRep].key 
                    var nI = program.modules[nIRParent].inputs[nIRKey]
                    console.log("ATTACHING", nI, nIRKey, 'to', mdl.outputs[outName])
                    mdl.outputs[outName].attach(nI)
                }
            }
        }
    
    Jake Read's avatar
    Jake Read committed
    
        // once modules exist, link inputs / outputs / copy state ? 
    
        return program
    }
    
    
    Jake Read's avatar
    Jake Read committed
    /*
    
    
    Jake Read's avatar
    Jake Read committed
    PROGRAM REPRESENT 
    
    Jake Read's avatar
    Jake Read committed
    
    */
    
    
    Jake Read's avatar
    Jake Read committed
    
    
    Jake Read's avatar
    Jake Read committed
    //console.log('modules at prgmem start', modules)
    function putReps() {
    
        var reps = new Array()
    
    Jake Read's avatar
    Jake Read committed
        for (mod in modules) {
    
            reps.push(makeRep(modules[mod]))
    
    Jake Read's avatar
    Jake Read committed
        }
    
        socketSend('add reps', reps)
    
    Jake Read's avatar
    Jake Read committed
    }
    
    
    Jake Read's avatar
    Jake Read committed
    function makeRep(mod) {
    
    Jake Read's avatar
    Jake Read committed
        // scrape for only things we'll want to represent 
        var rep = {
            id: mod.id,
            description: mod.description,
            inputs: mod.inputs,
            outputs: mod.outputs,
            state: {}
        }
    
        // holds UI information - to load mods at location
    
    Jake Read's avatar
    Jake Read committed
        if (mod.ui != null) {
    
            rep.ui = mod.ui
    
    Jake Read's avatar
    Jake Read committed
        }
    
    
    Jake Read's avatar
    Jake Read committed
        for (var key in mod.state) {
            if (isStateKey(key)) {
    
    Jake Read's avatar
    Jake Read committed
                rep.state[key] = mod.state[key]
            }
        }
    
    
        return rep
    }
    
    function putRep(mod) {
        socketSend('add rep', makeRep(mod))
    }
    
    
    Jake Read's avatar
    Jake Read committed
    function changeRep(mod) {
    
        socketSend('change rep', makeRep(mod))
    
    Jake Read's avatar
    Jake Read committed
    }
    
    // update state from UI to server 
    function changeState(data) {
    
    Jake Read's avatar
    Jake Read committed
        // should just recv all state, walk tree for differences
    
    Jake Read's avatar
    Jake Read committed
        var oldState = modules[data.id].state
        var newState = data.state
        // rep only holds proper key-values w/o emitters, _values etc
    
    Jake Read's avatar
    Jake Read committed
        for (var key in newState) {
    
    Jake Read's avatar
    Jake Read committed
            if (isStateKey(key)) {
    
    Jake Read's avatar
    Jake Read committed
                if (oldState[key].isButton) {
                    if (oldState[key].isPressed != newState[key].isPressed) {
    
                        console.log('CHANGE BUTTON STATE', key, 'to', newState[key], 'in', data.id)
    
    Jake Read's avatar
    Jake Read committed
                        // this will trigger some 'onChange' f'ns
                        // that might change some state
    
    Jake Read's avatar
    Jake Read committed
                        oldState[key] = newState[key]
    
    Jake Read's avatar
    Jake Read committed
                        // to prevent quickly changing it back, we'll exit now (one state change from UI per trx)
    
                } else if (oldState[key].isMultiLine) {
    
                    if (oldState[key].value != newState[key].value) {
    
                        console.log('CHANGE MULTILINE STATE', key, 'to', newState[key].value, 'in', data.id)
    
                        oldState[key].value = newState[key].value
                        return true
    
    Jake Read's avatar
    Jake Read committed
                } else if (oldState[key] !== newState[key]) {
    
    Jake Read's avatar
    Jake Read committed
                    console.log('CHANGE STATE', key, 'to', newState[key], 'in', data.id)
    
                    oldState[key] = newState[key]
                    return true
    
    Jake Read's avatar
    Jake Read committed
                }
    
    Jake Read's avatar
    Jake Read committed
            }
    
    Jake Read's avatar
    Jake Read committed
    function changeUi(data) {
    
        var mod = modules[data.id]
    
    Jake Read's avatar
    Jake Read committed
        if (mod.ui == null) {
    
            console.log("NO UI")
            mod.ui = {}
        }
        mod.ui = data.ui
        console.log('CHANGE UI', mod.id, mod.ui)
    }
    
    
    Jake Read's avatar
    Jake Read committed
    function setUiPos(module, left, top) {
        if (module.ui == null) {
            module.ui = {}
        }
        module.ui.left = left
        module.ui.top = top
    }
    
    
    
    Jake Read's avatar
    Jake Read committed
    function isStateKey(key) {
        if (key.indexOf('_') == 0 || key == 'emitters' || key == 'onChange' || key == 'emitChange') {
    
    Jake Read's avatar
    Jake Read committed
            return false
        } else {
            return true
        }
    }
    
    // push new state from server to UI
    function putState(mod) {
        // push just the state to the individual mod
    
    Jake Read's avatar
    Jake Read committed
        // copy only the keys we want to see 
        var state = {}
    
    Jake Read's avatar
    Jake Read committed
        for (var key in mod.state) {
            if (isStateKey(key)) {
    
    Jake Read's avatar
    Jake Read committed
                state[key] = mod.state[key]
            }
        }
    
    Jake Read's avatar
    Jake Read committed
        var data = {
            id: mod.id,
    
    Jake Read's avatar
    Jake Read committed
            state: state
    
    Jake Read's avatar
    Jake Read committed
        }
    
        socketSend('change state', data)
    
    Jake Read's avatar
    Jake Read committed
    }
    
    
    function putLink(data) {
        var fromModule = modules.find((module) => {
            return module.id === data.from.id
        })
    
        var toModule = modules.find((module) => {
            return module.id === data.to.id
        })
    
    
        if (fromModule.outputs[data.from.output].isLinked(toModule.inputs[data.to.input])) {
            console.log("HOOKDOWN")
            fromModule.outputs[data.from.output].remove(toModule.inputs[data.to.input])
    
        } else {
    
            fromModule.outputs[data.from.output].attach(toModule.inputs[data.to.input])
    
        // replace it 
        changeRep(fromModule)
    
    Jake Read's avatar
    Jake Read committed
    function buildMenu() {
        var tree = {}
    
    Jake Read's avatar
    Jake Read committed
        var dir = fs.readdirSync('./src')
    
    Jake Read's avatar
    Jake Read committed
        for (i in dir) {
    
    Jake Read's avatar
    Jake Read committed
            tree[dir[i]] = {}
            var subdir = fs.readdirSync('./src/' + dir[i])
    
    Jake Read's avatar
    Jake Read committed
            for (j in subdir) {
    
    Jake Read's avatar
    Jake Read committed
                // find js files
    
    Jake Read's avatar
    Jake Read committed
                if (subdir[j].slice(-3) === '.js') {
    
    Jake Read's avatar
    Jake Read committed
                    var obj = {}
                    obj.path = './src/' + dir[i] + '/' + subdir[j]
                    tree[dir[i]][subdir[j].slice(0, -3)] = obj
                }
            }
        }
    
    Jake Read's avatar
    Jake Read committed
        return tree
    }
    
    Jake Read's avatar
    Jake Read committed
    // put