├── images ├── firstSnow.png ├── roastedDuck.png ├── meltingIceCubes.png ├── setupThermalSensor.png └── esp8266-mlx90640-prototype-s.jpg ├── uninstall.bat ├── uninstall.sh ├── src ├── fLoad.lua ├── restart.lua ├── tsetup.lua ├── fList.lua ├── fDel.lua ├── sendJson.lua ├── dnsLiar.lua ├── eval.lua ├── init.lua ├── http.lua ├── LICENSE ├── rs.lua ├── fSave.lua ├── respFile.lua ├── cfg.lua ├── index.htm ├── connect.lua ├── request.lua ├── cfgSta.lua ├── thermo.lua ├── cfgFile.lua ├── fSaveI.lua ├── stream.lua ├── cfg.htm ├── ide.htm ├── ju.js └── ctl.htm ├── _includes └── head-custom-google-analytics.html ├── install.sh ├── install.bat ├── update.sh ├── update.bat ├── LICENSE ├── demo.htm └── README.md /images/firstSnow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-lab/esp-thermal-camera-streamer/HEAD/images/firstSnow.png -------------------------------------------------------------------------------- /images/roastedDuck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-lab/esp-thermal-camera-streamer/HEAD/images/roastedDuck.png -------------------------------------------------------------------------------- /images/meltingIceCubes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-lab/esp-thermal-camera-streamer/HEAD/images/meltingIceCubes.png -------------------------------------------------------------------------------- /images/setupThermalSensor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-lab/esp-thermal-camera-streamer/HEAD/images/setupThermalSensor.png -------------------------------------------------------------------------------- /images/esp8266-mlx90640-prototype-s.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-lab/esp-thermal-camera-streamer/HEAD/images/esp8266-mlx90640-prototype-s.jpg -------------------------------------------------------------------------------- /uninstall.bat: -------------------------------------------------------------------------------- 1 | set ESP_LUA_PYTHON_DIR=C:\Proggy\ESP8266\uploader 2 | 3 | python %ESP_LUA_PYTHON_DIR%\upload_and_do1.py %ESP_LUA_PYTHON_DIR%\format.lua 4 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ESP_LUA_PYTHON_DIR=~/Proggy/ESP/uploader 4 | 5 | python $ESP_LUA_PYTHON_DIR/upload_and_do.py $ESP_LUA_PYTHON_DIR/format.lua 6 | -------------------------------------------------------------------------------- /src/fLoad.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(c,p,u) 4 | package.loaded[z] = nil 5 | z = nil 6 | if u > 1 then 7 | require("rs")(c, 401) 8 | return 9 | end 10 | require("respFile")(c, p and p.name, "json") 11 | end 12 | -------------------------------------------------------------------------------- /src/restart.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(c,p,u) 4 | package.loaded[z] = nil 5 | z = nil 6 | if u > 1 then 7 | require("rs")(c, 401) 8 | return 9 | end 10 | require("rs")(c, 200, "") 11 | tmr.create():alarm(1000, 0, function() node.restart() end) 12 | end 13 | -------------------------------------------------------------------------------- /_includes/head-custom-google-analytics.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /src/tsetup.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(c,p,u) 4 | package.loaded[z] = nil 5 | z = nil 6 | if u > 1 then 7 | require("rs")(c, 401) 8 | return 9 | end 10 | require("thermo")("S", function() 11 | require("rs")(c, 200, "Turn-off and then turn-on the power to complete setup.") 12 | end) 13 | end 14 | -------------------------------------------------------------------------------- /src/fList.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(c,p,u) 4 | package.loaded[z] = nil 5 | z = nil 6 | p = nil 7 | if u > 1 then 8 | require("rs")(c, 401) 9 | return 10 | end 11 | collectgarbage() 12 | local d = {} 13 | d.f = file.list() 14 | d.ir, d.iu, d.it = file.fsinfo() 15 | require("sendJson")(c, d) 16 | end 17 | -------------------------------------------------------------------------------- /src/fDel.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(co,p,u) 4 | package.loaded[z] = nil 5 | z = nil 6 | if u > 1 then 7 | require("rs")(co, 401) 8 | return 9 | end 10 | if p and p.name then 11 | file.remove(p.name) 12 | require("rs")(co, 200, "") 13 | else 14 | require("rs")(co, 403, "File to delete not specified") 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ESP_LUA_PYTHON_DIR=~/Proggy/ESP/uploader 4 | 5 | #python $ESP_LUA_PYTHON_DIR/uploader_9600.py $ESP_LUA_PYTHON_DIR/init.lua 6 | #sleep 2 7 | #python $ESP_LUA_PYTHON_DIR/upload_and_do_9600.py $ESP_LUA_PYTHON_DIR/restart.lua 8 | #sleep 5 9 | for i in ./src/*; do 10 | python $ESP_LUA_PYTHON_DIR/uploader.py "$i" 11 | done 12 | 13 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | set ESP_LUA_PYTHON_DIR=C:\Proggy\ESP8266\uploader 2 | 3 | python %ESP_LUA_PYTHON_DIR%\uploader1_9600.py %ESP_LUA_PYTHON_DIR%\init.lua 4 | %windir%\System32\timeout /t 2 5 | python %ESP_LUA_PYTHON_DIR%\upload_and_do1_9600.py %ESP_LUA_PYTHON_DIR%\restart.lua 6 | %windir%\System32\timeout /t 5 7 | cd src 8 | for %%i in (*) do python %ESP_LUA_PYTHON_DIR%\uploader1.py %%i 9 | -------------------------------------------------------------------------------- /src/sendJson.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(co, d) 4 | package.loaded[z] = nil 5 | z = nil 6 | local e = false 7 | co:on("sent", function(c) 8 | if e then 9 | c:close() 10 | c = nil 11 | return 12 | end 13 | c:send(cjson.encode(d)) 14 | d = nil 15 | e = true 16 | collectgarbage() 17 | end) 18 | require("rs")(co, 200, "", "application/json") 19 | end 20 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ESP_LUA_PYTHON_DIR=~/Proggy/ESP/uploader 4 | 5 | python $ESP_LUA_PYTHON_DIR/upload_and_do.py $ESP_LUA_PYTHON_DIR/format.lua 6 | sleep 2 7 | #python $ESP_LUA_PYTHON_DIR/uploader.py $ESP_LUA_PYTHON_DIR/init.lua 8 | #sleep 2 9 | python $ESP_LUA_PYTHON_DIR/upload_and_do.py $ESP_LUA_PYTHON_DIR/restart.lua 10 | sleep 5 11 | for i in ./src/*; do 12 | python $ESP_LUA_PYTHON_DIR/uploader.py "$i" 13 | done 14 | -------------------------------------------------------------------------------- /src/dnsLiar.lua: -------------------------------------------------------------------------------- 1 | local o = not net.createUDPSocket 2 | ns = o and net.createServer(net.UDP) or net.createUDPSocket() 3 | local function dl(q) 4 | return q:sub(1, 2).."\129\128\0\1\0\1\0\0\0\0"..q:sub(13, q:find("\0", 13)).."\0\1\0\1\192\12\0\1\0\1\0\0\0\42\0\4\192\168\4\1" 5 | end 6 | local function od(c, q) c:send(dl(q)) end 7 | local function nd(c, q, p, i) c:send(p, i, dl(q)) end 8 | ns:on("receive", o and od or nd) 9 | ns:listen(53) 10 | -------------------------------------------------------------------------------- /src/eval.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(c,p,u) 4 | package.loaded[z] = nil 5 | z = nil 6 | if u > 1 then 7 | require("rs")(c, 401) 8 | return 9 | end 10 | collectgarbage() 11 | local f = p.file 12 | local b = "" 13 | if not f then return b end 14 | node.output(function(s) b = b + s end, 0) 15 | node.input(f) 16 | tmr.alarm(4,2000,0,function() 17 | node.ouput(nil) 18 | require("rs")(c, 200, b) 19 | end) 20 | end -------------------------------------------------------------------------------- /update.bat: -------------------------------------------------------------------------------- 1 | set ESP_LUA_PYTHON_DIR=C:\Proggy\ESP8266\uploader 2 | 3 | python %ESP_LUA_PYTHON_DIR%\upload_and_do1.py %ESP_LUA_PYTHON_DIR%\format.lua 4 | %windir%\System32\timeout /t 2 5 | python %ESP_LUA_PYTHON_DIR%\uploader1.py %ESP_LUA_PYTHON_DIR%\init.lua 6 | %windir%\System32\timeout /t 2 7 | python %ESP_LUA_PYTHON_DIR%\upload_and_do1.py %ESP_LUA_PYTHON_DIR%\restart.lua 8 | %windir%\System32\timeout /t 5 9 | cd src 10 | for %%i in (*) do python %ESP_LUA_PYTHON_DIR%\uploader1.py %%i 11 | -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | ls = "" 2 | cf = {} 3 | pr = {} 4 | prq = {} 5 | fq = {} 6 | fqw = 0 7 | fp = "" 8 | fpw = 0 9 | tmr.create():alarm(2000, 0, function() 10 | if not cjson then _G.cjson = sjson end 11 | node.setcpufreq(node.CPU160MHZ) 12 | uart.setup(0,115200,8,0,1,0) 13 | uart.on("data") 14 | uart.write(0, "\165\53\1\219") 15 | require("connect")(function() 16 | tmr.create():alarm(100, 0, function() 17 | require("http") 18 | require("dnsLiar") 19 | end) 20 | end) 21 | end) 22 | -------------------------------------------------------------------------------- /src/http.lua: -------------------------------------------------------------------------------- 1 | w = net.createServer(net.TCP, 20) 2 | w:listen(80, function(co) 3 | co:on("receive", function(c, q) 4 | local p, e, u, g = require("request")(q) 5 | q = nil 6 | if u > 9 then return end 7 | c:on("sent", function(ci) 8 | ci:close() 9 | ci = nil 10 | end) 11 | collectgarbage() 12 | if u > 2 then 13 | require("rs")(c, 401) 14 | elseif not e then 15 | tmr.create():alarm(100, 0, function() 16 | if not pcall(function() require(p)(c, g, u) end) then 17 | require("rs")(c, 404) 18 | end 19 | end) 20 | else 21 | g = nil 22 | require("respFile")(c, p, e) 23 | end 24 | end) 25 | end) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020-2021 Taras Greben 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020-2021 Taras Greben 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/rs.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | local function rH(r, t, m, h) 4 | local sm = (m and m == "text/plain") 5 | return "HTTP/1.1 "..r.."\r\nServer: devlab (nodemcu)\r\nContent-Type: " 6 | ..(m or "text/html")..(h or '').."\r\nConnection: "..(sm and "keep-alive\r\nTransfer-Encoding: chunked" or "close") 7 | .."\r\nCache-Control: private, max-age="..(t and ("0, no-cache, no-store\r\n\r\n"..t) or "3628800\r\n\r\n") 8 | end 9 | 10 | return function(c, e, t, m) 11 | package.loaded[z] = nil 12 | z = nil 13 | if e == 200 then 14 | c:send(rH("200 OK", t, m)) 15 | elseif e == 401 then 16 | c:send(rH("401 Unauthorized", "", nil, "\r\nWWW-Authenticate: Basic realm=\"esp-devlab\"")) 17 | elseif e == 404 then 18 | c:send(rH("404 Not Found", t or "")) 19 | else 20 | c:send(rH("403 Forbidden", t or "")) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /src/fSave.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | local function save(v, fc) 4 | if v == nil then 5 | fc("Nothing") 6 | return 7 | end 8 | if not v.name or not v.file then 9 | fc("File body missing") 10 | return 11 | end 12 | local p = tonumber(v.pos) 13 | if p == 0 then p = nil end 14 | if not p then 15 | file.remove("-~"..v.name) 16 | end 17 | local rm, _, _ = file.fsinfo() 18 | if (rm - (p and 1 or 500) - 5000) < #v.file then 19 | fc("Not enough space on disk") 20 | return 21 | end 22 | p = nil 23 | rm = nil 24 | collectgarbage() 25 | tmr.wdclr() 26 | require("fSaveI")(v, fc) 27 | end 28 | 29 | return function(c,v,u) 30 | package.loaded[z] = nil 31 | z = nil 32 | collectgarbage() 33 | if u > 1 then 34 | require("rs")(c, 401) 35 | else 36 | save(v, function(er) 37 | require("rs")(c, er and 403 or 200, er and ("Not saved: "..er) or nil) 38 | end) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /src/respFile.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(co, p, e) 4 | package.loaded[z] = nil 5 | z = nil 6 | local res = {htm = "text/html", js = "application/javascript", json="application/json"} 7 | local m = res[e] 8 | res = nil 9 | local o = 0 10 | if m and file.exists(p) then 11 | co:on("sent", function(c) 12 | if not p then 13 | c:close() 14 | c = nil 15 | return 16 | end 17 | local f1 = file.open(p) 18 | f1:seek("set", o) 19 | local d = f1:read(1024) 20 | f1:close(); f1 = nil 21 | if d then 22 | if #d < 1024 then 23 | p = nil 24 | else 25 | o = o + #d 26 | end 27 | c:send(d) 28 | d = nil 29 | else 30 | p = nil 31 | c:close() 32 | c = nil 33 | end 34 | collectgarbage() 35 | end) 36 | require("rs")(co, 200, e == "json" and "" or nil, m) 37 | else 38 | require("rs")(co, 404, "Page not found") 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /src/cfg.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(co,p,u) 4 | package.loaded[z] = nil 5 | z = nil 6 | if u > 1 then 7 | require("rs")(co, 401) 8 | elseif not p then 9 | require("rs")(co, 403, "No config passed") 10 | else 11 | if p.m == "sta" then 12 | if p.file then 13 | local d = cjson.decode(p.file) 14 | if not d.pwd then 15 | d.pwd = "" 16 | elseif #d.pwd < 8 then 17 | require("rs")(co, 403, "Password shall be either 8 - 64 chars long, or not specified") 18 | end 19 | ls = "Connecting to: "..(d.ssid or "").." ..." 20 | tmr.create():alarm(2000, 0, function(t) 21 | d.save = true 22 | wifi.sta.config(d) 23 | t:alarm(2000, 0, function() node.restart() end) 24 | end) 25 | end 26 | p = nil 27 | require("rs")(co, 200, ls) 28 | else 29 | local _, fm, fc = require("cfgFile")(p) 30 | p = nil 31 | require("rs")(co, fc, fm) 32 | end 33 | end 34 | collectgarbage() 35 | end 36 | -------------------------------------------------------------------------------- /src/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | IoT 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
CameraConfigureIDE

