-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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