7 changed files with 544 additions and 60 deletions
Binary file not shown.
@ -1,12 +1,11 @@ |
|||
nodemcu-tool reset |
|||
esptool.py erase_flash |
|||
esptool.py --port /dev/ttyUSB0 write_flash -fm qio 0x00000 nodemcu-release-16-modules-2021-10-09-22-33-53-float.bin |
|||
esptool.py --port /dev/ttyUSB0 write_flash -fm qio 0x00000 firmware/nodemcu-release-16-modules-2021-10-09-22-33-53-float.bin |
|||
|
|||
|
|||
nodemcu-tool upload *.lua |
|||
nodemcu-tool upload *.lua libs/* |
|||
|
|||
while [[ $? != 0 ]]; do |
|||
nodemcu-tool upload *.lua |
|||
nodemcu-tool upload *.lua libs/* |
|||
done |
|||
|
|||
echo 'dofile("init.lua")' | nodemcu-tool terminal |
|||
|
@ -0,0 +1,45 @@ |
|||
-- A generic fifo module. See docs/lua-modules/fifo.md for use examples. |
|||
|
|||
local tr, ti = table.remove, table.insert |
|||
|
|||
-- Remove an element and pass it to k, together with a boolean indicating that |
|||
-- this is the last element in the queue; if that returns a value, leave that |
|||
-- pending at the top of the fifo. |
|||
-- |
|||
-- If k returns nil, the fifo will be advanced. Moreover, k may return a |
|||
-- second result, a boolean, indicating "phantasmic" nature of this element. |
|||
-- If this boolean is true, then the fifo will advance again, passing the next |
|||
-- value, if there is one, to k, or priming itself for immediate execution at |
|||
-- the next call to queue. |
|||
-- |
|||
-- If the queue is empty, do not invoke k but flag it to enable immediate |
|||
-- execution at the next call to queue. |
|||
-- |
|||
-- Returns 'true' if the queue contained at least one non-phantom entry, |
|||
-- 'false' otherwise. |
|||
local function dequeue(q,k) |
|||
if #q > 0 |
|||
then |
|||
local new, again = k(q[1], #q == 1) |
|||
if new == nil |
|||
then tr(q,1) |
|||
if again then return dequeue(q, k) end -- note tail call |
|||
else q[1] = new |
|||
end |
|||
return true |
|||
else q._go = true ; return false |
|||
end |
|||
end |
|||
|
|||
-- Queue a on queue q and dequeue with `k` if the fifo had previously emptied. |
|||
local function queue(q,a,k) |
|||
ti(q,a) |
|||
if k ~= nil and q._go then q._go = false; dequeue(q, k) end |
|||
end |
|||
|
|||
-- return a table containing just the FIFO constructor |
|||
return { |
|||
['new'] = function() |
|||
return { ['_go'] = true ; ['queue'] = queue ; ['dequeue'] = dequeue } |
|||
end |
|||
} |
@ -0,0 +1,134 @@ |
|||
-- Wrap a two-staged fifo around a socket's send; see |
|||
-- docs/lua-modules/fifosock.lua for more documentation. |
|||
-- |
|||
-- See fifosocktest.lua for some examples of use or tricky cases. |
|||
-- |
|||
-- Our fifos can take functions; these can be useful for either lazy |
|||
-- generators or callbacks for parts of the stream having been sent. |
|||
|
|||
local BIGTHRESH = 256 -- how big is a "big" string? |
|||
local SPLITSLOP = 16 -- any slop in the big question? |
|||
local FSMALLLIM = 32 -- maximum number of small strings held |
|||
local COALIMIT = 3 |
|||
|
|||
local concat = table.concat |
|||
local insert = table.insert |
|||
local gc = collectgarbage |
|||
|
|||
local function wrap(sock) |
|||
-- the two fifos |
|||
local fsmall, lsmall, fbig = {}, 0, (require "fifo").new() |
|||
|
|||
-- ssend last aggregation string and aggregate count |
|||
local ssla, sslan = nil, 0 |
|||
local ssend = function(s,islast) |
|||
local ns = nil |
|||
|
|||
-- Optimistically, try coalescing FIFO dequeues. But, don't try to |
|||
-- coalesce function outputs, since functions might be staging their |
|||
-- execution on the send event implied by being called. |
|||
|
|||
if type(s) == "function" then |
|||
if sslan ~= 0 then |
|||
sock:send(ssla) |
|||
ssla, sslan = nil, 0; gc() |
|||
return s, false -- stay as is and wait for :on("sent") |
|||
end |
|||
s, ns = s() |
|||
elseif type(s) == "string" and sslan < COALIMIT then |
|||
if sslan == 0 |
|||
then ssla, sslan = s, 1 |
|||
else ssla, sslan = ssla .. s, sslan + 1 |
|||
end |
|||
if islast then |
|||
-- this is shipping; if there's room, steal the small fifo, too |
|||
if sslan < COALIMIT then |
|||
sock:send(ssla .. concat(fsmall)) |
|||
fsmall, lsmall = {}, 0 |
|||
else |
|||
sock:send(ssla) |
|||
end |
|||
ssla, sslan = "", 0; gc() |
|||
return nil, false |
|||
else |
|||
return nil, true |
|||
end |
|||
end |
|||
|
|||
-- Either that was a function or we've hit our coalescing limit or |
|||
-- we didn't ship above. Ship now, if there's something to ship. |
|||
if s ~= nil then |
|||
if sslan == 0 then sock:send(s) else sock:send(ssla .. s) end |
|||
ssla, sslan = nil, 0; gc() |
|||
return ns or nil, false |
|||
elseif sslan ~= 0 then |
|||
assert (ns == nil) |
|||
sock:send(ssla) |
|||
ssla, sslan = nil, 0; gc() |
|||
return nil, false |
|||
else |
|||
assert (ns == nil) |
|||
return nil, true |
|||
end |
|||
end |
|||
|
|||
-- Move fsmall to fbig; might send if fbig empty |
|||
local function promote(f) |
|||
if #fsmall == 0 then return end |
|||
local str = concat(fsmall) |
|||
fsmall, lsmall = {}, 0 |
|||
fbig:queue(str, f or ssend) |
|||
end |
|||
|
|||
local function sendnext() |
|||
if not fbig:dequeue(ssend) then promote() end |
|||
end |
|||
|
|||
sock:on("sent", sendnext) |
|||
|
|||
return function(s) |
|||
-- don't sweat the petty things |
|||
if s == nil or s == "" then return end |
|||
|
|||
-- Function? Go ahead and queue this thing in the right place. |
|||
if type(s) == "function" then promote(); fbig:queue(s, ssend); return; end |
|||
|
|||
s = tostring(s) |
|||
|
|||
-- cork sending until the end in case we're the head of line |
|||
local corked = false |
|||
local function corker(t) corked = true; return t end |
|||
|
|||
-- small fifo would overfill? promote it |
|||
if lsmall + #s > BIGTHRESH or #fsmall >= FSMALLLIM then promote(corker) end |
|||
|
|||
-- big string? chunk and queue big components immediately |
|||
-- behind any promotion that just took place |
|||
while #s > BIGTHRESH + SPLITSLOP do |
|||
local pfx |
|||
pfx, s = s:sub(1,BIGTHRESH), s:sub(BIGTHRESH+1) |
|||
fbig:queue(pfx, corker) |
|||
end |
|||
|
|||
-- Big string? queue and maybe tx now |
|||
if #s > BIGTHRESH then fbig:queue(s, corker) |
|||
-- small and fifo in immediate dequeue mode |
|||
elseif fbig._go and lsmall == 0 then fbig:queue(s, corker) |
|||
-- small and queue already moving; let it linger in the small fifo |
|||
else insert(fsmall, s) ; lsmall = lsmall + #s |
|||
end |
|||
|
|||
-- if it happened that we corked the transmission above... |
|||
-- if we queued a good amount of data, go ahead and start transmitting; |
|||
-- otherwise, wait a tick and hopefully we will queue more in the interim |
|||
-- before transmitting. |
|||
if corked then |
|||
if #fbig <= COALIMIT |
|||
then tmr.create():alarm(1, tmr.ALARM_SINGLE, sendnext) |
|||
else sendnext() |
|||
end |
|||
end |
|||
end |
|||
end |
|||
|
|||
return { wrap = wrap } |
@ -0,0 +1,145 @@ |
|||
-- |
|||
-- Set verbose to 0 for quiet output (either the first assertion failure or |
|||
-- "All tests OK"), to 1 to see the events ("SEND", "SENT", "CHECK") without |
|||
-- the actual bytes, or to 2 to see the events with the bytes. |
|||
-- |
|||
local verbose = 0 |
|||
|
|||
local vprint = (verbose > 0) and print or function() end |
|||
|
|||
-- |
|||
-- Mock up enough of the nodemcu tmr structure, but pretend that nothing |
|||
-- happens between ticks. This won't exercise the optimistic corking logic, |
|||
-- but that's probably fine. |
|||
-- luacheck: push ignore |
|||
tmr = {} |
|||
tmr.ALARM_SINGLE = 0 |
|||
function tmr.create() |
|||
local r = {} |
|||
function r:alarm(_i, _t, cb) vprint("TMR") cb() end |
|||
return r |
|||
end |
|||
-- luacheck: pop |
|||
|
|||
-- |
|||
-- Mock up enough of the nodemcu net.socket type; have it log all the sends |
|||
-- into this "outs" array so that we can later check against it. |
|||
-- |
|||
local outs = {} |
|||
local fakesock = { |
|||
cb = nil, |
|||
on = function(this, _, cb) this.cb = cb end, |
|||
send = function(this, s) vprint("SEND", (verbose > 1) and s) table.insert(outs, s) end -- luacheck: no unused |
|||
} |
|||
local function sent() vprint("SENT") fakesock.cb() end |
|||
|
|||
-- And wrap a fifosock around this fake socket |
|||
local fsend = (require "fifosock").wrap(fakesock) |
|||
|
|||
-- Verify that the next unconsumed output is as indicated |
|||
local function fcheck(x) |
|||
vprint ("CHECK", (verbose > 1) and x) |
|||
assert (#outs > 0) |
|||
assert (x == outs[1]) |
|||
table.remove(outs, 1) |
|||
end |
|||
|
|||
-- Enqueue an empty function to prevent coalescing within the fifosock |
|||
local function nocoal() fsend(function() return nil end) end |
|||
|
|||
-- Send and check, for when the string should be sent exactly as is |
|||
local function fsendc(x) fsend(x) fcheck(x) end |
|||
|
|||
-- Check that there are no more outputs |
|||
local function fchecke() vprint("CHECKE") assert (#outs == 0) end |
|||
|
|||
-- |
|||
-- And now for the tests, which start easy and grow in complexity |
|||
-- |
|||
|
|||
fsendc("abracadabra none") |
|||
sent() ; fchecke() |
|||
|
|||
fsendc("abracadabra three") |
|||
fsend("short") |
|||
fsend("string") |
|||
fsend("build") |
|||
sent() ; fcheck("shortstringbuild") |
|||
sent() ; fchecke() |
|||
|
|||
-- Hit default FSMALLLIM while building up |
|||
fsendc("abracadabra lots small") |
|||
for i = 1, 32 do fsend("a") end -- luacheck: no unused |
|||
nocoal() |
|||
for i = 1, 4 do fsend("a") end -- luacheck: no unused |
|||
sent() ; fcheck(string.rep("a", 32)) |
|||
sent() ; fcheck(string.rep("a", 4)) |
|||
sent() ; fchecke() |
|||
|
|||
-- Hit string length while building up |
|||
fsendc("abracadabra overlong") |
|||
for i = 1, 10 do fsend(string.rep("a",32)) end -- luacheck: no unused |
|||
sent() ; fcheck(string.rep("a", 320)) |
|||
sent() ; fchecke() |
|||
|
|||
-- Hit neither before sending a big string |
|||
fsendc("abracadabra mid long") |
|||
for i = 1, 6 do fsend(string.rep("a",32)) end -- luacheck: no unused |
|||
fsend(string.rep("b", 256)) |
|||
nocoal() |
|||
for i = 1, 6 do fsend(string.rep("c",32)) end -- luacheck: no unused |
|||
sent() ; fcheck(string.rep("a", 192) .. string.rep("b", 256)) |
|||
sent() ; fcheck(string.rep("c", 192)) |
|||
sent() ; fchecke() |
|||
|
|||
-- send a huge string, verify that it coalesces |
|||
fsendc(string.rep("a",256) .. string.rep("b", 256) .. string.rep("c", 260)) |
|||
sent() ; fchecke() |
|||
|
|||
-- send a huge string, verify that it coalesces save for the short bit at the end |
|||
fsend(string.rep("a",256) .. string.rep("b", 256) .. string.rep("c", 256) .. string.rep("d",256)) |
|||
fsend("e") |
|||
fcheck(string.rep("a",256) .. string.rep("b", 256) .. string.rep("c", 256)) |
|||
sent() ; fcheck(string.rep("d",256) .. "e") |
|||
sent() ; fchecke() |
|||
|
|||
-- send enough that our 4x lookahead still leaves something in the queue |
|||
fsend(string.rep("a",512) .. string.rep("b", 512) .. string.rep("c", 512)) |
|||
fcheck(string.rep("a",512) .. string.rep("b", 512)) |
|||
sent() ; fcheck(string.rep("c",512)) |
|||
sent() ; fchecke() |
|||
|
|||
-- test a lazy generator |
|||
do |
|||
local ix = 0 |
|||
local function gen() vprint("GEN", ix); ix = ix + 1; return ("a" .. ix), ix < 3 and gen end |
|||
fsend(gen) |
|||
fsend("b") |
|||
fcheck("a1") |
|||
sent() ; fcheck("a2") |
|||
sent() ; fcheck("a3") |
|||
sent() ; fcheck("b") |
|||
sent() ; fchecke() |
|||
end |
|||
-- test a completion-like callback that does send text |
|||
do |
|||
local ix = 0 |
|||
local function gen() vprint("GEN"); ix = 1; return "efgh", nil end |
|||
fsend("abcd"); fsend(gen); fsend("ijkl") |
|||
assert (ix == 0) |
|||
fcheck("abcd"); assert (ix == 0) |
|||
sent() ; fcheck("efgh"); assert (ix == 1); ix = 0 |
|||
sent() ; fcheck("ijkl"); assert (ix == 0) |
|||
sent() ; fchecke() |
|||
end |
|||
-- and one that doesn't |
|||
do |
|||
local ix = 0 |
|||
local function gen() vprint("GEN"); ix = 1; return nil, nil end |
|||
fsend("abcd"); fsend(gen); fsend("ijkl") |
|||
assert (ix == 0) |
|||
fcheck("abcd"); assert (ix == 0) |
|||
sent() ; fcheck("ijkl"); assert (ix == 1); ix = 0 |
|||
sent() ; fchecke() ; assert (ix == 0) |
|||
end |
|||
print("All tests OK") |
@ -0,0 +1,217 @@ |
|||
------------------------------------------------------------------------------ |
|||
-- HTTP server module |
|||
-- |
|||
-- LICENCE: http://opensource.org/licenses/MIT |
|||
-- Vladimir Dronnikov <dronnikov@gmail.com> |
|||
------------------------------------------------------------------------------ |
|||
local collectgarbage, tonumber, tostring = collectgarbage, tonumber, tostring |
|||
|
|||
local http |
|||
do |
|||
------------------------------------------------------------------------------ |
|||
-- request methods |
|||
------------------------------------------------------------------------------ |
|||
local make_req = function(conn, method, url) |
|||
return { |
|||
conn = conn, |
|||
method = method, |
|||
url = url, |
|||
} |
|||
end |
|||
|
|||
------------------------------------------------------------------------------ |
|||
-- response methods |
|||
------------------------------------------------------------------------------ |
|||
local make_res = function(csend, cfini) |
|||
local send = function(self, data, status) |
|||
-- TODO: req.send should take care of response headers! |
|||
if self.send_header then |
|||
csend("HTTP/1.1 ") |
|||
csend(tostring(status or 200)) |
|||
-- TODO: real HTTP status code/name table |
|||
csend(" OK\r\n") |
|||
-- we use chunked transfer encoding, to not deal with Content-Length: |
|||
-- response header |
|||
self:send_header("Transfer-Encoding", "chunked") |
|||
-- TODO: send standard response headers, such as Server:, Date: |
|||
end |
|||
if data then |
|||
-- NB: no headers allowed after response body started |
|||
if self.send_header then |
|||
self.send_header = nil |
|||
-- end response headers |
|||
csend("\r\n") |
|||
end |
|||
-- chunked transfer encoding |
|||
csend(("%X\r\n"):format(#data)) |
|||
csend(data) |
|||
csend("\r\n") |
|||
end |
|||
end |
|||
|
|||
local send_header = function(_, name, value) |
|||
-- NB: quite a naive implementation |
|||
csend(name) |
|||
csend(": ") |
|||
csend(value) |
|||
csend("\r\n") |
|||
end |
|||
-- finalize request, optionally sending data |
|||
local finish = function(self, data, status) |
|||
-- NB: res.send takes care of response headers |
|||
if data then |
|||
self:send(data, status) |
|||
end |
|||
-- finalize chunked transfer encoding |
|||
csend("0\r\n\r\n") |
|||
-- close connection |
|||
cfini() |
|||
end |
|||
-- |
|||
local res = { } |
|||
res.send_header = send_header |
|||
res.send = send |
|||
res.finish = finish |
|||
return res |
|||
end |
|||
|
|||
------------------------------------------------------------------------------ |
|||
-- HTTP parser |
|||
------------------------------------------------------------------------------ |
|||
local http_handler = function(handler) |
|||
return function(conn) |
|||
local csend = (require "fifosock").wrap(conn) |
|||
|
|||
local req, res |
|||
local buf = "" |
|||
local method, url |
|||
|
|||
local ondisconnect = function(connection) |
|||
connection:on("receive", nil) |
|||
connection:on("disconnection", nil) |
|||
connection:on("sent", nil) |
|||
collectgarbage("collect") |
|||
end |
|||
|
|||
local cfini = function() |
|||
csend(function() |
|||
conn:on("sent", nil) |
|||
conn:close() |
|||
ondisconnect(conn) |
|||
end) |
|||
end |
|||
|
|||
|
|||
-- header parser |
|||
local cnt_len = 0 |
|||
|
|||
local onheader = function(_, k, v) |
|||
-- TODO: look for Content-Type: header |
|||
-- to help parse body |
|||
-- parse content length to know body length |
|||
if k == "content-length" then |
|||
cnt_len = tonumber(v) |
|||
end |
|||
if k == "expect" and v == "100-continue" then |
|||
csend("HTTP/1.1 100 Continue\r\n") |
|||
end |
|||
-- delegate to request object |
|||
if req and req.onheader then |
|||
req:onheader(k, v) |
|||
end |
|||
end |
|||
|
|||
-- body data handler |
|||
local body_len = 0 |
|||
local ondata = function(_, chunk) |
|||
-- feed request data to request handler |
|||
if not req or not req.ondata then return end |
|||
req:ondata(chunk) |
|||
-- NB: once length of seen chunks equals Content-Length: |
|||
-- ondata(conn) is called |
|||
body_len = body_len + #chunk |
|||
-- print("-B", #chunk, body_len, cnt_len, node.heap()) |
|||
if body_len >= cnt_len then |
|||
req:ondata() |
|||
end |
|||
end |
|||
|
|||
local onreceive = function(connection, chunk) |
|||
-- merge chunks in buffer |
|||
if buf then |
|||
buf = buf .. chunk |
|||
else |
|||
buf = chunk |
|||
end |
|||
-- consume buffer line by line |
|||
while #buf > 0 do |
|||
-- extract line |
|||
local e = buf:find("\r\n", 1, true) |
|||
if not e then break end |
|||
local line = buf:sub(1, e - 1) |
|||
buf = buf:sub(e + 2) |
|||
-- method, url? |
|||
if not method then |
|||
do |
|||
local _ |
|||
-- NB: just version 1.1 assumed |
|||
_, _, method, url = line:find("^([A-Z]+) (.-) HTTP/1.1$") |
|||
end |
|||
if method then |
|||
-- make request and response objects |
|||
req = make_req(connection, method, url) |
|||
res = make_res(csend, cfini) |
|||
end |
|||
-- spawn request handler |
|||
handler(req, res) |
|||
-- header line? |
|||
elseif #line > 0 then |
|||
-- parse header |
|||
local _, _, k, v = line:find("^([%w-]+):%s*(.+)") |
|||
-- header seems ok? |
|||
if k then |
|||
k = k:lower() |
|||
onheader(connection, k, v) |
|||
end |
|||
-- headers end |
|||
else |
|||
-- NB: we explicitly reassign receive handler so that |
|||
-- next received chunks go directly to body handler |
|||
connection:on("receive", ondata) |
|||
-- NB: we feed the rest of the buffer as starting chunk of body |
|||
ondata(connection, buf) |
|||
-- buffer no longer needed |
|||
buf = nil |
|||
-- parser done |
|||
break |
|||
end |
|||
end |
|||
end |
|||
|
|||
conn:on("receive", onreceive) |
|||
conn:on("disconnection", ondisconnect) |
|||
end |
|||
end |
|||
|
|||
------------------------------------------------------------------------------ |
|||
-- HTTP server |
|||
------------------------------------------------------------------------------ |
|||
local srv |
|||
local createServer = function(port, handler) |
|||
-- NB: only one server at a time |
|||
if srv then srv:close() end |
|||
srv = net.createServer(net.TCP, 15) |
|||
-- listen |
|||
srv:listen(port, http_handler(handler)) |
|||
return srv |
|||
end |
|||
|
|||
------------------------------------------------------------------------------ |
|||
-- HTTP server methods |
|||
------------------------------------------------------------------------------ |
|||
http = { |
|||
createServer = createServer, |
|||
} |
|||
end |
|||
|
|||
return http |
@ -1,56 +0,0 @@ |
|||
temperature = "0" |
|||
|
|||
require("httpserver").createServer(80, function(req, res) |
|||
-- analyse method and url |
|||
print("+R", req.method, req.url, node.heap()) |
|||
-- setup handler of headers, if any |
|||
req.onheader = function(self, name, value) -- luacheck: ignore |
|||
print("+H", name, value) |
|||
-- E.g. look for "content-type" header, |
|||
-- setup body parser to particular format |
|||
-- if name == "content-type" then |
|||
-- if value == "application/json" then |
|||
-- req.ondata = function(self, chunk) ... end |
|||
-- elseif value == "application/x-www-form-urlencoded" then |
|||
-- req.ondata = function(self, chunk) ... end |
|||
-- end |
|||
-- end |
|||
end |
|||
-- setup handler of body, if any |
|||
req.ondata = function(self, chunk) -- luacheck: ignore |
|||
print("+B", chunk and #chunk, node.heap()) |
|||
--if not chunk then |
|||
-- reply |
|||
res:send(nil, 200) |
|||
-- res:send_header("Connection", "close") |
|||
-- res:send("Hello, world!\n") |
|||
--res:finish("yolo") |
|||
--end |
|||
|
|||
res:finish(temperature .. "\n") |
|||
end |
|||
-- or just do something not waiting till body (if any) comes |
|||
--res:finish("Hello, world!") |
|||
--res:finish("Salut, monde!") |
|||
end) |
|||
|
|||
print("Starting up") |
|||
|
|||
for k, v in pairs(softuart) do |
|||
print(tostring(k) .. ", " .. tostring(v)) |
|||
end |
|||
|
|||
if s then |
|||
print("Configuring callback") |
|||
s:on("data", "\n", |
|||
function(data) |
|||
stripped = string.gsub(data, '%s+', '') |
|||
print("Temperature = " .. stripped) |
|||
s:write("Sending back to cp: \n") |
|||
print("Wrote stuff back") |
|||
temperature = stripped |
|||
if data == "quit" then |
|||
s:on("data") -- unregister callback function |
|||
end |
|||
end, 0) |
|||
end |
Loading…
Reference in new issue