Скрипт на луне для ESP8266. Мониторинг, управление и обновление ПО по WiFi.

Logik
Offline
Зарегистрирован: 05.08.2014

Попалась мне темка, дома организовать очень простенькое управление устройством - 2-3 дискретных выход, возможно вход один. В перспективе не сложней одного ШИМ. Разумеется управлять с мобильника или планшета. Прямо из броузера проще всего. Подумалось о ESP и это как бы намекнуло, что и скриптик управления тоже можна заливать по тому же каналу по  WiFi. Это очень кстати, т.к. разбирать для заливки не хочется совсем, а выводить спецом разем для этого практически не возможно. В общем попробовал и написал скетч, который это делает, его сейчас и  выложу, если не запретит тот, кто всегда запрещает ;)

Logik
Offline
Зарегистрирован: 05.08.2014

Тестился с на ESP8266-01 с NodeMCU 0.9.6 build 20150627  powered by Lua 5.1.4. Броузер Опера 36.0.2130.80 , но это думаю не принципиально. Код.

local WebSockAnsv="HTTP/1.1 101 Switching Protocols\r\n\Upgrade: websocket\r\n\Connection: Upgrade\r\n\Sec-WebSocket-Accept: "
local WebSocketEnd="\r\n";
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

local HTTP_html="HTTP/1.1 200 OK\nServer: ESP (nodeMCU)\nContent-Length: "
local html=[[
<head><meta charset="utf-8"></head><h1>WebSocket for ESP8266</h1>
<table><tr><td><input type="button" onclick="OpenWS()" value="Open"/></td><td>
 <input type="button" onclick="CloseWS()" value="Close"/>
</td><td><input type="button" onclick="snd('?print(node.heap())')" value="Send"/>
</td><td><input type="button" onclick="ld()" value="Load"/></td>
</td></tr></table><input type="file" id="fn"/>
<p style="color:red" id="time">--</p><p style="color:red" id="state">--</p>
<table><tr><th>GPIO #</th><th>0</th><th>1</th><th>2</th><th>3</th></tr>
<tr><td>Input</td><td><span id="g0">?</span></td><td><span id="g1">?</span></td>
<td><span id="g2">?</span></td><td><span id="g3">?</span></td></tr>
<tr><td rowspan="3">Set mode</td>
<td><input type="button" onclick="snd('g0i')" value="Inp"></td>
<td><input type="button" onclick="snd('g1i')" value="Inp"></td>
<td><input type="button" onclick="snd('g2i')" value="Inp"></td>
<td><input type="button" onclick="snd('g3i')" value="Inp"></td></tr>
<tr><td><input type="button" onclick="snd('g01')" value="Set HIGH"></td>
<td><input type="button" onclick="snd('g11')" value="Set HIGH"></td>
<td><input type="button" onclick="snd('g21')" value="Set HIGH"></td>
<td><input type="button" onclick="snd('g31')" value="Set HIGH"></td></tr>
<tr><td><input type="button" onclick="snd('g00')" value="Set LOW"></td>
<td><input type="button" onclick="snd('g10')" value="Set LOW"></td>
<td><input type="button" onclick="snd('g20')" value="Set LOW"></td>
<td><input type="button" onclick="snd('g30')" value="Set LOW"></td></tr>
</table>
<p id="all"></p>
<script language="Javascript">
ESP_URL = "ws://192.168.0.20:5000";
cmdPas="^9h87hg8yg";
cmdOpen='?file.remove("tmp") file.open("tmp","w+")';
cmdClose='?file.flush() file.close() file.remove("http_serv.lua") file.rename("tmp","http_serv.lua") PRG_mode=nil';
var pr;
var prpos;var socketW=null;
function getIn(o,s){document.getElementById(o).innerHTML=s;}
function addLog(s){getIn('all',document.getElementById('all').innerHTML+s+'<br>');}
function RecWS(messageEvent)
{var d=messageEvent.data;addLog(d);
  var s=d.substr(1);
 if(d.charAt(0)=='t') {getIn('time',s);if(Prg>0) Prg++;}
 else if(d.charAt(0)=='s') getIn('state',s);
 else if(d.charAt(0)=='g') getIn('g'+d.charAt(1),d.charAt(2));
 if(Prg>0){socketW.send(cmdPas);socketW.onmessage=RecPRG;}}
function OnOpen() {funcPing();}
function OnClose(Event) {socketW=null;getIn('time',"???")}
function OpenWS(){
 if(socketW==null) socketW= new WebSocket(ESP_URL);
  if(socketW!=null) {socketW.onmessage =RecWS;socketW.onopen = OnOpen;socketW.onclose = OnClose;}
}
function CloseWS(){ if(socketW!=null) socketW.close();}
function fdl(event) {pr=event.target.result;prpos=0;socketW.send(cmdOpen);}
function fde(event) {getIn('time')=event.data;}
function RecPRG(messageEvent)
{var d=messageEvent.data;addLog(d);
 if(pr){s=pr.substr(prpos,100);
  if(s!=""){ prpos=prpos+100;socketW.send("#"+s);getIn('time',prpos);return;}
  pr=null;socketW.send(cmdClose);socketW.onmessage=RecWS;Prg=0;}
 else if(d=="tPRG"){var fd=new FileReader();
  fd.onload=fdl;fd.onerror=fde;
  getIn('time',"PRG:"+document.getElementById("fn").files[0].name);
  fd.readAsText(document.getElementById("fn").files[0]);}
}
var Prg=0;
function ld(){Prg=1;}
function snd(d){if(Prg<2) if(socketW!=null) if (socketW.readyState==1) socketW.send(d);}
function funcPing(){snd("#print(node.chipid())");if(socketW!=null);
setTimeout(funcPing,1000);}
</script>]]
local TCP_connect = 0
local ProcessWS
local CountWS=0