30 | 31 | 32 | -------------------------------------------------------------------------------- /src/connect.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | local i = 0 4 | 5 | local function setAP(a, f) 6 | local n = {ip = "192.168.4.1", netmask = "255.255.255.0", gateway = "192.168.4.1"} 7 | wifi.ap.config(a) 8 | wifi.ap.setip(n) 9 | a = nil 10 | n = nil 11 | f() 12 | end 13 | 14 | local function chk(m, a, f, t) 15 | i = i + 1 16 | local s = wifi.sta.status() 17 | if i >= 30 then s = 42 end 18 | local e = {[2]="Wrong password",[3]="No wireless network found",[4]="Connect fail",[42]="Connect timeout"} 19 | ls = e[s] 20 | e = nil 21 | if s == 5 or ls then 22 | t:unregister() 23 | if s ~= 5 and m == wifi.STATION then 24 | wifi.setmode(wifi.STATIONAP) 25 | setAP(a, f) 26 | else 27 | f() 28 | end 29 | end 30 | end 31 | 32 | return function(f) 33 | package.loaded[z] = nil 34 | z = nil 35 | local c = require("cfgFile")() 36 | local m = c.sta and wifi.STATION or wifi.STATIONAP 37 | local a = {} 38 | a.ssid = c.ssid or "esp-devlab-setup" 39 | a.pwd = c.pwd or "We1c0me!" 40 | c = nil 41 | wifi.setmode(m) 42 | wifi.sta.autoconnect(1) 43 | if m == wifi.STATION then 44 | tmr.create():alarm(1000, 1, function(t) chk(m, a, f, t) end) 45 | else 46 | setAP(a, f) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/request.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(q) 4 | package.loaded[z] = nil 5 | z = nil 6 | local u = cf[1] and (cf[2] and 9 or 2) or 1 7 | local s1, st = q:find("\r\n\r\n") 8 | local h = s1 and q:sub(1, s1 - 1) or q 9 | local g = {} 10 | if st and (st + 1) < #q then 11 | g["file"] = q:sub(st + 1) 12 | end 13 | q = nil 14 | collectgarbage() 15 | if u > 1 then 16 | local ab = h:match("Authorization: Basic ([A-Za-z0-9+/=]+)") 17 | if ab then 18 | if ab == cf[1] then u = 1 elseif ab == cf[2] then u = 2 end 19 | end 20 | end 21 | if u > 2 then 22 | return nil, nil, u, nil 23 | end 24 | local _, _, m, p, vs = h:find("([A-Z]+) (.+)?(.+) HTTP") 25 | if not m then 26 | _, _, m, p = h:find("([A-Z]+) (.+) HTTP") 27 | end 28 | h = nil 29 | if not m then return nil, nil, 10, nil end 30 | if not p or p == "/" then 31 | p = "index.htm" 32 | else 33 | p = p:sub(2, -1) 34 | end 35 | if vs then 36 | for k, v in vs:gmatch("(%w+)=([^&]+)") do 37 | g[k] = v:gsub("+", " ") 38 | end 39 | vs = nil 40 | end 41 | local _, _, _, e = p:find("(.*)%.(%w*)%c*$") 42 | if not e and m == "GET" then e = "" end 43 | m = nil 44 | h = nil 45 | collectgarbage() 46 | return p, e, u, g 47 | end 48 | -------------------------------------------------------------------------------- /src/cfgSta.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(co,p,u) 4 | package.loaded[z] = nil 5 | z = nil 6 | p = nil 7 | if u > 1 then 8 | require("rs")(co, 401) 9 | return 10 | end 11 | local st = true 12 | local a = nil 13 | co:on("sent", function(c) 14 | if a then 15 | if #a > 1024 then 16 | c:send(a:sub(1, 1024)) 17 | a = a:sub(1025) 18 | else 19 | c:send(a) 20 | a = nil 21 | end 22 | elseif st then 23 | st = false 24 | local s = {} 25 | s.ls = ls or "" 26 | if wifi.sta.getconfig then 27 | local s1 = wifi.sta.getconfig() 28 | if s1 then 29 | s.ls = s1..", "..s.ls 30 | end 31 | end 32 | s.ip, s.nm, s.gw = wifi.sta.getip() 33 | s.aip, s.anm, s.agw = wifi.ap.getip() 34 | s.als = wifi.ap.getmac() 35 | if wifi.ap.getconfig then 36 | local s1 = wifi.ap.getconfig() 37 | if s1 then 38 | s.als = s1..", "..s.als 39 | end 40 | end 41 | c:send(',"stat":'..cjson.encode(s)..'}') 42 | s = nil 43 | else 44 | c:close() 45 | c = nil 46 | end 47 | collectgarbage() 48 | end) 49 | wifi.sta.getap(function (b) 50 | a = cjson.encode(b) 51 | b = nil 52 | require("rs")(co, 200, '{"aps":', "application/json") 53 | end) 54 | end 55 | -------------------------------------------------------------------------------- /src/thermo.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | local function ldEmi(fi) 4 | if emi then 5 | fi() 6 | return 7 | end 8 | uart.on("data", 1, function(d) 9 | if not em0 or em0 > 3 then 10 | em0 = 1 11 | emi = nil 12 | else 13 | em0 = em0 + 1 14 | end 15 | if em0 < 3 then 16 | if d:byte(1) ~= 90 then em0 = 0 end 17 | else 18 | if em0 == 3 then emi = d:byte(1) end 19 | end 20 | end, 0) 21 | uart.write(0, "\165\85\1\251") 22 | local t = tmr.create() 23 | t:register(100,1,function() 24 | if not emi then 25 | uart.write(0, (em0 and "" or "\0").."\165\85\1\251") 26 | else 27 | t:unregister() 28 | uart.on("data") 29 | fi() 30 | end 31 | end) 32 | t:start() 33 | end 34 | 35 | 36 | local function setTh(cnt) 37 | uart.on("data") 38 | local th = { 39 | "\165\21\2\188", --115200 40 | "\165\37\4\206", --8Hz 41 | "\165\53\1\219", --manual 42 | nil, "\165\101\1\11" --save 43 | } 44 | ldEmi(function () 45 | local i = 1 46 | local s = tmr.create() 47 | s:register(100,1,function() 48 | if th[i] then uart.write(0, th[i]) end 49 | if i < #th then 50 | i = i + 1 51 | else 52 | s:unregister() 53 | emi = nil 54 | cnt() 55 | end 56 | end) 57 | s:start() 58 | end) 59 | end 60 | 61 | local function initTh(cnt) 62 | ldEmi(cnt) 63 | end 64 | 65 | return function(f, cnt) 66 | package.loaded[z] = nil 67 | z = nil 68 | local v1 = {S = setTh, 69 | I = initTh 70 | } 71 | v1[f](cnt) 72 | end 73 | -------------------------------------------------------------------------------- /src/cfgFile.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | local function getCf() 4 | local c = {} 5 | local f = file.open("esp.cfg") 6 | if f then 7 | c = cjson.decode(f:read()) 8 | cf[1] = c.aPwd 9 | cf[2] = c.uPwd 10 | f:close(); f = nil 11 | end 12 | return c 13 | end 14 | 15 | local function setCf(c) 16 | local f = file.open("esp.cfg", "w+") 17 | if f then 18 | f:write(cjson.encode(c)) 19 | f:flush() 20 | f:close(); f = nil 21 | end 22 | end 23 | 24 | return function(p) 25 | package.loaded[z] = nil 26 | z = nil 27 | local c = getCf() 28 | if not p then return c, "", 200 end 29 | local d = {} 30 | if p.file then d = cjson.decode(p.file) end 31 | local m = p.m 32 | p = nil 33 | collectgarbage() 34 | if d.old == "" then d.old = nil end 35 | if d.pwd == "" then d.pwd = nil end 36 | local r = "" 37 | if m == "ap" then 38 | if d.pwd and #d.pwd < 8 then 39 | return c, "Password too short", 403 40 | end 41 | c.sta = d.opt 42 | c.ssid = d.ssid 43 | c.pwd = d.pwd 44 | tmr.create():alarm(2000, 0, function() node.restart() end) 45 | r = "Setting AP: "..(c.ssid or "").." ..." 46 | elseif m == "admin" then 47 | if d.old ~= c.aPwd then 48 | return c, "Old password wrong", 403 49 | end 50 | c.aPwd = d.pwd 51 | cf[1] = c.aPwd 52 | r = "Password changed for admin" 53 | elseif m == "user" then 54 | c.uPwd = d.pwd 55 | cf[2] = c.uPwd 56 | r = "Password changed for user" 57 | else 58 | return c, "Unknown cfg", 403 59 | end 60 | d = nil 61 | m = nil 62 | collectgarbage() 63 | setCf(c) 64 | return c, r, 200 65 | end 66 | -------------------------------------------------------------------------------- /src/fSaveI.lua: -------------------------------------------------------------------------------- 1 | local z = ... 2 | 3 | return function(v, fc) 4 | package.loaded[z] = nil 5 | z = nil 6 | local n = v.name 7 | local p = tonumber(v.pos) 8 | if not p then p = 0 end 9 | local f = v.file 10 | local fl = tonumber(v.flush) 11 | if fl == 0 then fl = nil end 12 | v = nil 13 | collectgarbage() 14 | local n1 = "~"..n 15 | local n2 = "-"..n1 16 | local f2 = file.open(n2, "a") 17 | if not f2 then 18 | fc("Can't open file: "..n2) 19 | return 20 | end 21 | if p ~= nil and p > 0 then 22 | local cur = f2:seek("end") 23 | if cur ~= p then 24 | f2:close(); f2 = nil 25 | fc("File seek error, pos: "..p..", cur: "..(cur or "nil")) 26 | return 27 | end 28 | end 29 | local function fc2() 30 | n1 = nil 31 | n2 = nil 32 | n = nil 33 | f = nil 34 | collectgarbage() 35 | fc(nil) 36 | end 37 | local function fc1() 38 | f2:close(); f2 = nil 39 | if fl then 40 | tmr.create():alarm(10, 0, function() 41 | if file.exists(n) then 42 | file.remove(n1) 43 | file.rename(n, n1) 44 | end 45 | if not file.rename(n2, n) then 46 | if not file.rename(n2, n) then 47 | file.rename(n1, n) 48 | fc("File rename error") 49 | return 50 | end 51 | end 52 | file.remove(n1) 53 | fc2() 54 | end) 55 | else 56 | fc2() 57 | end 58 | end 59 | if not f2:write(f) then 60 | tmr.create():alarm(10, 0, function() 61 | local cur = f2:seek("set", p) 62 | if cur ~= p or not f2:write(f) then 63 | f2:close(); f2 = nil 64 | fc("File write error, pos: "..p) 65 | else 66 | fc1() 67 | end 68 | end) 69 | else 70 | fc1() 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /demo.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 19 | Thermal Camera UI Demo 20 | 84 | 85 | 86 | Select one of the previously recorded examples of thermal data to play: 87 | 88 |
89 | 90 | 98 | 99 | -------------------------------------------------------------------------------- /src/stream.lua: -------------------------------------------------------------------------------- 1 | local function delC(d, c) 2 | if not pcall(function() 3 | local v, a = c:getpeer() 4 | prq[a..":"..v] = nil 5 | if d > 0 then 6 | c:on("sent",function() end) 7 | c:close() 8 | end end) 9 | then pcall(function() 10 | for k, v in pairs(prq) do 11 | local rm = true 12 | for i, y in ipairs(pr) do 13 | local w, a = y:getpeer() 14 | if k == a..":"..w then 15 | rm = false 16 | break 17 | end 18 | end 19 | if rm then prq[k] = nil end 20 | end end) 21 | end 22 | collectgarbage() 23 | end 24 | 25 | local function delCd(d, c) 26 | if d > 0 then 27 | table.remove(pr, d) 28 | else 29 | for i, v in ipairs(pr) do 30 | if v == c then table.remove(pr, i) end 31 | end 32 | end 33 | if c then delC(d, c) end 34 | if #pr == 0 then 35 | for k, v in pairs(prq) do prq[k] = nil end 36 | uart.on("data") 37 | uart.write(0, "\165\53\1\219") 38 | emi = nil 39 | em0 = nil 40 | collectgarbage() 41 | pr = {} 42 | prq = {} 43 | fq = {} 44 | fqw = 0 45 | fp = "" 46 | fpw = 0 47 | end 48 | end 49 | 50 | local function onSent(q, c) 51 | if not pcall(function() 52 | tmr.wdclr() 53 | local v, a = c:getpeer() 54 | a = a..":"..v 55 | v = prq[a] 56 | if v == fqw then 57 | if v > 0 then v = -v end 58 | elseif q == 0 or v <= 0 then 59 | if v < 0 then v = -v end 60 | v = (v == 4 and 1 or (v + 1)) 61 | c:send(fq[v]) 62 | end 63 | prq[a] = v 64 | end) then 65 | delCd(q, c) 66 | end 67 | collectgarbage() 68 | end 69 | 70 | local function onUart(d) 71 | if fpw == 0 or fpw == 7 then 72 | if #d == 222 then 73 | local s1, st = d:find("\90\90") 74 | if not s1 then 75 | fpw = 0 76 | uart.on("data", 222, onUart, 0) 77 | return 78 | elseif s1 > 1 then 79 | fp = d:sub(s1) 80 | uart.on("data", 222 - #fp, onUart, 0) 81 | return 82 | else 83 | fp = d 84 | end 85 | else 86 | fp = fp..d 87 | end 88 | fpw = 0 89 | end 90 | uart.on("data", fpw == 5 and 212 or 222, onUart, 0) 91 | fpw = fpw + 1 92 | if fpw == 1 then 93 | return 94 | elseif fpw == 4 then 95 | fp = d 96 | else 97 | local l = (fpw == 7) 98 | fp = fp..d 99 | if fpw == 3 or l then 100 | fqw = (fqw == 4 and 1 or (fqw + 1)) 101 | local fp1 = encoder.toBase64(fp) 102 | fq[fqw] = string.format('%X', #fp1 + (l and 9 or 0)).."\r\n"..fp1..(l and string.format('%8X\n', tmr.now()) or "").."\r\n" 103 | fp = "" 104 | fp1 = nil 105 | if l then for i, c in ipairs(pr) do onSent(i, c) end end 106 | end 107 | end 108 | collectgarbage() 109 | end 110 | 111 | return function(c,p,u) 112 | p = nil 113 | if u > 1 then 114 | require("rs")(c, 401) 115 | return 116 | end 117 | local prc = #pr 118 | pr[prc + 1] = c 119 | if #pr > 3 then 120 | delCd(1, pr[1]) 121 | end 122 | c:on("sent", function(ci) onSent(0, ci) end) 123 | require("rs")(c, 200, nil, 'text/plain') 124 | local v, a = c:getpeer() 125 | prq[a..":"..v] = 0 126 | if prc == 0 then 127 | local t = tmr.create() 128 | t:register(100,0,function() 129 | require("thermo")("I", function() 130 | uart.on("data", 222, onUart, 0) 131 | uart.write(0, "\165\53\2\220") 132 | end) 133 | end) 134 | t:start() 135 | end 136 | collectgarbage() 137 | end 138 | -------------------------------------------------------------------------------- /src/cfg.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | Configure 18 | 132 | 133 | 134 |
135 |

