commit 0b71f3701452e8c2789391abad4d45bad468b12d Author: Wesley Kerfoot Date: Mon Nov 21 01:52:51 2022 -0500 LFS related updates diff --git a/.nodemcutool b/.nodemcutool new file mode 100644 index 0000000..361f6b5 --- /dev/null +++ b/.nodemcutool @@ -0,0 +1,7 @@ +{ + "baudrate": "115200", + "port": "/dev/ttyUSB0", + "minify": false, + "compile": false, + "keeppath": false +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..974cbbd --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +Lua code, scripts, and firmware for my smart light project. + +## How to use +You can flash this to your esp8266 (with nodemcu on it), change the pin numbers correspondingly, and it should work. + +It has a relay attached to it that switches a light on/off depending on the voltage, as well as a temperature sensor connected to the ADC (analog-digital converter) pin, which is used to handle motion detection using a sonar sensor. You can modify the code according to whatever hardware you have. + +The main useful thing here is the code for getting reliable sonar readings, which take several samples and average them, and use the standard error to try and make it more accurate. It also uses the temperature to calculate the speed of sound, which varies depending on the temperature and air humidity. + +Hardware used: + +* https://www.adafruit.com/product/3942 +* https://learn.adafruit.com/adafruit-power-relay-featherwing +* https://www.nodemcu.com/index_en.html +* https://www.adafruit.com/product/165 diff --git a/firmware/0x00000.bin b/firmware/0x00000.bin new file mode 100644 index 0000000..cb33bb9 Binary files /dev/null and b/firmware/0x00000.bin differ diff --git a/firmware/0x10000.bin b/firmware/0x10000.bin new file mode 100644 index 0000000..47c3020 Binary files /dev/null and b/firmware/0x10000.bin differ diff --git a/flash.sh b/flash.sh new file mode 100755 index 0000000..9b7bd03 --- /dev/null +++ b/flash.sh @@ -0,0 +1,28 @@ +rm firmware/*bin +cp /home/wes/code/nodemcu-firmware/bin/*bin ./firmware +rm -f sources/lfs.img +cd sources && ./compile.sh +cd ../ + +nodemcu-tool reset +function reset_flash() { + nodemcu-tool reset + esptool.py --port /dev/ttyUSB0 erase_flash + esptool.py --port /dev/ttyUSB0 write_flash -fm dio 0x00000 firmware/0x00000.bin + esptool.py --port /dev/ttyUSB0 write_flash -fm dio 0x10000 firmware/0x10000.bin +} + +reset_flash + +CONN_DELAY=1000 + +nodemcu-tool --connection-delay $CONN_DELAY remove lfs.img +nodemcu-tool --connection-delay $CONN_DELAY upload sources/lfs.img + +while [[ $? != 0 ]]; do + nodemcu-tool --connection-delay $CONN_DELAY upload sources/lfs.img +done +# +echo 'print(node.LFS.reload("lfs.img"))' | nodemcu-tool --connection-delay $CONN_DELAY terminal +nodemcu-tool --connection-delay $CONN_DELAY remove lfs.img +nodemcu-terminal diff --git a/sources/_init.lua b/sources/_init.lua new file mode 100644 index 0000000..770294a --- /dev/null +++ b/sources/_init.lua @@ -0,0 +1,252 @@ +-- Never change these unless the board changes +red = 7 +green = 5 + +pins = {toggle_red=red, toggle_green=green} +lights = {} +lights[green] = false +lights[red] = false + + +function display_table(t) + print("table") + for k, v in pairs(t) do + print("k = " .. tostring(k), ", v = " .. tostring(v)) + end +end + +function calc_duty_cycle(duty_cycle_factor) + if duty_cycle_factor <= 1 then + return 1023 + else + return 1023 / duty_cycle_factor + end +end + +function make_timer(pin, interval, initial_duty_cycle) + -- interval is in milliseconds + -- pin is the gpio number + local duty_cycle_t = initial_duty_cycle + local direction = 1 + local delay = 0 + print("timer created!") + return tmr.create():alarm(interval, tmr.ALARM_AUTO, function() + if delay > 0 then + delay = delay - 1 + return + end + + if duty_cycle_t >= 1023 then + delay = 200 + direction = -1 + elseif duty_cycle_t <= 0 then + delay = 200 + direction = 1 + end + + duty_cycle_t = duty_cycle_t + direction + + if delay > 0 then + return + end + + -- if this is running then it's turned on + lights[pin] = true + pwm.setduty(pin, duty_cycle_t) + end) +end + +pwm.setup(green, 500, 0) +pwm.setup(red, 500, 0) +pwm.start(green) +pwm.start(red) + +green_timer = make_timer(green, 50, 1) +red_timer = make_timer(red, 50, 1023) + +function turn_light_on(pin, duty_cycle) + if not lights[pin] then + lights[pin] = true + pwm.setduty(pin, duty_cycle) + end +end + +function turn_light_off(pin) + if lights[pin] then + pwm.setduty(pin, 0) + lights[pin] = false + end +end + +function toggle_light(pin) + print("Toggling " .. tostring(pin)) + local duty_cycle = calc_duty_cycle(1) + print("duty_cycle = ".. duty_cycle) + if lights[pin] then + turn_light_off(pin) + else + turn_light_on(pin, duty_cycle) + end +end + +print("Booted up") + +function hex_to_char(x) + return string.char(tonumber(x, 16)) +end + +function get_time() + local t = tmr.time() + local hours = t/3600 + local seconds_leftover = t % 3600 + return tostring(hours) .. " hours, " .. tostring(minutes_leftover) +end + +function urldecode(url) + if url == nil then + return + end + url = url:gsub("+", " ") + url = url:gsub("%%(%x%x)", hex_to_char) + return url +end + +function extract_formdata(s) + local cgi = {} + for name, value in string.gmatch(s, "([^&=]+)=([^&=]+)") do + name = name + value = value + cgi[name] = value + end + return cgi +end + +function get_info(group) + local info = node.info(group) + local result = "" + for key, value in pairs(info) do + result = result .. "" + end + + return result .. "
" .. tostring(group) .. "
" .. tostring(key) .. "" .. tostring(value) .. "
" +end + +function gen_select(name, id, options) + local result = "" +end + +function gen_form(name, endpoint, fields, gen_inputs) + local result = "

" .. name .. "

" + result = result .. gen_inputs(name, endpoint, fields) + return result .. "
" +end + +function gen_buttons(name, endpoint, fields) + local result = "

" .. name .. "

" + for key, value in pairs(fields) do + result = result .. "
" + result = result .. "" .. "status here" .. "
" + end + return result +end + +function startup() + sntp.sync( + nil, + function(sec, usec, server, info) + print('synced ntp ', sec, usec, server) + end, + function() + print('failed to sync ntp') + end, + 1 -- auto-repeat sync + ) + file.close("_init.lua") + print("Starting up") + local httpserver = node.LFS.get("httpserver")() + print(httpserver) + + httpserver.createServer(8080, function(req, res) + --print("+R", req.method, req.url, node.heap()) + + req.ondata = function(self, chunk) + --print("+B", chunk and #chunk, node.heap()) + print(req.url) + if chunk ~= nil then + if req.url == "/toggle" then + local params = extract_formdata(urldecode(chunk)) + if params["toggle"] ~= nil then + toggle_light(pins[params["toggle"]]) + end + elseif req.url == "/add_job" then + post_data = urldecode(chunk) + if string.len(post_data) > 6 then + local a, b = string.find(post_data, "=") + local cron_expression = post_data:sub(a+1) + + print(error_msg) + else + ran = false + error_msg = "invalid" + end + + if not ran then + print("post_data = " .. post_data) + cron_error = error_msg + end + elseif req.url == "/reboot" then + node.restart() + end + end + if not chunk then + -- reply + if req.url == "/" then + res:send(nil, 200) + res:send_header("Content-Type", "text/html") + res:send_header("Connection", "close") + + local toggle_lights = gen_buttons("Toggle Lights", "toggle_lights", {["toggle_red"]="Red", ["toggle_green"]="Green"}) + local toggle_lights_form = gen_form("Toggle Lights Form", "toggle", {["toggle_red"]="Red", ["toggle_green"]="Green"}, gen_select) + + res:send("
Uptime: ".. tostring(tmr.time()) .. " seconds" .. toggle_lights_form .. toggle_lights .. "
" .. get_info("hw") .. get_info("build_config") .. get_info("sw_version") .. "
") + res:send("\r\n") + elseif req.url == "/toggle" then + res:send(nil, 303) + res:send_header("Location", "/") + res:send_header("Connection", "close") + res:send("switching light\r\n") + res:send("\r\n") + else + res:send(nil, 303) + res:send_header("Location", "/") + res:send_header("Connection", "close") + res:send("\r\n") + end + res:finish() + end + end + + end) +end + +function connect_wifi() + print("Trying to connect to wifi with captive portal") + enduser_setup.start( + function() + if wifi.sta.getip() ~= nil then + print("Connected to WiFi as:" .. wifi.sta.getip()) + tmr.create():alarm(3000, tmr.ALARM_SINGLE, startup) + end + end, + function(err, str) + print("enduser_setup: Err #" .. err .. ": " .. str) + end, + print + ) +end + +connect_wifi() diff --git a/sources/compile.sh b/sources/compile.sh new file mode 100755 index 0000000..0bf5b8d --- /dev/null +++ b/sources/compile.sh @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +#while read f; do /home/wes/code/nodemcu-firmware/luac.cross.int -o "$(echo $f | cut -d '.' -f 1).lc" $f ; done < <(ls *lua) +#rm -f init.lc + +rm -f lfs.img +/home/wes/code/nodemcu-firmware/luac.cross.int -f -o lfs.img *lua diff --git a/sources/fifo.lua b/sources/fifo.lua new file mode 100644 index 0000000..d21b1da --- /dev/null +++ b/sources/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/sources/fifosock.lua b/sources/fifosock.lua new file mode 100644 index 0000000..64f625f --- /dev/null +++ b/sources/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, (node.LFS.get("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/sources/httpserver.lua b/sources/httpserver.lua new file mode 100644 index 0000000..c1f483b --- /dev/null +++ b/sources/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 = (node.LFS.get("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/sources/lfs.img b/sources/lfs.img new file mode 100644 index 0000000..bc95526 Binary files /dev/null and b/sources/lfs.img differ