From 4ecdf4f5bcdb8f57d6c78f119cd586e46c4021ed Mon Sep 17 00:00:00 2001
From: Jake Read <jake.read@cba.mit.edu>
Date: Mon, 7 Oct 2019 12:20:53 -0400
Subject: [PATCH] chat with leo, hello workers

---
 cf.js                                    |  52 ++++---
 gogetter.js                              |   2 +-
 hunks/hunks.js                           |   4 +
 hunks/image/readpng_leo.js               | 107 +++++++++++++++
 hunks/image/thresholdrgba_leo.js         | 109 +++++++++++++++
 hunks/pipes/pipetemplate.js              |  34 +++++
 hunks/pipes/vfpt.js                      | 164 +++++++++++++++++++++++
 hunks/{comm => pipes}/websocketclient.js |   0
 pipes/pipetemplate.js                    |   5 +
 scratch/test.png                         | Bin 0 -> 4028 bytes
 10 files changed, 457 insertions(+), 20 deletions(-)
 create mode 100644 hunks/image/readpng_leo.js
 create mode 100644 hunks/image/thresholdrgba_leo.js
 create mode 100644 hunks/pipes/pipetemplate.js
 create mode 100644 hunks/pipes/vfpt.js
 rename hunks/{comm => pipes}/websocketclient.js (100%)
 create mode 100644 pipes/pipetemplate.js
 create mode 100644 scratch/test.png

diff --git a/cf.js b/cf.js
index 8589b95..860be38 100644
--- a/cf.js
+++ b/cf.js
@@ -7,14 +7,20 @@ 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 {
+  Worker, workerData
+} = require('worker_threads')
 // will use these to figure where tf we are
 const os = require('os')
 let ifaces = os.networkInterfaces()
 
 // 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}))
