diff --git a/firmware/nodemcu-release-16-modules-2021-10-09-22-33-53-float.bin b/firmware/nodemcu-release-16-modules-2021-10-09-22-33-53-float.bin new file mode 100644 index 0000000..9fd21e1 Binary files /dev/null and b/firmware/nodemcu-release-16-modules-2021-10-09-22-33-53-float.bin differ diff --git a/flash.sh b/flash.sh index 8405b61..01636ff 100755 --- a/flash.sh +++ b/flash.sh @@ -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 diff --git a/libs/fifo.lua b/libs/fifo.lua new file mode 100644 index 0000000..d21b1da --- /dev/null +++ b/libs/fifo.lua @@ -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 +} diff --git a/libs/fifosock.lua b/libs/fifosock.lua new file mode 100644 index 0000000..c453fb1 --- /dev/null +++ b/libs/fifosock.lua @@ -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 } diff --git a/libs/fifosocktest.lua b/libs/fifosocktest.lua new file mode 100644 index 0000000..4ac9f21 --- /dev/null +++ b/libs/fifosocktest.lua @@ -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") diff --git a/libs/httpserver.lua b/libs/httpserver.lua new file mode 100644 index 0000000..edab602 --- /dev/null +++ b/libs/httpserver.lua @@ -0,0 +1,217 @@ +------------------------------------------------------------------------------ +-- HTTP server module +-- +-- LICENCE: http://opensource.org/licenses/MIT +-- Vladimir Dronnikov +------------------------------------------------------------------------------ +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 diff --git a/server.lua b/server.lua deleted file mode 100644 index ca2d9d1..0000000 --- a/server.lua +++ /dev/null @@ -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