Serial Comms

Note. deprecated

Playdate now has serialMessageReceived(msg) which makes this kind of thing much simpler.

About

Playdate has an undocumented USB API with a serial interface.

Here’s some notes on creating a mechanism to control your Playdate creations from another device. To call code within your project you need to use the eval method, this requires sending Lua bytecode, but Playdate uses a custom Lua environment.

Make the Lua Fork

Playdate runs a custom Lua environment, somebody has created a fork of Lua including those changes. In order to generate the Lua bytecode to send to your app/game you need to compile some code using this custom compiler.

Create your own API

You app/game needs some kind of hook, create a function in your Playdate project to call over USB. For my own project I want to control a drum machine, I want to send which column and row indexes the user clicked to the Playdate, so in my project I’ve added a serialTap(c, r) function to main.lua (column and row indexes in a grid). Once that’s ready install on a Playdate - make sure to turn the simulator off after install - you won’t be able to connect to the Playdate from whatever you implement a serial client in if the simulator is running.

Compile some Lua bytecode

With your freshly minted custom Lua compiler generate some bytecode, I created a main.lua file in the same directory as the new lua and luac binaries with nothing more than serialTap(1,1) in:

I repeated this process 16 times for each of my 4x4 cells - though from inspecting the bytecode in a hex editor it should be easy enough to just edit a single byte array.

Use eval to send the bytecode

From whatever environment you’re coding in import the bytecode to a byte array ready to send over the serial interface, pseudo code:

serialPort = Serial("/dev/tty.usbmodemPDXX_XXXXXXXXXX", 115200);
bytes = loadBytes("serial1x1.luac")
serialPort.write("eval " + bytes.length + "\n")
serialPort.write(bytes)

You’ll need the address of your Playdate, it’ll look something like /dev/tty.usbmodemPDXX_X1234567

Processing Example

Proof-of-concept client written in Processing:

import processing.serial.*;

ArrayList<byte[]> byteCodes = new ArrayList<byte[]>();

int w = 480;
int h = 480;
int columns = 4;
int rows = 4;
int columnWidth = w/columns;
int rowHeight = h/rows;

Serial serialPort = null;

boolean serialActive = false;
ArrayList<Pad> pads = new ArrayList<Pad>();

void setup() {
  size(480, 480);
  printArray(Serial.list());

  byteCodes.add(loadBytes("serial1x1.luac"));//1
  byteCodes.add(loadBytes("serial2x1.luac"));//2
  byteCodes.add(loadBytes("serial3x1.luac"));//3
  byteCodes.add(loadBytes("serial4x1.luac"));//4
  
  byteCodes.add(loadBytes("serial1x2.luac"));//q
  byteCodes.add(loadBytes("serial2x2.luac"));//w
  byteCodes.add(loadBytes("serial3x2.luac"));//e
  byteCodes.add(loadBytes("serial4x2.luac"));//r
  
  byteCodes.add(loadBytes("serial1x3.luac"));//a
  byteCodes.add(loadBytes("serial2x3.luac"));//s
  byteCodes.add(loadBytes("serial3x3.luac"));//d
  byteCodes.add(loadBytes("serial4x3.luac"));//f
  
  byteCodes.add(loadBytes("serial1x4.luac"));//z
  byteCodes.add(loadBytes("serial2x4.luac"));//x
  byteCodes.add(loadBytes("serial3x4.luac"));//c
  byteCodes.add(loadBytes("serial4x4.luac"));//v

  for(int r = 0; r < rows; r++){
    for(int c = 0; c < columns; c++){
      pads.add(new Pad(c, r, c * columnWidth, r * rowHeight, columnWidth, rowHeight));
    }
  }
  
  noStroke();
  fill(255);
  stroke(0);
}

void draw() {
  background(0);
  
  strokeWeight(2);
  
  for(int i = 0 ; i < pads.size() ; i++){
    Pad pad = pads.get(i);
    pad.update().draw();
  }
  
  if(serialActive){
    while (serialPort.available() > 0) {
      String inBuffer = serialPort.readString();   
      if (inBuffer != null) {
        println(">>>>> " + inBuffer);
      }
    }
  }
}

void mousePressed() {
  Pad pad = find(mouseX, mouseY);
  if(pad != null){
    println("Clicked pad: " + pad.id());
    pad.click();
  }else{
    //Clicked some other area of the window
  }
}

void sendByteCode(int index){
  if(serialActive){
    print("sendByteCode(): " + index);
    byte[] byteCode = byteCodes.get(index);
    serialPort.write("eval " + byteCode.length + "\n");
    serialPort.write(byteCode);
  }
}

void keyPressed() {
  if (key == '1') {
    pads.get(0).click();
    sendByteCode(0);
  }else if(key == '2'){
    pads.get(1).click();
    sendByteCode(1);
  }else if(key == '3'){
    pads.get(2).click();
    sendByteCode(2);
  }else if(key == '4'){
    pads.get(3).click();
    sendByteCode(3);
  }else if(key == 'q'){
    pads.get(4).click();
    sendByteCode(4);
  }else if(key == 'w'){
    pads.get(5).click();
    sendByteCode(5);
  }else if(key == 'e'){
    pads.get(6).click();
    sendByteCode(6);
  }else if(key == 'r'){
    pads.get(7).click();
    sendByteCode(7);
  }else if(key == 'a'){
    pads.get(8).click();
    sendByteCode(8);
  }else if(key == 's'){
    pads.get(9).click();
    sendByteCode(9);
  }else if(key == 'd'){
    pads.get(10).click();
    sendByteCode(10);
  }else if(key == 'f'){
    pads.get(11).click();
    sendByteCode(11);
  }else if(key == 'z'){
    pads.get(12).click();
    sendByteCode(12);
  }else if(key == 'x'){
    pads.get(13).click();
    sendByteCode(13);
  }else if(key == 'c'){
    pads.get(14).click();
    sendByteCode(14);
  }else if(key == 'v'){
    pads.get(15).click();
    sendByteCode(15);
  }else if(key == 'o'){
    println("Starting serial connection...");
    serialPort = new Serial(this, "/dev/tty.usbmodemPDXX_XXXXX", 115200);
    serialActive = true;
  }else if(key == 'p'){
    println("Closing serial connection.");
    serialPort.clear();
    serialPort.stop();
    serialPort = null;
    serialActive = false;
  }
}

Pad find(int x, int y){
  
  for(int i = 0 ; i < pads.size() ; i++){
    Pad pad = pads.get(i);
    if(pad.hit(x, y)){
      return pad;
    }
  }
  
  return null;
}

class Pad{
  int column, row, pX, pY, pWidth, pHeight;
  
  int g = 255;
  
  public Pad(int column, int row, int pX, int pY, int pWidth, int pHeight){
    this.column = column;
    this.row = row;
    this.pX = pX;
    this.pY = pY;
    this.pWidth = pWidth;
    this.pHeight = pHeight;
  }
  
  String id(){
    return "" + column + "x" + row;
  }
  
  boolean hit(int x, int y){
    return x > pX && x < pX + pWidth && y > pY && y < pY + pHeight;
  }
  
  void click(){
    g = 0;
  }
  
  Pad update(){
    if(g < 255) {
      g += 15;
    }
    return this;
  }
  
  void draw(){
    noStroke();
    fill(g);
    rect(pX, pY, pWidth, pHeight, 12);
  }
}