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 |
149 |
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 + "" + t + ">" : "/>");}
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 | 
23 |
24 | #### Setup Wi-Fi access point:
25 | [](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 | [](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 | 
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 |