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
lua
andluac
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:
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;
= null;
Serial serialPort
boolean serialActive = false;
ArrayList<Pad> pads = new ArrayList<Pad>();
void setup() {
size(480, 480);
printArray(Serial.list());
.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
byteCodes
for(int r = 0; r < rows; r++){
for(int c = 0; c < columns; c++){
.add(new Pad(c, r, c * columnWidth, r * rowHeight, columnWidth, rowHeight));
pads}
}
noStroke();
fill(255);
stroke(0);
}
void draw() {
background(0);
strokeWeight(2);
for(int i = 0 ; i < pads.size() ; i++){
= pads.get(i);
Pad pad .update().draw();
pad}
if(serialActive){
while (serialPort.available() > 0) {
String inBuffer = serialPort.readString();
if (inBuffer != null) {
println(">>>>> " + inBuffer);
}
}
}
}
void mousePressed() {
= find(mouseX, mouseY);
Pad pad if(pad != null){
println("Clicked pad: " + pad.id());
.click();
pad}else{
//Clicked some other area of the window
}
}
void sendByteCode(int index){
if(serialActive){
print("sendByteCode(): " + index);
byte[] byteCode = byteCodes.get(index);
.write("eval " + byteCode.length + "\n");
serialPort.write(byteCode);
serialPort}
}
void keyPressed() {
if (key == '1') {
.get(0).click();
padssendByteCode(0);
}else if(key == '2'){
.get(1).click();
padssendByteCode(1);
}else if(key == '3'){
.get(2).click();
padssendByteCode(2);
}else if(key == '4'){
.get(3).click();
padssendByteCode(3);
}else if(key == 'q'){
.get(4).click();
padssendByteCode(4);
}else if(key == 'w'){
.get(5).click();
padssendByteCode(5);
}else if(key == 'e'){
.get(6).click();
padssendByteCode(6);
}else if(key == 'r'){
.get(7).click();
padssendByteCode(7);
}else if(key == 'a'){
.get(8).click();
padssendByteCode(8);
}else if(key == 's'){
.get(9).click();
padssendByteCode(9);
}else if(key == 'd'){
.get(10).click();
padssendByteCode(10);
}else if(key == 'f'){
.get(11).click();
padssendByteCode(11);
}else if(key == 'z'){
.get(12).click();
padssendByteCode(12);
}else if(key == 'x'){
.get(13).click();
padssendByteCode(13);
}else if(key == 'c'){
.get(14).click();
padssendByteCode(14);
}else if(key == 'v'){
.get(15).click();
padssendByteCode(15);
}else if(key == 'o'){
println("Starting serial connection...");
= new Serial(this, "/dev/tty.usbmodemPDXX_XXXXX", 115200);
serialPort = true;
serialActive }else if(key == 'p'){
println("Closing serial connection.");
.clear();
serialPort.stop();
serialPort= null;
serialPort = false;
serialActive }
}
find(int x, int y){
Pad
for(int i = 0 ; i < pads.size() ; i++){
= pads.get(i);
Pad pad 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(){
= 0;
g }
update(){
Pad if(g < 255) {
+= 15;
g }
return this;
}
void draw(){
noStroke();
fill(g);
rect(pX, pY, pWidth, pHeight, 12);
}
}