Skip to content

Instantly share code, notes, and snippets.

@ericlewis
Last active December 29, 2023 10:22
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ericlewis/43d07016275308de11a5519466deea85 to your computer and use it in GitHub Desktop.
Save ericlewis/43d07016275308de11a5519466deea85 to your computer and use it in GitHub Desktop.
Change playerID to 1 or 2 for each Playdate. Supply a Firebase config using `config.js` with real-time database url included.
const drivelist = require('drivelist');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { SerialPort } = require('serialport');
const fs = require('fs-extra');
const POLL_INTERVAL = 1000;
const poll = async ({ fn, validate, interval, maxAttempts }) => {
let attempts = 0;
const executePoll = async (resolve, reject) => {
const result = await fn();
attempts++;
if (validate(result)) {
return resolve(result);
} else if (maxAttempts && attempts === maxAttempts) {
return reject(new Error('Exceeded max attempts'));
} else {
setTimeout(executePoll, interval, resolve, reject);
}
};
return new Promise(executePoll);
};
async function findDisk(type) {
console.log(`finding ${type} disk...`);
const dataDisk = await poll({
fn: async () => {
const drives = await drivelist.list();
const playdate = drives.filter((drive) => drive.description.startsWith("Panic Playdate"))[0];
return playdate;
},
validate: (disk) => !!disk && disk.mountpoints.length > 0,
interval: POLL_INTERVAL
});
return dataDisk;
}
function getMountpoint(disk, label) {
if (!disk.mountpoints || disk.mountpoints.length == 0 || disk.mountpoints[0].label !== label) {
throw Error("Unable to find disk mountpoint")
}
let mountpoint = disk.mountpoints[0].path
if (!mountpoint.endsWith("/" + label)) {
throw Error("Unable to find disk mountpoint")
}
return mountpoint
}
async function ejectDisk(disk) {
let raw = disk.raw
console.log("ejecting disk...");
if (process.platform == "darwin") {
await exec("diskutil eject " + raw);
} else if (process.platform == "linux") {
// TODO: correct way to eject on linux..?
// it seems `eject /dev/sdb` doesn't kick the device back to serial comm mode.
console.log("Please press the A button on the playdate (to end USB file sharing and resume serial communication).")
while (fs.existsSync(raw)) {
await sleep(200)
}
} else {
throw Error("Cannot eject disk on this platform. Please edit index.js.")
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const send_raw = (command, port) => {
return new Promise((resolve, reject) => {
port.write(command, function(err) {
if (err) {
reject(err);
}
});
resolve();
});
}
const send = (command, port) => {
return new Promise((resolve, reject) => {
port.write(command + '\n', function(err) {
if (err) {
reject(err);
}
});
port.on('data', function (data) {
resolve(data);
});
port.on('error', function(err) {
reject(err);
});
});
}
async function connectSerialPort(idx = 0) {
console.log("connecting to serial...");
const port = await poll({
fn: async () => {
const serialPorts = await SerialPort.list();
const playdates = serialPorts.filter(({manufacturer}) => manufacturer === "Panic Inc");
if (playdates) {
const port = new SerialPort({ path: playdates[idx].path, baudRate: 230400 });
return port;
}
},
validate: (port) => !!port,
interval: POLL_INTERVAL
});
await send("echo off", port);
return port;
}
module.exports = {
poll,
findDisk,
ejectDisk,
sleep,
connectSerialPort,
send,
send_raw,
getMountpoint
}
import "CoreLibs/sprites"
local playerID = 1
local playerImage = playdate.graphics.image.new(10, 10, playdate.graphics.kColorBlack)
local player = playdate.graphics.sprite.new(playerImage)
player:moveTo(200, 120)
player:add()
local player2 = playdate.graphics.sprite.new(playerImage)
player2:moveTo(215, 135)
player2:add()
function input(j)
local data = json.decode(j)
if data.player == 1 and playerID ~= data.player then
player:moveTo(data.x, data.y)
end
if data.player == 2 and playerID ~= data.player then
player2:moveTo(data.x, data.y)
end
end
local function report()
if playerID == 1 then
print(json.encode({x = player.x, y = player.y, player = playerID}))
else
print(json.encode({x = player2.x, y = player2.y, player = playerID}))
end
end
function playdate.update()
local current = playdate.getButtonState()
local delta = 3
local dx = 0
local dy = 0
if current == playdate.kButtonLeft then
dx = -delta
elseif current == playdate.kButtonRight then
dx = delta
elseif current == playdate.kButtonUp then
dy = -delta
elseif current == playdate.kButtonDown then
dy = delta
elseif current == playdate.kButtonDown|playdate.kButtonRight then
dy = delta
dx = delta
elseif current == playdate.kButtonDown|playdate.kButtonLeft then
dy = delta
dx = -delta
elseif current == playdate.kButtonUp|playdate.kButtonLeft then
dy = -delta
dx = -delta
elseif current == playdate.kButtonUp|playdate.kButtonRight then
dy = -delta
dx = delta
end
if dy ~= 0 or dx ~= 0 then
if playerID == 1 then
player:moveBy(dx, dy)
else
player2:moveBy(dx, dy)
end
report()
end
playdate.graphics.sprite.update()
end
report()
const { connectSerialPort, send_raw } = require("./common");
const { ReadlineParser } = require('@serialport/parser-readline');
const { initializeApp } = require("firebase/app");
const { getDatabase, ref, set, onValue } = require("firebase/database");
const firebaseConfig = require("./config");
const app = initializeApp(firebaseConfig);
const database = getDatabase(app);
function s2b(str) {
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
function a2h(str) {
var arr1 = [];
for (var n = 0, l = str.length; n < l; n ++) {
var hex = Number(str.charCodeAt(n)).toString(16);
arr1.push(hex);
}
return arr1.join('');
}
function getSize(str) {
return (129 + (str.length / 2)).toString(16);
}
function createPayload(name, arg) {
const pre = "1B4C7561540019930D0A1A0A040404785600000040B943018A40746573742E6C75618080000102854F000000090000008380000042000201440001018204";
const post = "8101000080850100000000808081855F454E56";
const functionName = a2h(name);
const functionNameSize = getSize(functionName);
const string = a2h(arg);
const stringSize = getSize(string);
const b = Buffer.from(pre + functionNameSize + functionName + "04" + stringSize + string + post, 'hex');
return new Uint8Array(b);
}
function evaluate(payload, port) {
const cmd = `eval ${ payload.byteLength }\n`;
const data = new Uint8Array(cmd.length + payload.byteLength);
data.set(s2b(cmd), 0);
data.set(new Uint8Array(payload), cmd.length);
return send_raw(data, port);
}
function handle(obj) {
set(ref(database, 'users/' + obj.player), obj);
}
async function connect(idx) {
let port = await connectSerialPort(idx);
const parser = port.pipe(new ReadlineParser())
parser.on('data', (data) => {
if (data.startsWith("{") && data.endsWith("}")) {
const json = JSON.parse(data);
handle(json);
}
});
onValue(ref(database, '/users/1'), async (snapshot) => {
const data = snapshot.val();
if (data) {
const payload = createPayload("input", JSON.stringify(data));
await evaluate(payload, port);
}
});
onValue(ref(database, '/users/2'), async (snapshot) => {
const data = snapshot.val();
if (data) {
const payload = createPayload("input", JSON.stringify(data));
await evaluate(payload, port);
}
});
}
(async () => {
connect(0);
connect(1);
})();
{
"name": "playdate-multiplayer",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "Eric Lewis",
"license": "MIT",
"dependencies": {
"drivelist": "^9.2.4",
"firebase": "^9.9.1",
"fs-extra": "^10.1.0",
"serialport": "^10.4.0",
"undici": "^5.3.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment