-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy path.preload
More file actions
366 lines (329 loc) · 10.3 KB
/
.preload
File metadata and controls
366 lines (329 loc) · 10.3 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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
-- Source code for the tutorial:
-- https://realtimelogic.com/articles/Your-First-Embedded-Single-Page-Application
-- Import and abreviate the string formatting function for later use.
local fmt=string.format
-- Table to keep track of connected SPA clients.
-- Key is the socket ('sock') and the value is a JSONS instance.
-- JSONS: https://realtimelogic.com/ba/doc/?url=lua.html#JSONS
local clients={}
-- Import the Serpent library for pretty-printing tables.
local serpent=require"serpent"
-- Print JSON message: name, payload, where payload is a Lua table
local function printMsg(msg,pl)
tracep(false,5,msg,serpent.block(pl,{comment=false}))
end
-- If the 'esp32' global variable is not defined, simulate the GPIO for ESP32.
-- Used when running on the Mako Server.
if not esp32 then
tracep(false,0,"Installing GPIO ESP32 simulator")
local G={} -- G: GPIO API
G.__index=G
function G:close()
tracep(false,5,"closing GPIO", self.pin)
end
-- Set or get the value of the simulated GPIO.
function G:value(val)
if val ~= nil then
self.state=val
tracep(false,5,"Set GPIO:value",self.pin,val)
return true
end
return self.state
end
-- Create a simulated ESP32 environment.
local esp32={}
_G.esp32=esp32
-- Simulate the GPIO pin creation for ESP32.
function esp32.gpio(pin)
return setmetatable({pin=pin},G)
end
end
-- Send a message to one or all connected clients (SPAs)
-- msg: Message name
-- payload: string or table
-- js: JSONS instance. Used when sending a message to one client.
local function sendMsg(msg, payload, js)
printMsg("Sending: "..msg,payload)
local data={msg,payload}
if js then -- unicast
js:put(data)
else -- multicast
for sock,js in pairs(clients) do
js:put(data)
end
end
end
-- Define the LED configurations used by the SPA for building the LED UI.
local ledsInfo={
{
name="LED1",
id=1,
color="red",
},
{
name="LED2",
id=2,
color="green",
},
{
name="LED3",
id=3,
color="blue",
},
{
name="LED4",
id=4,
color="yellow",
},
}
-- Function to configure a button with an interrupt driven callback.
local function cfgButton(pin,cb)
local cfg={
pulldown=true,
type="POSEDGE",
callback=cb
}
return esp32.gpio(pin,"IN", cfg)
end
-- Table to store LED data. Initialized below.
local ledsData
-- Called when a button is clicked; See ledsData below.
local function buttonClick(index, on)
tracep(false,5,"Button",index)
local ledObj = ledsData[index]
if nil == on then -- if arg 'on' not provided
ledObj.state = not ledObj.state -- toggle
else
ledObj.state = not on -- inverse
end
ledObj.led:value(ledObj.state) -- set LED
sendMsg("setled", {id = index, on = not ledObj.state})
end
local function cfgLed(pin)
local led=esp32.gpio(pin,"OUT")
led:value(true) -- Set high: led off
return led
end
-- Initialize the LED data.
ledsData={
{
state=true,
button=cfgButton(9, function() buttonClick(1) end),
led=cfgLed(1)
},
{
state=true,
button=cfgButton(8, function() buttonClick(2) end),
led=cfgLed(2)
},
{
state=true,
button=cfgButton(7, function() buttonClick(3) end),
led=cfgLed(3)
},
{
state=true,
button=cfgButton(44,function() buttonClick(4) end),
led=cfgLed(4)
},
}
-- Function to get the IO (Input/Output) interface.
-- If the 'mako' global variable is defined, use the 'home' IO,
-- otherwise use the 'disk' IO.
local function getIO()
return mako and ba.openio"home" or ba.openio"disk"
end
-- Function to hash a password using the SHA1 algorithm.
local function hashPwd(pwd)
return ba.crypto.hash"sha1"(pwd)(true)
end
-- Function to retrieve stored credentials.
-- If no credentials are stored, return the default values root/password.
local function getCredentials()
local fp <close> = getIO():open"credentials.json"
if fp then
local cred = ba.json.decode(fp:read"*a")
if cred then return cred.uname,ba.b64decode(cred.pwd) end
end
-- Default
return "root", hashPwd"password"
end
-- AJAX requests managed by server code
-- Tutorial: https://makoserver.net/articles/AJAX-over-WebSockets
local ajaxRequests={
["math/add"]=function(req) return req[1] + req[2] end,
["math/subtract"]=function(req) return req[1] - req[2] end,
["math/mul"]=function(req) return req[1] * req[2] end,
["math/div"]=function(req)
if req[2] == 0 then return nil,"Divide by zero!" end
return req[1] / req[2]
end,
["auth/setcredentials"]=function(req)
local uname, shaPwd = getCredentials()
local reqUname, reqShaPwd = req[1], hashPwd(req[2])
if uname==reqUname and shaPwd==reqShaPwd then
local newUname, newShaPwd = req[3], hashPwd(req[4])
local fp <close> = getIO():open("credentials.json","w")
if fp then
fp:write(ba.json.encode{uname=newUname,pwd=ba.b64encode(newShaPwd)})
return true
end
return nil, "Cannot save credentials!"
end
return nil, "Incorrect account or password"
end,
}
-- Find the AJAX request function in 'ajaxRequests' and call the function
local function ajax(msg)
-- Ajax request name, AJAX response handle, payload
local req,handle,pl=msg[1],msg[2],msg[3]
-- payload is an array with two numbers
if pl[1] == ba.json.null then pl[1]=0 end
if pl[2] == ba.json.null then pl[2]=0 end
-- Lookup and call AJAX function
local rsp,err=ajaxRequests[req](pl)
return "AJAX",{handle, rsp and {rsp=rsp} or {err=err}}
end
-- Called when a user clicks a LED button in the SPA
local function setled(msg)
buttonClick(msg.id, msg.on)
end
-- Table with key (name: string) and value (SPA request handler: function)
-- Used by the "Main loop" below.
local requests={
setled=setled,
AJAX=ajax
}
-- Store the simulated temperature. Initialized in the 'tempManager' function.
local temperature
-- send the current temperature to client(s). All clients if js not provided.
local function sendTemperature(js)
sendMsg("settemp", temperature, js)
end
-- Called when we receive a binary WebSocket frame. Binary messages
-- are used for uploading firmware.
local function recFirmware()
local BinMsg_Upload <const> = 1
local BinMsg_UploadEOF <const> = 2
local io=getIO()
local fp,ok,err,binMsg,frameCounter
local function closeOnErr(js,err)
tracep(false,0,"Firmware upload error:",err)
-- Close socket, see internals in JSONS.lua
js._sock:close()
if fp then fp:close() fp=nil end
end
return function(data,bytesRead,frameLen,js)
if not fp then
tracep(false,5,"Opening FIRMWARE.bin")
fp,err=io:open("FIRMWARE.bin","wb")
if not fp then
return closeOnErr(js,fmt("Cannot open %s: %s",io:realpath("FIRMWARE.bin", err)))
end
frameCounter,binMsg=0,nil
end
-- eom: end of message (websocket frame)
local eom = not bytesRead or bytesRead == frameLen
if not binMsg then
binMsg=data:byte(1)
data=data:sub(2,-1) -- remainder
end
ok,err=fp:write(data)
if not ok then return closeOnErr(js,fmt("Writing failed: %s",err)) end
frameCounter=frameCounter+1
sendMsg("uploadack", frameCounter, js)
if eom then
if BinMsg_UploadEOF == binMsg then
tracep(false,5,"Closing FIRMWARE.bin")
fp:close()
fp=nil
end
binMsg=nil -- Reset
end
end
end
-- Called by code in index.lsp when a new SPA client connects.
function newClient(sock)
tracep(false,5,"New client", sock)
local uname,shaPwd = getCredentials()
-- JSONS: https://realtimelogic.com/ba/doc/?url=lua.html#JSONS
local js,err = require("JSONS").create(sock, {
maxsize=2048,
bincb=recFirmware()
})
-- Designed for receiving JSON with: [message,payload]
-- Returns: message,payload
local function recMsg()
local msg = js:get()
if msg then -- msg is an array, where [1]=message name, [2]=payload
printMsg("Received: "..msg[1],msg[2])
return msg[1],msg[2]
end
end
-- Sent to the SPA when it connects.
local function sendLedInfo()
sendMsg("ledinfo",{leds=ledsInfo}, js)
for ix,ledObj in ipairs(ledsData) do
sendMsg("setled", {id = ix, on = not ledObj.state}, js)
end
end
sendMsg("devname",esp32 and "ESP32" or "simulator") -- First message
local authenticated=false
while not authenticated do -- Authentication loop
local nonce = ba.rndbs(12)
sendMsg("nonce",ba.b64encode(nonce), js)
local msg,payload=recMsg()
if not msg then break end
if "auth" == msg and
payload.name == uname and
payload.hash == ba.crypto.hash"sha1"(shaPwd)(nonce)(true,"hex") then
authenticated=true
sendLedInfo()
sendTemperature(js)
else
nonce = ba.rnds(12)
sendMsg("nonce",ba.b64encode(nonce), js)
end
end
clients[sock]=js -- Register SPA
while true do -- Main loop
local msg,payload=recMsg()
if not msg then break end
msg,payload=requests[msg](payload)
-- Send unicast message if AJAX response
if msg then sendMsg(msg, payload, "AJAX" == msg and js) end
end
clients[sock]=nil -- Deregister SPA
tracep(false,5,"Closing client connection")
end
-- Timer coroutine used for simulating temperature.
local function tempManager()
while true do
temperature=0
while true do
sendTemperature()
temperature = temperature + ba.rnd(5,100)
if temperature > 1000 then break end
coroutine.yield(true)
end
temperature=1000
while true do
sendTemperature()
temperature = temperature - ba.rnd(5,200)
if temperature < 0 then break end
coroutine.yield(true)
end
end
end
tempTimer=ba.timer(tempManager)
tempTimer:set(5000)
-- Clean up resources when the script is unloaded.
function onunload()
for ix,ledObj in ipairs(ledsData) do
ledObj.button:close()
ledObj.led:close()
end
for sock in pairs(clients) do
sock:close()
end
end