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.
- Clone the repo: github.com/orllewin/lua54 which is a fork of 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
luaandluacbinaries.
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:
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);
}
}