diff --git a/credentials.lua.example b/credentials.lua.example deleted file mode 100644 index 9b92e9d..0000000 --- a/credentials.lua.example +++ /dev/null @@ -1,2 +0,0 @@ -SSID="something" -PASSWORD="yourpassword" diff --git a/firmware/firmware.bin b/firmware/firmware.bin new file mode 100644 index 0000000..732656b Binary files /dev/null and b/firmware/firmware.bin differ diff --git a/firmware/nodemcu-release-19-modules-2021-11-10-19-19-06-float.bin b/firmware/nodemcu-release-19-modules-2021-11-10-19-19-06-float.bin deleted file mode 100644 index 34eb8b9..0000000 Binary files a/firmware/nodemcu-release-19-modules-2021-11-10-19-19-06-float.bin and /dev/null differ diff --git a/flash.sh b/flash.sh index cd851cd..0fc2385 100755 --- a/flash.sh +++ b/flash.sh @@ -1,11 +1,14 @@ nodemcu-tool reset -esptool.py erase_flash -esptool.py --port /dev/ttyUSB0 write_flash -fm qio 0x00000 firmware/nodemcu-release-19-modules-2021-11-10-19-19-06-float.bin +esptool.py --port /dev/ttyUSB0 erase_flash +esptool.py --port /dev/ttyUSB0 write_flash -fm qio 0x00000 firmware/firmware.bin -nodemcu-tool upload *.lua libs/* +CONN_DELAY=1000 + +nodemcu-tool --connection-delay $CONN_DELAY upload *.lua libs/* +#nodemcu-tool upload *.lua libs/* while [[ $? != 0 ]]; do - nodemcu-tool upload *.lua libs/* + nodemcu-tool --connection-delay $CONN_DELAY upload *.lua libs/* done - -echo 'dofile("init.lua")' | nodemcu-tool terminal +# +echo 'dofile("init.lua")' | nodemcu-tool --connection-delay $CONN_DELAY terminal diff --git a/init.lua b/init.lua index 0de7604..07e7025 100644 --- a/init.lua +++ b/init.lua @@ -1,293 +1,184 @@ -- Never change these unless the board changes -echo_pin = 2 -trig_pin = 3 -light_pin = 5 - -use_sonar = false +trig_pin = 5 light_on = false -if adc.force_init_mode(adc.INIT_ADC) -then - node.restart() - return -- don't bother continuing, the restart is scheduled +function reset_light() + gpio.write(trig_pin, gpio.LOW) + tmr.delay(100000) + gpio.write(trig_pin, gpio.HIGH) + tmr.delay(100000) + gpio.write(trig_pin, gpio.LOW) end -print("System voltage (mV):", adc.readvdd33(0)) - -gpio.mode(light_pin, gpio.OUTPUT) -gpio.mode(echo_pin, gpio.INT) -- interrupt mode -gpio.mode(trig_pin, gpio.OUTPUT) -gpio.write(light_pin, gpio.HIGH) - function toggle_light() - if light_on then - gpio.write(light_pin, gpio.HIGH) - else - gpio.write(light_pin, gpio.LOW) - end + print("toggling") + reset_light() light_on = not light_on + file.putcontents("./light_status", light_on and "on" or "off") end function turn_light_on() - gpio.write(light_pin, gpio.LOW) + if not light_on then + toggle_light() + end end function turn_light_off() - gpio.write(light_pin, gpio.HIGH) -end - -function tablelen(T) - local count = 0 - for _ in pairs(T) do count = count + 1 end - return count -end - -samples = {} -distance = {start_v=0.0, end_v=0.0} - -sample_rate = 4 -- number of samples it requires before triggering, recommended <= 5, >=3 -max_stderr = 0.5 -- margin of error allowed before triggering the light, recommended <= 1 -distance_max = 30 -- threshold for how far an object must be to trigger it in cm -distance_min = 10 -- minimum distance (below this amount don't trigger it), recommended >= 5, <= 20 - --- Get the mean value of a table -function mean(t) - -- http://lua-users.org/wiki/SimpleStats - local sum = 0 - local count = 0 - - for k,v in pairs(t) do - if type(v) == 'number' then - sum = sum + v - count = count + 1 - end + if light_on then + toggle_light() end - return (sum / count) end -function stdev(t) - -- http://lua-users.org/wiki/SimpleStats - local m - local vm - local sum = 0 - local count = 0 - local result - - m = mean(t) - - for k, v in pairs(t) do - if type(v) == 'number' then - vm = v - m - sum = sum + (vm * vm) - count = count + 1 - end - end +light_status = file.getcontents("./light_status") +if light_status == "on" then + turn_light_on() +end +print("Booted up") - result = math.sqrt(sum / (count-1)) +gpio.mode(trig_pin, gpio.OUTPUT) +gpio.write(trig_pin, gpio.LOW) - return result +function hex_to_char(x) + return string.char(tonumber(x, 16)) end -function stderr(t) - -- based on the definition of standard error - return stdev(t) / math.sqrt(tablelen(t)) +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 measure(level, ts, evcount) - if level == 1 then - distance["start_v"] = ts - else - -- check if there's a low voltage but no starting high voltage - if distance["start_v"] > 0 then - distance["end_v"] = ts - - if distance["start_v"] >= distance["end_v"] then - distance = {start_v=0.0, end_v=0.0} -- bad reading - return - end - - -- See https://www.adafruit.com/product/165 - local temperature = ((adc.read(0)*100)-50)/1000 - - -- See http://hyperphysics.phy-astr.gsu.edu/hbase/Sound/souspe.html - local speed_of_sound = (331.4 + 0.6*temperature)/10000 - - -- See https://randomnerdtutorials.com/esp8266-nodemcu-hc-sr04-ultrasonic-arduino/ - local d = ((distance["end_v"] - distance["start_v"]) * speed_of_sound) / 2.0 - - -- Only care about values between 10 and 50 - if math.floor(d) > distance_min and d < distance_max then - table.insert(samples, d) - local s = stderr(samples) - if s > max_stderr and tablelen(samples) == (sample_rate - 1) then -- start over if it would cause stderr to be too great - samples = {} - distance = {start_v=0.0, end_v=0.0} - end - - end - distance = {start_v=0, end_v=0} - end +function urldecode(url) + if url == nil then + return end + url = url:gsub("+", " ") + url = url:gsub("%%(%x%x)", hex_to_char) + return url +end - local s_num = tablelen(samples) - - if s_num >= sample_rate then - local distance_mean = mean(samples) - local s = stderr(samples) - if s < max_stderr and distance_mean < distance_max and distance_mean > distance_min then - print("stder = " .. s) - print("distance = " .. distance_mean) - if not light_on then - toggle_light() - end - end - samples = {} +function get_info(group) + local info = node.info(group) + local result = "" + for key, value in pairs(info) do + result = result .. "" end -end -function trig() - gpio.write(trig_pin, gpio.HIGH) - tmr.delay(11) - gpio.write(trig_pin, gpio.LOW) + return result .. "
" .. tostring(group) .. "
" .. tostring(key) .. "" .. tostring(value) .. "
" end --- load credentials, 'SSID' and 'PASSWORD' declared and initialize in there -dofile("credentials.lua") 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 + ) if file.open("init.lua") == nil then print("init.lua deleted or renamed") else - print("Running") + cron_error = nil file.close("init.lua") - print("Starting up") - sntp.sync(nil, - function(sec, usec, server, info) - print("sync'd") - - tm = rtctime.epoch2cal(rtctime.get()) - - print(tm["hour"]) - print(tm["min"]) - - if tm["hour"] >= 12 or tm["hour"] < 2 then - if tm["hour"] == 12 and tm["min"] < 30 then - return - end - turn_light_on() - end - - if tm["hour"] >= 2 and tm["hour"] < 12 then - turn_light_off() - end - cron.schedule("0 02 * * *", function(e) -- 9 pm EST is 2 UTC - print("Turning light off") - turn_light_off() - end) + require("httpserver").createServer(8080, function(req, res) + --print("+R", req.method, req.url, node.heap()) - cron.schedule("30 12 * * *", function(e) -- 7:30 am EST is 12:30 UTC - print("Turning light on") - turn_light_on() - end) - - if use_sonar then - tmr.create():alarm(61, tmr.ALARM_AUTO, trig) - gpio.trig(echo_pin, "both", measure) - end - end, - nil, 1) - - require("httpserver").createServer(80, function(req, res) - print("+R", req.method, req.url, node.heap()) - - req.onheader = function(self, name, value) -- luacheck: ignore - print("+H", name, value) - end - -- setup handler of body, if any - req.ondata = function(self, chunk) -- luacheck: ignore - print("+B", chunk and #chunk, node.heap()) + req.ondata = function(self, chunk) + --print("+B", chunk and #chunk, node.heap()) print(req.url) - if not chunk then - -- reply - res:send(nil, 200) - res:send_header("Connection", "close") + if chunk ~= nil then if req.url == "/toggle" then toggle_light() elseif req.url == "/on" then turn_light_on() elseif req.url == "/off" then turn_light_off() - elseif req.url == "/toggle_sonar" then - use_sonar = not use_sonar + 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) + + local ran, error_msg = pcall(cron.schedule, cron_expression, toggle_light) + 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 == "/reset_light" then + reset_light() + elseif req.url == "/clear_jobs" then + cron.reset() + elseif req.url == "/reboot" then + cron.reset() + node.restart() + end + end + if not chunk then + local toggle_status = light_on and "On" or "Off" + local color = light_on and "green" or "black" + -- reply + if req.url == "/" then + res:send(nil, 200) + res:send_header("Content-Type", "text/html") + res:send_header("Connection", "close") + res:send("
Uptime: ".. tostring(tmr.time()) .. " seconds


".. toggle_status .. "



" .. get_info("hw") .. get_info("build_config") .. get_info("sw_version") .. "
") + res:send("\r\n") + elseif req.url == "/toggle" or req.url == "/reset_light" then + res:send(nil, 303) + res:send_header("Location", "/") + res:send_header("Connection", "close") + res:send("switching light\r\n") + res:send("\r\n") + elseif req.url == "/add_job" then + if cron_error then + res:send(nil, 400) + res:send_header("Content-Type", "text/html") + res:send_header("Connection", "close") + res:send(cron_error .. "\r\n") + res:send("\r\n") + cron_error = nil + else + res:send(nil, 303) + res:send_header("Location", "/") + res:send_header("Connection", "close") + res:send("\r\n") + end + else + res:send(nil, 303) + res:send_header("Location", "/") + res:send_header("Connection", "close") + res:send("\r\n") end - - res:send("The light is " .. (light_on and "on\n" or "off\n")) res:finish() end end - end) + end) end end --- Define WiFi station event callbacks -wifi_connect_event = function(T) - print("Connection to AP("..T.SSID..") established!") - print("Waiting for IP address...") - if disconnect_ct ~= nil then disconnect_ct = nil end -end - -wifi_got_ip_event = function(T) - -- Note: Having an IP address does not mean there is internet access! - -- Internet connectivity can be determined with net.dns.resolve(). - print("Wifi connection is ready! IP address is: "..T.IP) - print("Startup will resume momentarily, you have 3 seconds to abort.") - print("Waiting...") - tmr.create():alarm(3000, tmr.ALARM_SINGLE, startup) - mdns.register("smartlight", {hardware='NodeMCU'}) -end - -wifi_disconnect_event = function(T) - if T.reason == wifi.eventmon.reason.ASSOC_LEAVE then - --the station has disassociated from a previously connected AP - return - end - -- total_tries: how many times the station will attempt to connect to the AP. Should consider AP reboot duration. - local total_tries = 75 - print("\nWiFi connection to AP("..T.SSID..") has failed!") - - --There are many possible disconnect reasons, the following iterates through - --the list and returns the string corresponding to the disconnect reason. - for key,val in pairs(wifi.eventmon.reason) do - if val == T.reason then - print("Disconnect reason: "..val.."("..key..")") - break +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 - - if disconnect_ct == nil then - disconnect_ct = 1 - else - disconnect_ct = disconnect_ct + 1 - end - if disconnect_ct < total_tries then - print("Retrying connection...(attempt "..(disconnect_ct+1).." of "..total_tries..")") - else - wifi.sta.disconnect() - print("Aborting connection to AP!") - disconnect_ct = nil - end -end - --- Register WiFi Station event callbacks -wifi.eventmon.register(wifi.eventmon.STA_CONNECTED, wifi_connect_event) -wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, wifi_got_ip_event) -wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, wifi_disconnect_event) - -print("Connecting to WiFi access point...") -wifi.setmode(wifi.STATION) -wifi.sta.config({ssid=SSID, pwd=PASSWORD}) --- wifi.sta.connect() not necessary because config() uses auto-connect=true by default + end, + function(err, str) + print("enduser_setup: Err #" .. err .. ": " .. str) + end, + print +) diff --git a/libs/fifosocktest.lua b/libs/fifosocktest.lua deleted file mode 100644 index 4ac9f21..0000000 --- a/libs/fifosocktest.lua +++ /dev/null @@ -1,145 +0,0 @@ --- --- 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")