function OpenWS(conn,payload)
  if(string.find(payload,"GET / HTTP/1.1\r\n")~=1) then return 1 end --РЅРµ HTTP
  if(string.find(payload,WebSocketEnd..WebSocketEnd)==0) then return 2 end --принят не весь
  local p1=string.find(payload,"Sec-WebSocket-Key: ", 1, true)
  if(p1==nil) then return 3 end --это HTTP но не WebSocket
  local key=string.sub(payload, p1+19)
  key=string.sub(key, 1, string.find(key,WebSocketEnd)-1)
  key=key.."258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  key=WebSockAnsv..enc(crypto.hash("sha1",key))..WebSocketEnd..WebSocketEnd
  conn:send(key)
  ProcessWS=ReciveWS
  return 0 end

local PRG_mode
local ofs
function ReciveWS(conn,payload) 
 ofs=0
 local U=string.len(payload)
 while (ofs<U) do
 local InpStr=encodeWS(payload)   
 local h=string.sub(InpStr, 1, 1)
 if(PRG_mode) then
  local v=string.sub(InpStr, 2)
  if(h=="?") then node.input(v) SendTextWS(conn, "?")
  else file.write(v) SendTextWS(conn, ">")end
 else
 if(h=="#") then
  SendTextWS(conn,"t "..CountWS)
  SendTextWS(conn,"g0"..gpio.read(8))
  SendTextWS(conn,"g1"..gpio.read(5)) ---TX
  SendTextWS(conn,"g2"..gpio.read(9))
  SendTextWS(conn, "g3"..gpio.read(4)) ---RX
 else if(h=="?") then
 print("h")
  SendTextWS(conn, "sheap="..node.heap().." Time="..tmr.now())
 else if(h=="g") then
   local p=string.sub(InpStr, 2, 2)
   if(p==0) then p=8 else if(p==1) then p=5 else if(p==2) then p=9 else p=4 end end end
   local p=nGPIO(string.sub(InpStr, 2, 2))
   local i=string.sub(InpStr, 3, 3)
   if(i==0) then gpio.mode(p,gpio.OUTPUT,gpio.FLOAT) gpio.write(p,gpio.LOW)
   else if(i==1) then gpio.mode(p,gpio.OUTPUT,gpio.FLOAT) gpio.write(p,gpio.HIGH) 
   else gpio.mode(p,gpio.INPUTT,gpio.FLOAT)  end end
 else if(h=="^") then
   if(string.sub(InpStr, 2)=="9h87hg8yg") then PRG_mode=conn SendTextWS(conn,"tPRG") end
   end end end end end
 end
 return 0
end

