## Note. deprecated Playdate now has [serialMessageReceived(msg)](https://sdk.play.date/2.5.0/Inside%20Playdate.html#c-serialMessageReceived) 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. * Clone the repo: [github.com/orllewin/lua54](https://github.com/orllewin/lua54) which is a fork of [github.com/scratchminer/lua54](https://github.com/scratchminer/lua54) - they're identical, use either. * Open a terminal and cd to the repo, type `make` * If successful the directory will now contain newly compiled `lua` and `luac` binaries. ## 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: * `echo "serialTap(1,1)" > main.lua` * `./luac -o serial1x1.luac main.lua` 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](https://processing.org): ```java 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); } } ```