This repository was archived by the owner on Sep 3, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathpdportal.lua
More file actions
347 lines (286 loc) · 11.7 KB
/
Copy pathpdportal.lua
File metadata and controls
347 lines (286 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
import 'CoreLibs/object'
import 'CoreLibs/timer'
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- The PdPortal class is used to communicate from the Playdate back to the
-- browser (and other Playdates). Typically, you'll subclass this
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
class('PdPortal').extends()
local PdPortal <const> = PdPortal
local jsonEncode <const> = json.encode
local jsonDecode <const> = json.decode
local timer <const> = playdate.timer
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- These functions should be implemented by each app, and will be called by the
-- pdportal web app as appropriate.
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- Called by pdportal web app after serial connection established. Example
-- handler: Update UI to show that the serial connection was successful.
function PdPortal:onConnect(portalVersion)
--
end
--- Called by Lua code when serial keepalive fails. Example
-- handler: Update UI to show that the serial has been disconnected.
function PdPortal:onDisconnect()
--
end
--- Peer connection established, remote peer connection possible. Example
-- handler: Update UI to show this Playdate's peer ID. Other users can enter
-- the ID to connect.
-- @see https://peerjs.com/docs/#peeron-open
function PdPortal:onPeerOpen(peerId)
--
end
--- Peer connection destroyed, remote peer connection no longer possible.
-- Example handler: Update UI to show that peer connection is no longer
-- possible. Prompt user to reconnect?
-- @see https://peerjs.com/docs/#peeron-close
function PdPortal:onPeerClose()
--
end
--- A remote peer has connected to us via the peer server. The given
-- `remotePeerId` can be used to send data to this peer with
-- `PdPortal.PortalCommand.SendToPeerConn`. Example handler: Start game!
-- @see https://peerjs.com/docs/#peeron-connection
function PdPortal:onPeerConnection(remotePeerId)
--
end
-- We've connected to a remote peer via the peer server. The given
-- `remotePeerId` can be used to send data to this peer with
-- `PdPortal.PortalCommand.SendToPeerConn`. Example handler: Start game!
-- @see https://peerjs.com/docs/#dataconnection-on-open
function PdPortal:onPeerConnOpen(remotePeerId)
--
end
-- Connection to a remote peer has been lost. Example handler: End game.
-- @see https://peerjs.com/docs/#dataconnection-on-close
function PdPortal:onPeerConnClose(remotePeerId)
--
end
--- We've received data from a remote peer. Example handler: Read `payload` and
-- act appropriately based on the `remotePeerId` and current app state. Note
-- that `payload` is JSON-encoded, so you probably want to
-- `local decodedPayload = json.decode(payload)` before use.
-- @see https://peerjs.com/docs/#dataconnection-on-data
-- @see https://sdk.play.date/inside-playdate#M-json
function PdPortal:onPeerConnData(remotePeerId, payload)
--
end
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- Use these methods to communicate with the pdportal web app and/or other
-- connected Playdates.
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- Commands Playdate can send to the pdportal website
PdPortal.PortalCommand = {
--- Log the given arguments to the browser console
Log = 'l',
--- Keepalive command, automatically sent by the Lua side of pdportal
Keepalive = 'k',
-- Tell pdportal to initialize connection to the peer server for
-- communicating with another Playdate. Not required for `fetch`.
InitializePeer = 'ip',
-- Tell pdportal to disconnect from the peer server.
DestroyPeer = 'dp',
--- Takes two strings, the destination peer ID, and a JSON string. Sends the
-- JSON to that peer.
SendToPeerConn = 'p',
--- Takes one string, the peer ID to close the connection to.
ClosePeerConn = 'cpc',
--- HTTP fetch
Fetch = 'f',
}
--- Send the given PortalCommand and arguments to the pdportal site e.g.
-- `portalInstance:sendCommand(PdPortal.PortalCommand.Log, 'hello', 'world!')`
-- will log those two strings to the browser console.
function PdPortal:sendCommand(portalCommand, ...)
local cmd = {portalCommand}
for i = 1, select('#', ...) do
local arg = select(i, ...)
table.insert(cmd, PdPortal.portalArgumentSeparator)
table.insert(cmd, arg)
end
table.insert(cmd, PdPortal.portalCommandSeparator)
print(table.concat(cmd, ''))
end
--- Convenience method instead of calling
-- `:sendCommand(PdPortal.PortalCommand.Log, 'hello', 'world')`
function PdPortal:log(...)
self:sendCommand(
PdPortal.PortalCommand.Log,
table.unpack({...})
)
end
--- Convenience method instead of calling
-- `:sendCommand(PdPortal.PortalCommand.SendToPeerConn, peerConnId, encodedPayload)`
function PdPortal:sendToPeerConn(peerConnId, payload)
local jsonPayload = jsonEncode(payload)
self:sendCommand(
PdPortal.PortalCommand.SendToPeerConn,
peerConnId,
jsonPayload
)
end
--- Send an HTTP request using `fetch`.
-- @see https://developer.mozilla.org/en-US/docs/Web/API/fetch
function PdPortal:fetch(url, options, successCallback, errorCallback)
local encodedOptions = jsonEncode(options)
local requestId = tostring(self._nextRequestId)
self._nextRequestId += 1
self._fetchCallbacks[requestId] = {successCallback, errorCallback}
self:sendCommand(PdPortal.PortalCommand.Fetch, requestId, url, encodedOptions)
end
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- Most apps should _not_ need to use, override, or change things below here.
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- Things the pdportal site can tell Playdate to do
PdPortal.PlaydateCommand = {
OnConnect = 'oc',
OnPeerConnData = 'opcd',
OnPeerOpen = 'opo',
OnPeerClose = 'opc',
OnPeerConnection = 'opconn',
OnPeerConnOpen = 'opco',
OnPeerConnClose = 'opcc',
Keepalive = 'k',
OnFetchSuccess = 'ofs',
OnFetchError = 'ofe',
}
--- Map of PlaydateCommands to method names that should be called
PdPortal.playdateCommandMethods = {
[PdPortal.PlaydateCommand.OnConnect] = {'_onConnect', 'onConnect'},
[PdPortal.PlaydateCommand.OnPeerConnData] = {'onPeerConnData'},
[PdPortal.PlaydateCommand.OnPeerOpen] = {'onPeerOpen'},
[PdPortal.PlaydateCommand.OnPeerClose] = {'onPeerClose'},
[PdPortal.PlaydateCommand.OnPeerConnection] = {'onPeerConnection'},
[PdPortal.PlaydateCommand.OnPeerConnOpen] = {'onPeerConnOpen'},
[PdPortal.PlaydateCommand.OnPeerConnClose] = {'onPeerConnClose'},
[PdPortal.PlaydateCommand.Keepalive] = {'_onKeepalive'},
[PdPortal.PlaydateCommand.OnFetchSuccess] = {'_onFetchSuccess'},
[PdPortal.PlaydateCommand.OnFetchError] = {'_onFetchError'},
}
--- Sent from Playdate to serial host at the end of each command
PdPortal.portalCommandSeparator = string.char(30) -- RS (␞)
--- Sent from Playdate to serial host between commands and their arguments, and each argument
PdPortal.portalArgumentSeparator = string.char(31) -- US (␟)
--- Sent from serial host to Playdate at the end of each command.
PdPortal.playdateCommandSeparator = '~|~'
PdPortal.playdateCommandPattern = PdPortal.playdateCommandSeparator .. "()"
--- Sent from serial host to Playdate between commands and their arguments, and each argument
PdPortal.playdateArgumentSeparator = '~,~'
PdPortal.playdateArgumentPattern = '(.-)' .. PdPortal.playdateArgumentSeparator .. "()"
-- Partial commands can be sent from serial host to PD due to `msg` length limit. Add to this buffer until a complete command has been received (playdateCommandSeparator)
PdPortal.incomingCommandBuffer = ''
--- The PdPortal class (and by extension, your subclass) should be treated as a
-- singleton. On init, it sets the system `playdate.serialMessageReceived`
-- callback to its own method, replacing any existing handler.
-- @see https://sdk.play.date/2.4.1/Inside%20Playdate.html#c-serialMessageReceived
function PdPortal:init()
playdate.serialMessageReceived = function (msg)
self:_onSerialMessageReceived(msg)
end
self.serialKeepaliveTimer = nil
self._fetchCallbacks = {}
self._nextRequestId = 1
end
function PdPortal:update()
-- Required for serial keepalive
timer.updateTimers()
end
--- Handle input from the `msg` serial command, and distribute appropriately
function PdPortal:_onSerialMessageReceived(msgString)
-- Convert `~n~` back to actual `\n` newlines
msgString = msgString:gsub('~n~', '\n')
local completeCommandStrings, trailingCommand = PdPortal._splitCommandBuffer(
PdPortal.incomingCommandBuffer .. msgString,
PdPortal.playdateCommandPattern
)
PdPortal.incomingCommandBuffer = trailingCommand
for _i, commandString in ipairs(completeCommandStrings) do
local cmdArgs = PdPortal._splitString(commandString, PdPortal.playdateArgumentPattern)
local methodsToCall = PdPortal.playdateCommandMethods[cmdArgs[1]]
if methodsToCall == nil then
self:log('Unknown command received', cmdArgs[1], msgString, string.len(msgString))
return
end
for i, methodName in ipairs(methodsToCall) do
self[methodName](self, table.unpack(cmdArgs, 2))
end
end
end
function PdPortal:_onConnect()
self:_onKeepalive()
end
--- Called by self on connect, then automatically re-triggered by responses from
-- pdportal. This is required so the Playdate can know when it loses connection
-- with the portal.
function PdPortal:_onKeepalive()
-- We've received a keepalive message, cancel the keepalive timer. We live for
-- at least another few hundred milliseconds.
if self.serialKeepaliveTimer ~= nil then
self.serialKeepaliveTimer:pause()
self.serialKeepaliveTimer:remove()
self.serialKeepaliveTimer = nil
end
-- Schedule another keepalive cycle in 500ms
timer.performAfterDelay(500, function ()
-- If pdportal doesn't respond within 100ms, disconnect
self.serialKeepaliveTimer = playdate.timer.new(100, function ()
self:onDisconnect()
end)
self:sendCommand(PdPortal.PortalCommand.Keepalive)
end)
end
function PdPortal:_onFetchSuccess(requestId, responseText, responseDetails)
local callbacks = self._fetchCallbacks[requestId]
if callbacks == nil then
self:log('Success, but no callbacks found for request ID', requestId)
return
end
callbacks[1](responseText, jsonDecode(responseDetails))
self._fetchCallbacks[requestId] = nil
end
function PdPortal:_onFetchError(requestId, errorDetails)
local callbacks = self._fetchCallbacks[requestId]
if callbacks == nil then
self:log('Error, but no callbacks found for request ID', requestId)
return
end
callbacks[2](jsonDecode(errorDetails))
self._fetchCallbacks[requestId] = nil
end
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- Utility functions
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- Splits a string on the given pattern. Returns a table of complete commands, and a string with any trailing incomplete command info
PdPortal._splitCommandBuffer = function(cmdBufferString, pattern)
local completeCommands = {}
local start = 1
local first, last = string.find(cmdBufferString, pattern, start)
local trailingCommand = cmdBufferString
while first do
local command = string.sub(cmdBufferString, start, first - 1)
if command ~= "" then
table.insert(completeCommands, command)
end
start = last + 1
first, last = string.find(cmdBufferString, pattern, start)
end
if start <= string.len(cmdBufferString) then
trailingCommand = string.sub(cmdBufferString, start)
else
trailingCommand = ""
end
return completeCommands, trailingCommand
end
PdPortal._splitString = function(inputStr, pattern)
local result = {}
local lastEnd = 1
for part, endPos in string.gmatch(inputStr, pattern) do
table.insert(result, part)
lastEnd = endPos
end
-- Add the last part if there is any
if lastEnd <= #inputStr then
table.insert(result, string.sub(inputStr, lastEnd))
end
return result
end