function enc(data)
 return ((data:gsub('.', function(x) 
    local r,b='',x:byte()
    for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
    return r;
end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
  if (#x < 6) then return '' end
  local c=0
  for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
   return b:sub(c+1,c+1)
 end)..({'','==','='})[#data%3+1])
end

local opcod
function encodeWS(payload) 
 opcod=bit.band(string.byte( payload, 1),0x0f)
 local flags=bit.bor(bit.rshift(string.byte( payload, 1),4), bit.band(string.byte( payload, 2),0x0f))   
 local len=bit.band(string.byte( payload, 2),0x7f)
 ofs=ofs+3
 if(len==126) then
   len=string.byte( payload, 3)+string.byte( payload, 4)*256
   ofs=ofs+2
 elseif (len==127) then
   len=string.byte( payload, 3)+256*(string.byte( payload, 4)+256*(string.byte( payload, 5)+256*string.byte( payload, 6)))
   ofs=ofs+8
 end;
 local mas={string.byte( payload, ofs),string.byte( payload, ofs+1),
      string.byte( payload, ofs+2),string.byte( payload, ofs+3)}
 local result=""
 ofs=ofs+4
 local i=1;
 repeat
  result=result..string.char(bit.bxor(mas[i],string.byte( payload, ofs)))
  len=len-1  ofs=ofs+1  i=i+1  if(i==5) then i=1 end
 until len==0 
 return result
end

function SendTextWS(conn,t)
  local l=string.len(t) if(l>125) then t="long!" l=5 end
  conn:send(string.char(bit.bor(opcod,0x80))..string.char(l)..t)
end

if(wifi.sta.status()~=5) then
  wifi.setmode(wifi.STATION)
  wifi.sta.config("*****","*****")
  wifi.sta.setip({ip="192.168.0.20",netmask="255.255.252.0",gateway="192.168.0.1"})
  wifi.sta.connect()
end 
if(srv==nil) then srv=net.createServer(net.TCP,5) end;
srv:listen(80,function(conn)end)
local html_index=0
local ConectCount=0;
ProcessWS=OpenWS
srv:listen(5000,function(conn) 
  TCP_connect = 1
  ConectCount=ConectCount+1
  conn:on("connection", function(conn)TCP_connect=2 end)
  conn:on("reconnection", function(conn)ProcessWS=OpenWS end)
  conn:on("disconnection", function(conn)
  TCP_connect=0 ConectCount=ConectCount-1 if(ConectCount==0) then ProcessWS=OpenWS end end)
  conn:on("sent", function(conn)
   if(html_index>0) then
     local ss=string.sub(html,html_index,html_index+1000)
     if(string.len(ss)==0) then html_index=0  conn:close() 
     else conn:send(ss) html_index=html_index+1001 end
    end;
   end)
  conn:on("receive",function(conn,payload) 
   if(ProcessWS(conn,payload) == 3) then
    conn:send(HTTP_html)
    conn:send(string.len(html)..WebSocketEnd..WebSocketEnd)
    html_index=1
   end end) 
end)

TCP_stat=0
tmr.alarm(0, 10, 1, function()
  CountWS=CountWS+1 
  if(TCP_stat~=TCP_connect) then TCP_stat=TCP_connect end end )
Для запуска у себя - прописать WiFi  сетку и пароль (в коде забиты **). Ну и айпишники под свою сеть поправить, там 192.168.0.20. Порт 80 - для выдаши HTML в броузер 5000 - для самого обмена по WebSocket, все на нем.
Logik
Offline
Зарегистрирован: 05.08.2014

Код с пылу- с жару,  сегодня дотестил. Но вроде работает, цикл обмена в 1 сек держит и софт обновляет ))

Теперь о проблемах. 

Скрипт большой, очень большой. Больше в девайс одним куском не лезет. Даже попытка вынести айпишник в строку в начале кода дает not enough memory. Тоже самое при попытке запуска после софт-ресета, только хард;) И после обновления софта тоже ребутнуть. По ходу вопрос 1.Кто знает более экономную версию прошивки? 

При разработке пробовал отправлять сообщения по инициативе ESP, но столкнулся с проблемой. Откуда отправлять? Из обработчика периодического по таймер глючит сильно, как при несинхронизации потока. Делать из вечного цикла, как бы некрвсиво, а любой делей в нем намертво убивает процесс обмена - в ТСP канале сразу куча ошибок, повторов передач. Вопрос 2. Кто то делал отправку сообщения по TCP не из обработчиков серверного сокета? Как?

Вобще там Send - источник гемора. В начале пока ответ на GET HTML укладывался в один TCP пакет, у меня 1514 байт, все работало без вопросов. По мере роста - при превышении, отправка одним куском выдавало ошибки. Побил на два куска, отправляемые сразу друг за другом. Работает, пока не доросло до 2*1514байт ))) Делить на 3 отправляемых подряд не помогает. Повесил отправку не первого куска в обработчик sent. Сново все работает, на в 8КБ проверил. Вопрос 3. Я правильно делаю или есть какой другой путь? Думал все. Нет не все. Когда вызовов Send чтало много мелких, и при обмене "тудысюды" раз в сек, снова начал подглючивать, отправляемые данные просто пропадали в, канале их нет. Начал обединять по типу Send(a) Send(b) Send(c) заменил на Send(a..b..c). Вроде попустило. Но не уверен. Вопрос 3 Это че за фигня с net.socket:send()?

Тепер самый прикол. Используются 2 порта, 80 и 5000. 80 открывается, но обработчика на 80 нет ))) Его запросы приходят на 5000, похоже на последний открытый ))) Вопрос 4. Че за фигня, эту багу ктото встречал, её фиксили?

С этой бы багой еще можна было бы жить, если б не запрос favicon.ico посреди обмена по WebSocket(( Вопрос 5 Как в ответе на GET HTML  сделать так чтоб сраную favicon.ico  не запрашивало? Т.е. можна как то ответить на запрос страницы так, чтоб про икону не спрашивало? Такое возможно? С учетом этого рекомендую открывать сокет секунд через 15 после открытия формы.

С учетом того, что скрипт "под завязку" в памяти (потому ни коментов ни форматирования нет и не будет), далее я собираюсь его разбить на 3-5 частей, как раз по логике там выходит. Соответственно уже одним куском не будет, не выложеш запросто и это собственно побудило опубликовать "как есть", т.к. если кому интересно - то стартонуть с этого проще.

 

ПС. Это мой первый код на луне, не считая хелоуворд, так что пинайте, не стесняйтесь ;) Только не про коменты и форматирование - не влазит оно.