Help with emulating an LCD

I have made some custom hardware for LCD + 4 rotary-encoders. The idea is I want to send OSC messages like this from puredata to Open Stage Control:

text COLOR X Y ...TEXT - COLOR is 0/1
textsize X - set text-size to X
rectangle COLOR X Y W H - COLOR is 0/1
circle COLOR X Y R - draw a cirlce, COLOR is 0/1
triangle COLOR X0 Y0 X1 Y1 X2 Y2 - draw a triangle with 3 points, COLOR is 0/1
line COLOR X0 Y0 X1 Y1 - draw a line between 2 points, COLOR is 0/1
graph COLOR X Y W H ...NUMBERS - creates a scaled graph of NUMBERS that fits in the rect, COLOR is 0/1
clear - clear the display
scroll TYPE - do something with hardware-scrolling, TYPE is RIGHT/LEFT/DIAGRIGHT/DIAGLEFT/STOP
rotate DEG - rotate screen. DEG is 0-359
invert X - invert colors - X is 0/1
loadimage NAME FILENAME - load & convert an image (prepare for drawing)
image X Y NAME - display a loaded image

I am comfortable with javascript, and feel confident about implementing the actual canvas emulation, but I am unsure how to put it together in Open Stage Control.

I wrote a native pd extension in C that can handle pd messages like this, and display it on my real OLED, but I think it'd be cool to make an emulator that can handle all the message-routing for turning OSC into hardware commands. That way puredata can send commands to Open Stage Control frontend, and it will show things, and update the real hardware (if connected.) I can use it as an emulator for the hardware, if not available, and it will allow remote access over web, complete with screen. I think I want an "LCD fragment" so it can be inserted into different Open Stage Control UIs.

So I guess there are a few parts I am unclear on:

How do I get access to the ctx in a script that handles incoming messages? Like I did this to test:

And it works fine. I can work out the part of talking to real OLED with node native modules, but the issue is I don't see how to get access to ctx in module code, or anywhere other than in onDraw.

Another question is for things like scrolling, I think I am going to need to be able to set an interval. How does this work in Open Stage Control?

Am I thinking about this all wrong?

ctx on not exposed anywhere besides onDraw, all drawing commands should be performed from this script. Moreover, the custom module doesn't share anything with the client context, both must use osc messages communicate.

If I understand your use case correctly, something like this should do :

  1. send drawing commands from the custom module to the canvas, and wrap arguments in an array so that the canvas always sees the same number of arguments (and set the canva's valueLength to 1):
receive('/canvas_address', ['text', color, x, y, 'hello'])
  1. use the value to define what should be draw in onDraw (and set autoClear to false):
// make sure the value looks like something usable
if (Array.isArray(value)) {
  
  // extract command and args from incoming value
  let [cmd, ...args] = value

  // draw according to instructions
  if (cmd === 'text') {

    let [color, x, y, text] = args
    // ctx.whatever() 

  } else if (cmd === 'clear') {

    ctx.clear()

  } // etc

}
1 Like

Thanks for the reply.

Sidenote: I started implementing the LCD in JS, so it has roughly the same API here. Still needs some work, but it can do the bulk of things I will use it for, and looks pretty realistic (pixelated, only B&W, draw, text, etc.)

If I am understanding right, I need to make a queue in onValue, then draw the queue in onDraw, and I can send commands the LCD by OSC value calls. This makes sense! In your example, I see you are mixing ctx and command-processing similar to how I was originally thinking, which if I am understanding your text, is not viable. Did you mean more like this?:

function onValue(value) {
  if (Array.isArray(value)) {
    this.queue ||= []
    this.queue.push(value)
  }
}

function onDraw() {
  if (this.queue) {
    for (const cmd of this.queue) {
      // TODO run command on ctx here
    } 
    // all items have been drawn, so clear the queue
    this.queue = []
  }
}

Do both callbacks have access to the queue, via this, or do i need to do something with a shared var some other way?

If so, I really like this structure, and it should be really easy to make a self-contained & reusable LCD. So cool!

onDraw is called automatically when a value is received (with value as the latest received value) but you're right a queue will be needed to avoid losing values since onDraw calls are debounced, I forgot to take into account in my previous post.

Do both callbacks have access to the queue, via this , or do i need to do something with a shared var some other way?

JS is slightly twisted in widget script and this does not provide an object reference usable this way, but you can use the locals object to that effect:

// onCreate
local.queue = []

// onValue
if (Array.isArray(value)) {
  locals.queue.push(value)
}
1 Like

Perfect. I was just looking at locals. All this makes sense. Thank you!

Another thing I realized is I can set continuous to true to get a kind of "interval" (for scrolling) so I think a queue will be better, as you said for debouncing, since it's not a 1-to-1 relationship with value-set & draw-commands.

So, I guess I will implement the actual puredata interface in a module, that interacts with real OLED (if available) and takes messages from puredata and turns them into receive calls for the canvas. This should actually be simpler than I thought. This is great!

I think the last thing I need to work through is the actual hardware stuff. I wrote some low-level C stuff originally, but I found oled-i2c-bus which uses i2c-bus, which is a native node module, will hopefully work, we'll see. At very least, it will need to work only in node, on pi, so I bet it will work great, even if not with frontend app thing.