Browse Source

LFS related updates

master
Wesley Kerfoot 1 year ago
commit
0b71f37014
  1. 7
      .nodemcutool
  2. 15
      README.md
  3. BIN
      firmware/0x00000.bin
  4. BIN
      firmware/0x10000.bin
  5. 28
      flash.sh
  6. 252
      sources/_init.lua
  7. 7
      sources/compile.sh
  8. 45
      sources/fifo.lua
  9. 134
      sources/fifosock.lua
  10. 217
      sources/httpserver.lua
  11. BIN
      sources/lfs.img

7
.nodemcutool

@ -0,0 +1,7 @@
{
"baudrate": "115200",
"port": "/dev/ttyUSB0",
"minify": false,
"compile": false,
"keeppath": false
}

15
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

BIN
firmware/0x00000.bin

Binary file not shown.

BIN
firmware/0x10000.bin

Binary file not shown.

28
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

252
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 = "<table><thead><tr><th colspan='2'>" .. tostring(group) .. "</th></thead><tbody>"
for key, value in pairs(info) do
result = result .. "<tr><td>" .. tostring(key) .. "</td><td>" .. tostring(value) .. "</td></tr>"
end
return result .. "</tbody></table>"
end
function gen_select(name, id, options)
local result = "<label for='" .. id .. "'>" .. name .. "</label><select name='" .. id .. "' id='" .. id .. "'>"
for key, value in pairs(options) do
result = result .. "<option value='" .. key .. "'>" .. value .. "</option>"
end
return result .. "</select>"
end
function gen_form(name, endpoint, fields, gen_inputs)
local result = "<h2>" .. name .. "</h2><form action='/" .. endpoint .. "' method='post'>"
result = result .. gen_inputs(name, endpoint, fields)
return result .. "<div class='form-example'><input type='submit' value='Submit'></div></form>"
end
function gen_buttons(name, endpoint, fields)
local result = "<h2>" .. name .. "</h2>"
for key, value in pairs(fields) do
result = result .. "<form action='/" .. key .. "' method='post'>"
result = result .. "<button style='color:black;'>" .. value .. "</button><span style='color:black;'>" .. "status here" .. "</span></form>"
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("<style>.button{text-decoration:underline;}.body{padding:0; margin:0;}.par{display:flex;flex-direction:row;}.a{margin: auto;width:50%;}.b{margin: auto;width:50%;}</style><html><body><div class='par'><div class='a'><span>Uptime: ".. tostring(tmr.time()) .. " seconds</span>" .. toggle_lights_form .. toggle_lights .. "</div><div class='b'>" .. get_info("hw") .. get_info("build_config") .. get_info("sw_version") .. "</div></div></body></html>")
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()

7
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

45
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
}

134
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 }

217
sources/httpserver.lua

@ -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 = (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

BIN
sources/lfs.img

Binary file not shown.
Loading…
Cancel
Save