+
 // if these don't exist, they get 'nexted' to any other 'middleware' we write
 app.get('/fileList', (req, res) => {
   try{
@@ -42,6 +48,7 @@ app.get('/fileList', (req, res) => {
     res.send('server-side error retrieving list')
   }
 })
+
 // we also handle file-saving this way,
 app.post('/save/contexts/:context/:file', (req, res) => {
   // this is probably fine for now, but I kind of want a websocket to do this kind of stuff ?
@@ -68,11 +75,35 @@ app.post('/save/systems/:file', (req, res) => {
   })
 })
 
+// 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
+app.get('/pipeHookup/:file', (req, res) => {
+  // we can assume that the file is a reciprocal pipe-type in our local pipes/file.js location
+  // we'll open that can as a spawn, can assume it's hosting a websocket (it will tell us the port?)
+  // and we can send that information back up stream,
+  console.log('/pipes', req.params.file)
+  const piper = new Worker(`${__dirname}/pipes/${req.params.file}`)
+  piper.on('message', (msg) => {
+    console.log('worker msg', msg)
+  })
+  piper.on('error', (err) => {
+    console.log('worker err', err)
+  })
+  piper.on('exit', (code) => {
+    console.log('exit code', code)
+  })
+  res.send({address: 'localip', port: '1024'})
+  // then this (or similar) should be all we really need to add here, and we can do local-dev of the pipes in hunks/pipes/pipename.js and pipes/pipename.js
+  // so, do we spawn, or do we use workers ?
+  // awh yis it's workers, messages easy, errors also OK to catch... nextup: draw the sys, what wraps what doesn't? mostly: want to be able to refresh / reload / restart remotely
+})
+
+// finally, we tell the thing to listen here:
 let port = 8080
-// and listen,
 app.listen(port)
 
-// want to announce our existence, this just logs our IPs to the console:
+// this just logs the processes IP's to the termina
 Object.keys(ifaces).forEach(function(ifname) {
   var alias = 0;
 
@@ -93,20 +124,3 @@ Object.keys(ifaces).forEach(function(ifname) {
     ++alias;
   });
 });
-
-
-/*
-let begin = () => {
-  // setup to dish files,
-  ex.get('/', (req, res) => {
-    console.log('req /')
-    res.sendFile(__dirname + '/index.html')
-  })
-
-  http.listen(8080, () => {
-    console.log("is listening on 8080")
-  })
-}
-
-begin()
-*/
diff --git a/gogetter.js b/gogetter.js
index 767eb1c..2be6b76 100644
--- a/gogetter.js
+++ b/gogetter.js
@@ -8,7 +8,7 @@ function GoGetter() {
   this.recursivePathSearch = (root, debug) => {
     return new Promise((resolve, reject) => {
       jQuery.get(`/fileList?path=${root}`, (resp) => {
-        console.log('resp at jq', resp)
+        //console.log('resp at jq', resp)
         resolve(resp)
       })
     })
diff --git a/hunks/hunks.js b/hunks/hunks.js
index 3ad3bfa..eae5b6f 100644
--- a/hunks/hunks.js
+++ b/hunks/hunks.js
@@ -325,6 +325,10 @@ function deepCopy(obj) {
   // turns out parse/stringify is the fastest,
   //console.log('to dc', obj)
   //console.log(JSON.stringify(obj))
+  if(obj instanceof ImageData) {
+    let newData = new ImageData(obj.data, obj.width, obj.height)
+    return newData
+  }
   return JSON.parse(JSON.stringify(obj))
 }
 
diff --git a/hunks/image/readpng_leo.js b/hunks/image/readpng_leo.js
new file mode 100644
index 0000000..2426ce6
--- /dev/null
+++ b/hunks/image/readpng_leo.js
@@ -0,0 +1,107 @@
+/*
+
+hunk template
+
+*/
+
+// these are ES6 modules
+import {
+  Hunkify,
+  Input,
+  Output,
+  State
+} from '../hunks.js'
+
+import {
+  html,
+  svg,
+  render
+} from 'https://unpkg.com/lit-html?module';
+
+export default function UploadPNG() {
+  // this fn attaches handles to our function-object,
+  Hunkify(this)
+
+  const imageOut = new Output('RGBA', 'Image', this)
+  this.outputs.push(imageOut)
+
+  this.init = () => {
+    this.dom = $('<div>').get(0)
+  }
+
+  //import
+  const png_read_handler = (e) => {
+    const reader = new FileReader();
+
+    const files = e.target.files;
+    const file = files[0];
+
+    const img = new Image();
+    img.file = file;
+
+    const loader = (aImg) => (e) => {
+      aImg.src = e.target.result;
+
+      aImg.onload = () => {
+        const canvas = document.getElementById("wackyAndTotallyUniqueID2");
+        const ctx = canvas.getContext("2d");
+        ctx.clearRect(0, 0, canvas.width, canvas.height);
+        ctx.drawImage(aImg, 0, 0);
+
+        // string other functions here
+        const data = getImageDataRGBA();
+        // console.log("data", data);
+
+        imageOut.put(data)
+        // console.log(data);
+        // console.log("copied", JSON.stringify(data));
+      }
+
+    };
+    reader.onload = loader(img);
+    reader.readAsDataURL(file);
+  }
+
+  // get image data: RGBA
+  const getImageDataRGBA = () => { //should add dpi term here
+    const canvas = document.getElementById("wackyAndTotallyUniqueID2");
+    const ctx = canvas.getContext("2d");
+    const imageRGBA = ctx.getImageData(0, 0, canvas.width, canvas.height);
+    return imageRGBA;
+  }
+
+  this.onload = () => {
+    const target = $('<div>').get(0);
+    $(this.dom).append(target);
+
+    const view = html `
+      <style>
+        #wackyAndTotallyUniqueID2 {
+          border: 1px solid black;
+          margin: 5px;
+        }
+
+        #wackyAndTotallyUniqueID {
+          position: absolute;
+          left: 10px;
+          top: 350px;
+        }
+      </style>
+      <div>Upload!</div>
+      <canvas
+        id="wackyAndTotallyUniqueID2"
+        width=300
+        height=300>
+      </canvas>
+      <input
+        id="wackyAndTotallyUniqueID"
+        type="file"
+        accept="image/png"
+        @input=${(e) => png_read_handler(e)}></input>
+    `
+    render(view, target);
+  }
+
+
+  this.loop = () => {}
+}
diff --git a/hunks/image/thresholdrgba_leo.js b/hunks/image/thresholdrgba_leo.js
new file mode 100644
index 0000000..fcd4d6f
--- /dev/null
+++ b/hunks/image/thresholdrgba_leo.js
@@ -0,0 +1,109 @@
+/*
+
+hunk template
+
+*/
+
+// these are ES6 modules
+import {
+  Hunkify,
+  Input,
+  Output,
+  State
+} from '../hunks.js'
+
+import {
+  html,
+  svg,
+  render
+} from 'https://unpkg.com/lit-html?module';
+
+
+export default function Threshold() {
+  // this fn attaches handles to our function-object,
+  Hunkify(this)
+
+  // inputs
+  let imageIn = new Input('RGBA', 'Image', this);
+  this.inputs.push(imageIn);
+
+  // states
+  let threshold = new State('number', 'Threshold', .5);
+  this.states.push(threshold);
+
+  // outputs
+  let imageOut = new Output('RGBA', 'Image', this);
+  this.outputs.push(imageOut);
+
+  // Helper Functions
+  const thresholdRGBA = (imageRGBA, threshold) => {
+    console.log(imageRGBA)
+    const w = imageRGBA.width;
+    const h = imageRGBA.height;
+    const buf = imageRGBA.data;
+    const t = threshold;
+
+    let r, g, b, a, i;
+    for (var row = 0; row < h; ++row) {
+      for (var col = 0; col < w; ++col) {
+        r = buf[(h - 1 - row) * w * 4 + col * 4 + 0];
+        g = buf[(h - 1 - row) * w * 4 + col * 4 + 1];
+        b = buf[(h - 1 - row) * w * 4 + col * 4 + 2];
+        a = buf[(h - 1 - row) * w * 4 + col * 4 + 3];
+        i = (r + g + b) / (3 * 255);
+
+        let val;
+        if (a === 0) {
+          val = 255;
+        } else if (i > t) {
+          val = 255;
+        } else {
+          val = 0;
+        }
+
+        buf[(h - 1 - row) * w * 4 + col * 4 + 0] = val;
+        buf[(h - 1 - row) * w * 4 + col * 4 + 1] = val;
+        buf[(h - 1 - row) * w * 4 + col * 4 + 2] = val;
+        buf[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+      }
+    }
+
+    const imgdata = new ImageData(buf, w, h)
+
+    return imgdata;
+  }
+
+  // view
+  // as is tradition
+  this.dom = {}
+  this.init = () => {
+    this.dom = $('<div>').get(0)
+  }
+
+  this.onload = () => {
+    const target = $('<div>').get(0);
+    $(this.dom).append(target);
+
+    const view = html `
+    <button
+      @mousedown=${() => {
+        // console.log("imageIn", imageIn.get());
+        // console.log("copied", deepCopy(imageIn));
+        const newOut = thresholdRGBA(imageIn.get(), threshold.value)
+        imageOut.put(newOut);
+      }}>
+      Calculate
+    </button>
+    `
+
+    render(view, target);
+  }
+
+  //loop
+  this.loop = () => {
+    if(imageIn.io() && !imageOut.io()){
+      const thresholded = thresholdRGBA(imageIn.get(), threshold.value)
+      imageOut.put(thresholded)
+    }
+  }
+}
diff --git a/hunks/pipes/pipetemplate.js b/hunks/pipes/pipetemplate.js
new file mode 100644
index 0000000..9a7c5f3
--- /dev/null
+++ b/hunks/pipes/pipetemplate.js
@@ -0,0 +1,34 @@
+/*
+
+pipes are websocket-having devices that commune with our server,
+this is a scratch / example of one such object
+
+*/
+
+import {
+  Hunkify,
+  Input,
+  Output,
+  State
+} from '../hunks.js'
+
+export default function Pipe() {
+  Hunkify(this)
+  let debug = true
+
+  let statusMessage = new State('string', 'status', 'closed')
+  let retryButton = new State('boolean', 'retry', false)
+  this.states.push(statusMessage, retryButton)
+
+  // coming merge of init and onload, however:
+  this.init = () => {
+    // hijack ajax to ask for a websocket
+    jQuery.get('pipeHookup/pipetemplate.js', (data) => {
+      console.log('pipe jquery data', data)
+    })
+  }
+
+  this.loop = () => {
+    // ws status, ws messages ...
+  }
+}
diff --git a/hunks/pipes/vfpt.js b/hunks/pipes/vfpt.js
new file mode 100644
index 0000000..5627185
--- /dev/null
+++ b/hunks/pipes/vfpt.js
@@ -0,0 +1,164 @@
+/*
+
+very fast ~~picket ship~~ pipe transport
+
+*/
+
+import {
+  Hunkify,
+  Input,
+  Output,
+  State
+} from '../hunks.js'
+
+function VFPT() {
+  Hunkify(this)
+
+  let debug = false
+
+  let dtin = new Input('byteArray', 'data', this)
+  this.inputs.push(dtin)
+
+  let dtout = new Output('byteArray', 'data', this)
+  this.outputs.push(dtout)
+
+  // TODO is tackling state sets / updates / onupdate fn's
+  // this is hunk -> manager commune ...
+  let statusMessage = new State('string', 'status', 'closed')
+  let retryCountHandle = new State('number', 'retrycount', 3)
+  let resetRetryHandle = new State('boolean', 'retryreset', false)
+  let addressState = new State('string', 'address', '127.0.0.1')
+  let portState = new State('number', 'port', 2042)
+  this.states.push(statusMessage, retryCountHandle, resetRetryHandle, addressState, portState)
+
+  // this ws is a client,
+  let ws = {}
+  let url = 'ws://127.0.0.1:2020'
+  this.outbuffer = new Array()
+
+  this.init = () => {
+    setTimeout(startWs, 500)
+  }
+
+  resetRetryHandle.change = (value) => {
+    retryCountHandle.set(3)
+    startWs()
+    // to actually change the value, we would do:
+    // resetRetryHandle.set(value)
+  }
+
+  let startWs = () => {
+    // manager calls this once
+    // it is loaded and state is updated (from program)
+    url = 'ws://' + addressState.value + ':' + portState.value
+    this.log(`attempt start ws at ${url}`)
+    ws = new WebSocket(url)
+    ws.binaryType = "arraybuffer"
+    ws.onopen = (evt) => {
+      this.log('ws opened')
+      statusMessage.set('open')
+    }
+    ws.onerror = (evt) => {
+      this.log('ws error, will reset to check')
+      console.log('ws error:', evt)
+      if(debug) console.log(evt)
+      statusMessage.set('error')
+      setCheck(500)
+    }
+    ws.onclose = (evt) => {
+      this.log('ws close')
+      setCheck(500)
+    }
+    ws.onmessage = (message) => {
+      // this should be a buffer
+      if(debug) console.log('WS receives', message.data)
+      // tricks?
+      // ok, message.data is a blob, we know it's str8 up bytes, want that
+      // as an array
+      let msgAsArray = new Uint8Array(message.data)
+      // it's messy, yep!
+      let msgAsStdArray = Array.from(msgAsArray)
+      if(debug) console.log('WS receive, as an array:', msgAsArray);
+      if (dtout.ie && this.outbuffer.length === 0) {
+        dtout.put(msgAsStdArray)
+      } else {
+        this.outbuffer.push(msgAsStdArray)
+      }
+    }
+    statusMessage.set('ws initialized...')
+  }
+
+  let checking = false
+
+  let setCheck = (ms) => {
+    if (checking) {
+      // noop
+    } else {
+      setTimeout(checkWsStatus, ms)
+      checking = true
+    }
+  }
+
+  let checkWsStatus = () => {
+    let retrycount = retryCountHandle.value - 1
+    if (retrycount < 1) {
+      // give up
+      statusMessage.set('not connected')
+      retryCountHandle.set(0)
+      checking = false
+    } else {
+      retryCountHandle.set(retrycount)
+      checking = false
+      this.log('CHECKING STATUS')
+      switch (ws.readyState) {
+        case WebSocket.CONNECTING:
+          this.log('ws is in process of connecting...')
+          break
+        case WebSocket.OPEN:
+          this.log('is open')
+          break
+        case WebSocket.CLOSING:
+          this.log('is closing')
+          break
+        case WebSocket.CLOSED:
+          this.log('is closed, retrying ...')
+          startWs()
+          break
+        default:
+          throw new Error('nonsensical result at ws readystate check for ws')
+          break
+      }
+    }
+  }
+
+  // override default change f'n
+  retryCountHandle.change = (value) => {
+    this.log('retrycount reset')
+    retryCountHandle.set(value)
+    setCheck(10)
+  }
+
+  this.loop = () => {
+    // something like if(ws !== null && ws.isopen)
+    // if we have an open port, and have bytes to send downstream,
+    if (ws !== null && ws.readyState === 1) {
+      // no buffering
+      if (dtin.io()) {
+        let arr = dtin.get()
+        if(debug) console.log('WS transmission as array', arr)
+        let bytesOut = Uint8Array.from(arr)
+        // HERE insertion -> buffer.from() ?
+        if(debug) console.log("WS sending buffer", bytesOut.buffer)
+        ws.send(bytesOut.buffer)
+      }
+    }
+
+    // check if we have outgoing to pass along
+    if (this.outbuffer.length > 0 && !dtout.io()) {
+      dtout.put(this.outbuffer.shift())
+    }
+
+  }
+}
+
+export default VFPT
diff --git a/hunks/comm/websocketclient.js b/hunks/pipes/websocketclient.js
similarity index 100%
rename from hunks/comm/websocketclient.js
rename to hunks/pipes/websocketclient.js
diff --git a/pipes/pipetemplate.js b/pipes/pipetemplate.js
new file mode 100644
index 0000000..913ae02
--- /dev/null
+++ b/pipes/pipetemplate.js
@@ -0,0 +1,5 @@
+const {
+  parentPort
+} = require('worker_threads')
+
+parentPort.postMessage('hello worker')
diff --git a/scratch/test.png b/scratch/test.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d416e2339529d7e07a8fd2aeb97b6746bf65eb9
GIT binary patch
literal 4028
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

literal 0
HcmV?d00001

-- 
GitLab