Connect to wireless network:

136 | 137 | 138 | 142 | 143 | 144 | 145 | 146 | 147 |
Name: 139 |
Password:
148 |
149 |
150 |

Setup the thermal sensor:

151 | 152 | 153 | 154 | 155 |
156 |
157 |
158 | 166 | 167 | -------------------------------------------------------------------------------- /src/ide.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | IDE 12 | 194 | 195 | 196 | 197 |

198 |
199 | File: 202 | 203 | 204 | 205 | 206 | 207 |

208 |
209 | 210 | 218 | 219 | -------------------------------------------------------------------------------- /src/ju.js: -------------------------------------------------------------------------------- 1 | function StringBuffer() { 2 | var r = []; 3 | 4 | function p(v) { 5 | if(v) r.push(v); 6 | } 7 | 8 | function pp(v, len, ch) { 9 | if(!ch) ch = '0'; 10 | v = (v ? v.toString() : ""); 11 | if(v.length < len) { 12 | for(var i = v.length; i < len; ++i) p(ch); 13 | } 14 | p(v); 15 | } 16 | 17 | function p1(v, l, s) { 18 | pp(v, l); 19 | p(s); 20 | } 21 | 22 | return { 23 | toString : function() { 24 | return r.join(''); 25 | }, 26 | push : p, 27 | pushPadded : pp, 28 | pushTimestamp : function() { 29 | var d = new Date(); 30 | p1(d.getDate(), 2, '.'); 31 | p1(d.getMonth() + 1, 2, '.'); 32 | p1(d.getFullYear(), 4, ' '); 33 | p1(d.getHours(), 2, ':'); 34 | p1(d.getMinutes(), 2, ':'); 35 | p1(d.getSeconds(), 2, ':'); 36 | p1(d.getMilliseconds(), 3); 37 | } 38 | }; 39 | }; 40 | 41 | function getTimestamp() { 42 | var b = StringBuffer(); 43 | b.pushTimestamp(); 44 | return b.toString(); 45 | } 46 | 47 | function empty(s) { 48 | return (!s || s.length == 0); 49 | } 50 | 51 | function fixRN(s) { 52 | return s ? s.replace( /\r?\n/g, "\n" ) : s; 53 | } 54 | 55 | function createXmlHttp(handler, rsMin) { 56 | if(rsMin === undefined) rsMin = 4; 57 | var x = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); 58 | x.onreadystatechange = function() { 59 | if(x.readyState >= rsMin) { 60 | handler(x.responseText, x.status); 61 | } 62 | } 63 | return x; 64 | } 65 | 66 | function http(u, b, f, rsMin) { 67 | if(empty(u) || !f) return false; 68 | if(rsMin === undefined) rsMin = 4; 69 | b = fixRN(b); 70 | var q = createXmlHttp(function(t, s) { 71 | if(s == 0 || s == 200) { 72 | f(t, s); 73 | } else { 74 | f("" + s + ": " + t, s); 75 | } 76 | }, rsMin); 77 | q.open(b != null ? "POST" : "GET", u, true); 78 | try { 79 | q.send(b); 80 | } catch(e) { 81 | f(e, -1); 82 | } 83 | return q; 84 | } 85 | 86 | function upload(n, b, f) { 87 | if(empty(n) || empty(b) || !f) return false; 88 | b = fixRN(b); 89 | var p = 0; 90 | var CHS = 512; 91 | function sCh() { 92 | var q = "/fSave?name=" + uri(n) + "&pos=" + p; 93 | var d = CHS; 94 | if((p + d) >= b.length) q += "&flush=1"; 95 | return http(q, b.substr(p, d), function(re, s) { 96 | if(s == 200) { 97 | p += d; 98 | (p < b.length) ? sCh() : f(true); 99 | } else { 100 | confirm("Error when uploading " + n + ": " + re + ". Try again?") ? sCh() : f(false); 101 | } 102 | }); 103 | }; 104 | sCh(); 105 | } 106 | 107 | dc = document; 108 | function hT(v,t,a) {return "<"+ t + (a ? " " + a : "") + (v ? ">" + v + "" : "/>");} 109 | function hO(v) {return hT(v, "option") + "\n";} 110 | function hArg(n,v) {return ((v != null) ? ' ' + n + '="' + v + '"' : '');} 111 | function hB(v) {return hT(v, "b");} 112 | function hTd(v, a) {return hT(v, "td", a);} 113 | function hTr(v, a) {return hT(v, "tr", a) + "\n";} 114 | function hInput(t,i,n,v,a){return hT(null, 'input', hArg('type', t) + hArg('id', i) + hArg('name', n) + hArg('value', v) + (a?" " + a:""));} 115 | function hRadio(l,v,i,n,s,c) {return hInput('radio', i, n, v, hArg('onclick', c) + hArg('checked', (s?"checked":null))) + hT(l, "label", 'for="' + i + '"');} 116 | function hInputH(i,n,v,a) {return hInput("hidden",i,n,v,a);} 117 | function hInputB(i,n,v,a) {return hInput("button",i,n,v,a);} 118 | function hInputC(i,n,v,a) {return hInput("checkbox",i,n,v,a);} 119 | function hInputT(i,n,v,a) {return hInput("text",i,n,v,a);} 120 | function hInputP(i,n,v,a) {return hInput("password",i,n,v,a);} 121 | function hButton(t,l,n,v,a) {return hT(l, 'button', hArg('type', t) + hArg('value', v) + hArg('name', n) + (a?" " + a:""));} 122 | function hButtonS(l,n,v) {return hButton('submit',l,n,v);} 123 | function hFRow(l,c,h) {return hTr(hTd(hB(l)) + hTd(c) + (h?hTd(h):''));} 124 | function ce(c, t) { 125 | var e = dc.createElement(t ? t : 'div'); 126 | if(c) e.className = c; 127 | return e; 128 | } 129 | function ge(id) {return dc.getElementById(id);} 130 | function gt(o, tag) {return o.getElementsByTagName(tag);} 131 | function euri(v) {return encodeURIComponent(v);} 132 | function duri(v) {return decodeURIComponent(v);} 133 | function uri(v) {return euri(v).replace(/%20/g,'+');} 134 | function escp(v) {return v.replace(/\\/g,'\\\\').replace(/\"/g,'\\\"');} 135 | function sh(e,v) {e.innerHTML = v;} 136 | function gv(e) {return e.value;} 137 | function sv(e,v) {e.value = v;} 138 | function ric(r) { 139 | var i = 0; 140 | return function() {return r.insertCell(i++);}; 141 | } 142 | 143 | function getFormValue(e) { 144 | var t = null; 145 | if(e.length != null) t = e[0].type; 146 | if(!t) t = e.type; 147 | if(!t) return null; 148 | 149 | switch(t) { 150 | case 'radio': 151 | for(var i = 0; i < e.length; ++i) { 152 | if(e[i].checked) return gv(e[i]); 153 | } 154 | return null; 155 | case 'select-multiple': 156 | var r = []; 157 | for(var i = 0; i < e.length; ++i) { 158 | if(e[i].selected) r.push(gv(e[i])); 159 | } 160 | return r.join(','); 161 | case 'checkbox': 162 | return e.checked; 163 | default: 164 | return gv(e); 165 | } 166 | } 167 | 168 | function getEncodeType(n) { 169 | if(n == 'l' || n == 'f') return 2; 170 | if(n == 'p' || n == 'i' || n == 'v' || n == 'u') return 0; 171 | return 1; 172 | } 173 | 174 | function getForm(f, m, eq) { 175 | var r = []; 176 | var n = false; 177 | function p(v) {r.push(v);} 178 | p('{'); 179 | for(var i = 0; i < f.elements.length; ++i) { 180 | var e = f.elements[i]; 181 | if(!e.name) continue; 182 | var v = getFormValue(e); 183 | if(m) v = m(f, e.name, v); 184 | if(!v) continue; 185 | if(n) p(','); 186 | p('"' + e.name + '"' + eq); 187 | if(v === true) { 188 | p(1); 189 | } else if(v === false) { 190 | p(0); 191 | } else if(typeof v.replace === 'undefined') { 192 | p(v); 193 | } else { 194 | var t = getEncodeType(e.name); 195 | if(t > 0) { 196 | p('"' + (t > 1 ? euri(v) : escp(v)) + '"'); 197 | } else { 198 | p(v); 199 | } 200 | } 201 | n = true; 202 | } 203 | p('}'); 204 | return r.join(""); 205 | } 206 | 207 | function getFormJson(f, m) { 208 | return getForm(f, m, ":"); 209 | } 210 | 211 | function postForm(f, u, v, vv, r, h, m) { 212 | if(v) { 213 | if(!v(f, vv)) return false; 214 | } 215 | var d = getFormJson(f, m); 216 | http(u, d, function(re, s) { 217 | if(!empty(r)) { 218 | sh(ge(r), re); 219 | } 220 | if(h) h(re, s); 221 | }); 222 | return false; 223 | } 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thermal Camera Streamer 2 | In brief, ESP8266 streams the thermal data captured with GY-MCU90640 module to web clients. 3 | The project is inspired by [The Easiest Thermal Camera Build You’ll Ever See](https://hackaday.com/2019/03/01/the-easiest-thermal-camera-build-youll-ever-see/). 4 | 5 | ## Features 6 | * The 32x24 thermal frames, taken with the MLX90640 sensor, are streamed by ESP8266 to HTML5 capable web clients, where the frames are interpolated and shown on web UI; 7 | * The frame sequence, accumulated in web browser memory, can be saved as a file for later playing back and analyzing; 8 | * Web UI allows setting of up to 10 points for monitoring the temperature of any of the existing 768 points of the thermal picture, those points are persisted in file along with the thermal frames; 9 | * Wi-Fi connection (e.g. AP configuring, or connecting to the existing Wi-Fi network) can be configured through the Web interface; 10 | * Everything needed for streaming thermal data and showing it on browser is hosted on ESP8266 HTTP server locally, so no Internet required; 11 | * There is a built-in IDE for editing the software through Web interface (useful if there is a plenty of space left on ESP flash file system); 12 | * Up to 3 simultanious web sessions are allowed to receive a stream of thermal frames. If fourth client connects, the server removes the oldest web client recepient. *The maximum amount of simultanious client sessions may be later increased if the code is executed on more powerful NodeMCU hardware, e.g. ESP32 (not checked yet)*; 13 | * Responsive Web UI (usable from smartphones); 14 | * NodeMCU version agnostic (polyfills allow to work on on NodeMCU 1.5.4.1 - 3.0.0). 15 | 16 | ### Web UI overview: 17 | 18 | #### Thermal data player UI demo 19 | Navigate the following link [to play back pre-recorded thermal data](https://dev-lab.github.io/esp-thermal-camera-streamer/demo.htm). 20 | 21 | #### UI overview: 22 | ![Web UI Overview (video)](https://youtu.be/NmfaSpEZDmY) 23 | 24 | #### Setup Wi-Fi access point: 25 | [![Setup Wi-Fi access point (animation)](https://github.com/dev-lab/blob/blob/master/iot-power-strip/generic-switch-web-ui-config-wifi-ap-pic.png)](https://github.com/dev-lab/blob/blob/master/iot-power-strip/generic-switch-web-ui-config-wifi-ap.gif) 26 | 27 | #### Setup `admin` authentication: 28 | [![Setup admin authentication (animation)](https://github.com/dev-lab/blob/blob/master/iot-power-strip/generic-switch-web-ui-config-admin-auth-pic.png)](https://github.com/dev-lab/blob/blob/master/iot-power-strip/generic-switch-web-ui-config-admin-auth.gif) 29 | 30 | ## BOM 31 | * GY-MCU90640 module. The module consists of MLX90640 thermal sensor array, and STM32 microcontroller that allows communication through serial port. Please refer to [GY-MCU90640 User Manual](https://github.com/vvkuryshev/GY-MCU90640-RPI-Python/blob/master/GY_MCU9064%20user%20manual%20v1.pdf) for details. 32 | * ESP8266 module (any board shall fit ok here, but if you are going to hack the software using built-in IDE, the board with more than 512Kb of FLASH is preferable). 33 | 34 | ## Schema 35 | The schematic diagram can't be simpler: 36 | * connect TX of the GY-MCU90640 to RX of the ESP8266; 37 | * connect RX of the GY-MCU90640 to TX of the ESP8266; 38 | * supply the power for both modules (note, that GY-MCU90640 accepts any voltage between 3V and 5V, while ESP module usually requires 3.3V). 39 | If you use a simpler ESP module (like ESP-01), rather than ESP Dev Board (NodeMCU module), you may have to wire more ESP pins (usually CH_PD and RST must be connected to +3.3v). 40 | Here is a prototype I used for debugging: 41 | ![GY-MCU90640 connected with the ESP8266 NodeMCU module](./images/esp8266-mlx90640-prototype-s.jpg) 42 | 43 | 44 | ## Setup 45 | 1. Flash the ESP8266 module with the NodeMCU firmware as described in [Flashing the firmware](https://nodemcu.readthedocs.io/en/release/flash/) with any tool you like (e.g. [esptool.py](https://github.com/espressif/esptool), [NodeMCU Flasher](https://github.com/nodemcu/nodemcu-flasher) etc.). The custom NodeMCU firmware can be built online with the service: [NodeMCU custom builds](https://nodemcu-build.com/). If your ESP8266 EEPROM is only 512Kb, you have to take extra steps to be sure that firmware is as small as possible, and there is enough space for project files in EEPROM file system. So, build your custom firmware based on 1.5.4.1-final NodeMCU with the only 8 modules selected: `cjson`, `encoder`, `file`, `net`, `node`, `tmr`, `uart`, `wifi`, no TLS, no debug, and take the integer version of it (its size is 395300 bytes in my case). In the case if your ESP module goes with larger EEPROM installed, you can build even the most recent NodeMCU version (select the `release` option on online build tool) with more modules selected, just make sure that at least 70Kb of file system space is available when you run NodeMCU. Make sure the following 8 modules are selected when you are building the firmware: 46 | * `encoder`; 47 | * `file`; 48 | * `net`; 49 | * `node`; 50 | * `SJSON` (for new NodeMCU; if you build older NodeMCU (1.5.4.1) select `cJSON` instead); 51 | * `timer`; 52 | * `UART`; 53 | * `WiFi`. 54 | 2. Upload the project files (all the files from [src/](src/) directory) to the ESP module (read the [Uploading code](https://nodemcu.readthedocs.io/en/release/upload/) how to do it). If you prefer, you can use the tool for uploading NodeMCU files from https://github.com/dev-lab/esp-nodemcu-lua-uploader. Upload the software with either [install.sh](install.sh), or [install.bat](install.bat) depending on your OS. 55 | 3. Restart the ESP8266 module (turn it off and turn back on). After restarting you will see a new Wi-Fi access point with the name: `esp-devlab-setup`. You will be able to connect to the module with the default password: `We1c0me!`. The default Wi-Fi AP name and password are specified in file: [`connect.lua`](./src/connect.lua). 56 | 4. On successful connection to the `esp-devlab-setup` Access Point you will be able to reach the Web UI through the browser by typing anything looking like domain name as an URL, e.g.: `any.site.my`. You can do that because the software starts a DNS liar server (it responds with the ESP8266 IP to any DNS request) in AP mode. 57 | 5. The thermal sensor module has to be initialized. For doing that, you have to navigate to Configure tab of Web UI, and press **Setup** button in **Setup the thermal sensor** section. After a couple of seconds, on successful initialization, you will see the message: *Turn-off and then turn-on the power to complete setup.*. That means that the thermal module was initialized and setting are stored in CY-MCU90640 EEPROM. After that, turn-off power supply for both modules, and then turn-on for normal use. That procedure shall be done only **once**, here is short video: [Setup the thermal sensor](https://youtu.be/Ak7GxvKt0M8). 58 | 6. Web UI shall be quite self-explaining to use. You only have to remember that the best way to brick the software is to use Web IDE without checking twice what you are uploading to the file system. The changes are taken into account immediately. A bricked NodeMCU can be cured only with connecting of ESP module to computer through UART, formatting of NodeMCU file system, and rewriting the Lua software. In some cases you even have to re-flash the NodeMCU (e.g. if you did the mistake and removed the delay in [`init.lua`](./src/init.lua). 59 | 60 | ## [License](./LICENSE) 61 | Copyright (c) 2020-2021 Taras Greben 62 | 63 | Licensed under the [Apache License](./LICENSE). 64 | -------------------------------------------------------------------------------- /src/ctl.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | Camera 22 | 725 | 726 | 727 |
728 |
729 | 730 | 731 | 732 |
733 |
734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 |
748 | 749 | 750 | 751 | 752 |
753 |
754 | 762 | 763 | --------------------------------------------------------------------------------