diff --git a/.gitignore b/.gitignore index 0ac4a73..b9d369b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ wlancfg.lua +config.lua +simulation.config.lua *.swp unit/testTimesMarchOctoberWithAllSeconds.lua +diet/* diff --git a/Readme.md b/Readme.md index 36f3cf0..0d8df53 100644 --- a/Readme.md +++ b/Readme.md @@ -11,18 +11,25 @@ cd os/ ./flash.sh ttyUSB0 -Reboot the ESP, with a serial terminal, -format the filesystem with the following command and reboot it: +Connect to the ESP via a terminal emulator like screen using a baud rate of 115200. Then format the filesystem and reboot the ESP with the following commands:
 file.format()
-node.reboot()
+node.restart()
 
Then disconnect the serial terminal and copy the required files to the microcontroller:
-./tools/initialFlash.sh /dev/ttyUSB0
+./tools/initialDietFlash.sh /dev/ttyUSB0
 
+Install the optional packages: +
+./tools/initialDietFlash.sh /dev/ttyUSB0 mqtt.lua
+./tools/initialDietFlash.sh /dev/ttyUSB0 ds18b20.lua
+
+ + + ### Upgrade Determine the IP address of your clock and execute the following script: @@ -30,5 +37,69 @@ Determine the IP address of your clock and execute the following script: ./tools/remoteFlash.sh IP-Address -## Internal Setup +## Hardware Setup +Mandatory: * GPIO2 LEDs +* GPIO0 Bootloader (at start) +* GPIO0 factory reset (long during operation) + +Optional: +* ADC VT93N2, 48k light resistor +* GPIO4 DS18B20 Temperatur sensor + +## MQTT Interface +### Status +* **basetopic**/brightness **Current brightness in percent** +* **basetopic**/background **Current background color** +* **basetopic**/color **Current foreground color** +* **basetopic**/color1 **Current foreground color for first minute** +* **basetopic**/color2 **Current foreground color for second minute** +* **basetopic**/color3 **Current foreground color for third minute** +* **basetopic**/color4 **Current foreground color for fourth minute** +* **basetopic**/row1 **Current background color** +* **basetopic**/temp **Temperatur** + +### Commands +* **basetopic**/cmd/single + * ON **Set brightness to 100%** + * OFF **Set brightness to 0%** + * 0-100 **Set brightness to given value** + * #rrggbb **Background color is set to hex representation of red, green and blue** + * 0-255,0-255,0-255 **Background color is set to decimal representation of red, green an blue** +* **basetopic**/cmd/color + * 0-255,0-255,0-255 **Foreground color is set to decimal representation of red, green an blue** +* **basetopic**/cmd/color1 + * 0-255,0-255,0-255 **Foreground color for first minute is set to decimal representation of red, green an blue** +* **basetopic**/cmd/color2 + * 0-255,0-255,0-255 **Foreground color for second minute is set to decimal representation of red, green an blue** +* **basetopic**/cmd/color3 + * 0-255,0-255,0-255 **Foreground color for third minute is set to decimal representation of red, green an blue** +* **basetopic**/cmd/color4 + * 0-255,0-255,0-255 **Foreground color for fourth minute is set to decimal representation of red, green an blue** +* **basetopic**/cmd/telnet + * ignored **Stop MQTT server, clock and start telnetserver at port 23** +* **basetopic**/cmd/row1 + * 0-255,0-255,0-255 **Background color is set to decimal representation of red, green an blue** +* **basetopic**/cmd/row1 + * 0-255,0-255,0-255 **Background color is set to decimal representation of red, green an blue** +* **basetopic**/cmd/row2 + * 0-255,0-255,0-255 **Background color is set to decimal representation of red, green an blue** +* **basetopic**/cmd/row3 + * 0-255,0-255,0-255 **Background color is set to decimal representation of red, green an blue** +* For all rows... +* **basetopic**/cmd/row10 + * 0-255,0-255,0-255 **Background color is set to decimal representation of red, green an blue** + + +## OpenHAB2 +Tested MQTT with binding-mqtt 2.5.x +### Configuration +``` +Thing mqtt:topic:wordclock "Wordclock" (mqtt:broker) @ "MQTT" { + Channels: + Type dimmer : dim "Dimming" [ stateTopic="basetopic/brightness", commandTopic="basetopic/cmd/single" ] + Type string : cmd "Command" [ commandTopic="basetopic/cmd/single" ] + Type switch : active "Active" [ commandTopic="basetopic/cmd/single" ] + Type colorRGB : background "Background" [ stateTopic="basetopic/background", commandTopic="basetopic/cmd/single", on="28,0,0", off="0,0,0" ] +} +``` diff --git a/commands.lua b/commands.lua new file mode 100644 index 0000000..4f9ede5 --- /dev/null +++ b/commands.lua @@ -0,0 +1,186 @@ +function storeConfig(_ssid, _password, _timezoneoffset, _sntpserver, _inv46, _dim, _fcolor, _colorMin1, _colorMin2, _colorMin3, _colorMin4, _bcolor, _threequater) + +if ( (_ssid == nil) and + (_password == nil) and + (_timezoneoffset == nil) and + (_sntpserver == nil) and + (_inv46 == nil) and + (_dim == nil) and + (_fcolor == nil) and + (_colorMin1 == nil) and + (_colorMin2 == nil) and + (_colorMin3 == nil) and + (_colorMin4 == nil) and + (_bcolor == nil) and + (_threequater == nil) ) then + print("one parameter is mandatory:") + print("storeConfig(ssid, ") + print(" password,") + print(" timezoneoffset,") + print(" sntpserver,") + print(" inv46,") + print(" dim,") + print(" fcolor,") + print(" colorMin1,") + print(" colorMin2,") + print(" colorMin3,") + print(" colorMin4,") + print(" bcolor,") + print(" threequater)") + print(" ") + print("e.g.:") + print('storeConfig(nil, nil, 1, nil, "on", true, "00FF00", "00FF88", "008888", "00FF44", "004488", "000000", true)') + return +end + +if (_password==nil) then + _, password, _, _ = wifi.sta.getconfig() + print("Restore password") +else + password = _password +end +if (_ssid==nil) then + ssid, _, _, _ = wifi.sta.getconfig() +else + ssid = _ssid +end + +if (_sntpserver == nil) then + sntpserver = sntpserverhostname + print("Restore SNTP: " .. tostring(sntpserver)) +else + sntpserver = _sntpserver +end + +if (_timezoneoffset ~= nil) then +timezoneoffset = _timezoneoffset +end +if (_inv46 ~= nil) then +if ((_inv46 == true) or (_inv == "on")) then + inv46 = "on" +elseif ((_inv46 == false) or (_inv == "off")) then + inv46 = "off" +else + inv46 = "off" +end +end +if ( _dim ~= nil) then + dim = _dim +end +if (_fcolor ~= nil) then + fcolor = _fcolor +end +if (_bcolor ~= nil) then + bcolor = _bcolor +end +if (_colorMin1 ~= nil) then + colorMin1 = _colorMin1 +end +if (_colorMin2 ~= nil) then + colorMin2 = _colorMin2 +end +if (_colorMin3 ~= nil) then + colorMin3 = _colorMin3 +end +if (_colorMin4 ~= nil) then + colorMin4 = _colorMin4 +end +if (_threequater ~= nil) then + threequater = _threequater +end + +print("SSID = " .. tostring(ssid)) +print("TZNE = " .. tostring(timezoneoffset)) +print("NTP = " .. tostring(sntpserver)) +print("INVT = " .. tostring(inv46)) +print("DIM = " .. tostring(dim)) +print("FCOL = " .. tostring(fcolor)) +print("BCOL = " .. tostring(bcolor)) +print("MIN1 = " .. tostring(colorMin1)) +print("MIN2 = " .. tostring(colorMin2)) +print("MIN3 = " .. tostring(colorMin3)) +print("MIN4 = " .. tostring(colorMin4)) +print("3QRT = " .. tostring(threequater)) + +local configFile="config.lua" +-- Safe configuration: +file.remove(configFile .. ".new") +sec, _ = rtctime.get() +file.open(configFile.. ".new", "w+") +file.write("-- Config\n" .. "station_cfg={}\nstation_cfg.ssid=\"" .. ssid .. "\"\nstation_cfg.pwd=\"" .. password .. "\"\nstation_cfg.save=false\nwifi.sta.config(station_cfg)\n") +file.write("sntpserverhostname=\"" .. sntpserver .. "\"\n" .. "timezoneoffset=\"" .. timezoneoffset .. "\"\n".. "inv46=\"" .. tostring(inv46) .. "\"\n" .. "dim=\"" .. tostring(dim) .. "\"\n") + +if (fcolor ~= nil) then + local hexColor=string.sub(fcolor, 1) + local red = tonumber(string.sub(hexColor, 1, 2), 16) + local green = tonumber(string.sub(hexColor, 3, 4), 16) + local blue = tonumber(string.sub(hexColor, 5, 6), 16) + file.write("color=string.char(" .. green .. "," .. red .. "," .. blue .. ")\n") + -- fill the current values + color=string.char(green, red, blue) +end +if (colorMin1 ~= nil) then + local hexColor=string.sub(colorMin1, 1) + local red = tonumber(string.sub(hexColor, 1, 2), 16) + local green = tonumber(string.sub(hexColor, 3, 4), 16) + local blue = tonumber(string.sub(hexColor, 5, 6), 16) + file.write("color1=string.char(" .. green .. "," .. red .. "," .. blue .. ")\n") + color1=string.char(green, red, blue) +end +if ( colorMin2 ~= nil) then + local hexColor=string.sub(colorMin2, 1) + local red = tonumber(string.sub(hexColor, 1, 2), 16) + local green = tonumber(string.sub(hexColor, 3, 4), 16) + local blue = tonumber(string.sub(hexColor, 5, 6), 16) + file.write("color2=string.char(" .. green .. "," .. red .. "," .. blue .. ")\n") + color2=string.char(green, red, blue) +end +if ( colorMin3 ~= nil) then + local hexColor=string.sub(colorMin3, 1) + local red = tonumber(string.sub(hexColor, 1, 2), 16) + local green = tonumber(string.sub(hexColor, 3, 4), 16) + local blue = tonumber(string.sub(hexColor, 5, 6), 16) + file.write("color3=string.char(" .. green .. "," .. red .. "," .. blue .. ")\n") + color3=string.char(green, red, blue) +end +if ( colorMin4 ~= nil) then + local hexColor=string.sub(colorMin4, 1) + local red = tonumber(string.sub(hexColor, 1, 2), 16) + local green = tonumber(string.sub(hexColor, 3, 4), 16) + local blue = tonumber(string.sub(hexColor, 5, 6), 16) + file.write("color4=string.char(" .. green .. "," .. red .. "," .. blue .. ")\n") + color4=string.char(green, red, blue) +end +if ( bcolor ~= nil) then + local hexColor=string.sub(bcolor, 1) + local red = tonumber(string.sub(hexColor, 1, 2), 16) + local green = tonumber(string.sub(hexColor, 3, 4), 16) + local blue = tonumber(string.sub(hexColor, 5, 6), 16) + file.write("colorBg=string.char(" .. green .. "," .. red .. "," .. blue .. ")\n") + -- fill the current values + colorBg=string.char(green, red, blue) +end +if (getTime ~= nil) then + time = getTime(sec, timezoneoffset) + file.write("print(\"Config from " .. time.year .. "-" .. time.month .. "-" .. time.day .. " " .. time.hour .. ":" .. time.minute .. ":" .. time.second .. "\")\n") +end +if ( threequater ~= nil) then + file.write("threequater=true\n") + -- fill the current values + threequater=true +else + file.write("threequater=nil\n") -- unset threequater + -- fill the current values + threequater=nil +end +file.close() +collectgarbage() +sec=nil +file.remove(configFile) +if (file.rename(configFile .. ".new", configFile)) then + print("Rename Successfully") +else + print("Cannot rename " .. configFile .. ".new") +end + +end diff --git a/diet/webserver_diet.lua b/diet/webserver_diet.lua new file mode 100644 index 0000000..ba330cf --- /dev/null +++ b/diet/webserver_diet.lua @@ -0,0 +1,268 @@ +local o="config.lua" +local n=false +local t=0 +function sendPage(o,e,i) +collectgarbage() +print("Sending "..e.." "..t.."B already; "..node.heap().."B in heap") +o:on("sent",function(a) +if(t==0)then +a:close() +print("Page sent") +collectgarbage() +n=false +else +collectgarbage() +sendPage(a,e,i) +end +end) +if file.open(e,"r")then +local e="" +if(t<=0)then +e=e.."HTTP/1.1 200 OK\r\n" +e=e.."Content-Type: text/html\r\n" +e=e.."Connection: close\r\n" +e=e.."Date: Thu, 29 Dec 2016 20:18:20 GMT\r\n" +e=e.."\r\n\r\n" +end +file.seek("set",t) +local a=file.readline() +while(a~=nil)do +if(a:find("$")~=nil)then +if(i~=nil)then +for e,t in pairs(i) +do +a=string.gsub(a,e,t) +end +end +end +t=t+string.len(a) +e=e..a +if((string.len(e)>=500)or(node.heap()<2000))then +a=nil +o:send(e) +print("Sent part of "..t.."B") +return +else +a=file.readline() +end +end +t=0 +if(string.len(e)>0)then +o:send(e) +print("Sent rest") +end +end +end +function fillDynamicMap() +replaceMap={} +ssid,_=wifi.sta.getconfig() +if(ssid==nil)then return replaceMap end +if(sntpserverhostname==nil)then sntpserverhostname="ptbtime1.ptb.de"end +if(timezoneoffset==nil)then timezoneoffset=1 end +if(color==nil)then color=string.char(0,0,250)end +if(color1==nil)then color1=color end +if(color2==nil)then color2=color end +if(color3==nil)then color3=color end +if(color4==nil)then color4=color end +if(colorBg==nil)then colorBg=string.char(0,0,0)end +local t="#"..string.format("%02x",string.byte(color,2))..string.format("%02x",string.byte(color,1))..string.format("%02x",string.byte(color,3)) +local n="#"..string.format("%02x",string.byte(color1,2))..string.format("%02x",string.byte(color1,1))..string.format("%02x",string.byte(color1,3)) +local e="#"..string.format("%02x",string.byte(color2,2))..string.format("%02x",string.byte(color2,1))..string.format("%02x",string.byte(color2,3)) +local i="#"..string.format("%02x",string.byte(color3,2))..string.format("%02x",string.byte(color3,1))..string.format("%02x",string.byte(color3,3)) +local o="#"..string.format("%02x",string.byte(color4,2))..string.format("%02x",string.byte(color4,1))..string.format("%02x",string.byte(color4,3)) +local a="#"..string.format("%02x",string.byte(colorBg,2))..string.format("%02x",string.byte(colorBg,1))..string.format("%02x",string.byte(colorBg,3)) +replaceMap["$SSID"]=ssid +replaceMap["$SNTPSERVER"]=sntpserverhostname +replaceMap["$TIMEOFFSET"]=timezoneoffset +replaceMap["$THREEQUATER"]=(threequater and"checked"or"") +replaceMap["$ADDITIONAL_LINE"]="" +replaceMap["$HEXCOLORFG"]=t +replaceMap["$HEXCOLOR1"]=n +replaceMap["$HEXCOLOR2"]=e +replaceMap["$HEXCOLOR3"]=i +replaceMap["$HEXCOLOR4"]=o +replaceMap["$HEXCOLORBG"]=a +replaceMap["$INV46"]=((inv46~=nil and inv46=="on")and"checked"or"") +replaceMap["$AUTODIM"]=((dim~=nil and dim=="on")and"checked"or"") +return replaceMap +end +function startWebServer() +srv=net.createServer(net.TCP) +srv:listen(80,function(i) +i:on("receive",function(t,e) +if(n)then +print("HTTP sending... be patient!") +return +end +if(e:find("GET /")~=nil)then +n=true +if(color==nil)then +color=string.char(0,128,0) +end +ws2812.write(string.char(0,0,0):rep(56)..color:rep(2)..string.char(0,0,0):rep(4)..color:rep(2)..string.char(0,0,0):rep(48)) +if(sendPage~=nil)then +print("Sending webpage.html ("..tostring(node.heap()).."B free) ...") +replaceMap=fillDynamicMap() +sendPage(t,"webpage.html",replaceMap) +end +else if(e:find("POST /")~=nil)then +_,postdatastart=e:find("\r\n\r\n") +if postdatastart==nil then postdatastart=1 end +local a=string.sub(e,postdatastart+1) +local e={} +for t,a in string.gmatch(a,"(%w+)=([^&]+)&*")do +e[t]=a +end +if(e.action~=nil and e.action=="Reboot")then +node.restart() +return +end +if((e.ssid~=nil)and(e.sntpserver~=nil)and(e.timezoneoffset~=nil))then +print("New config!") +if(e.password==nil)then +_,password,_,_=wifi.sta.getconfig() +print("Restoring password : "..password) +e.password=password +password=nil +end +file.remove(o..".new") +sec,_=rtctime.get() +file.open(o..".new","w+") +file.write("-- Config\n".."station_cfg={}\nstation_cfg.ssid=\""..e.ssid.."\"\nstation_cfg.pwd=\""..e.password.."\"\nstation_cfg.save=false\nwifi.sta.config(station_cfg)\n") +file.write("sntpserverhostname=\""..e.sntpserver.."\"\n".."timezoneoffset=\""..e.timezoneoffset.."\"\n".."inv46=\""..tostring(e.inv46).."\"\n".."dim=\""..tostring(e.dim).."\"\n") +if(e.fcolor~=nil)then +print("Got fcolor: "..e.fcolor) +local e=string.sub(e.fcolor,4) +local t=tonumber(string.sub(e,1,2),16) +local a=tonumber(string.sub(e,3,4),16) +local e=tonumber(string.sub(e,5,6),16) +file.write("color=string.char("..a..","..t..","..e..")\n") +color=string.char(a,t,e) +end +if(e.colorMin1~=nil)then +local e=string.sub(e.colorMin1,4) +local t=tonumber(string.sub(e,1,2),16) +local a=tonumber(string.sub(e,3,4),16) +local e=tonumber(string.sub(e,5,6),16) +file.write("color1=string.char("..a..","..t..","..e..")\n") +color1=string.char(a,t,e) +end +if(e.colorMin2~=nil)then +local e=string.sub(e.colorMin2,4) +local a=tonumber(string.sub(e,1,2),16) +local t=tonumber(string.sub(e,3,4),16) +local e=tonumber(string.sub(e,5,6),16) +file.write("color2=string.char("..t..","..a..","..e..")\n") +color2=string.char(t,a,e) +end +if(e.colorMin3~=nil)then +local e=string.sub(e.colorMin3,4) +local t=tonumber(string.sub(e,1,2),16) +local a=tonumber(string.sub(e,3,4),16) +local e=tonumber(string.sub(e,5,6),16) +file.write("color3=string.char("..a..","..t..","..e..")\n") +color3=string.char(a,t,e) +end +if(e.colorMin4~=nil)then +local e=string.sub(e.colorMin4,4) +local t=tonumber(string.sub(e,1,2),16) +local a=tonumber(string.sub(e,3,4),16) +local e=tonumber(string.sub(e,5,6),16) +file.write("color4=string.char("..a..","..t..","..e..")\n") +color4=string.char(a,t,e) +end +if(e.bcolor~=nil)then +local e=string.sub(e.bcolor,4) +local t=tonumber(string.sub(e,1,2),16) +local a=tonumber(string.sub(e,3,4),16) +local e=tonumber(string.sub(e,5,6),16) +file.write("colorBg=string.char("..a..","..t..","..e..")\n") +colorBg=string.char(a,t,e) +end +if(getTime~=nil)then +time=getTime(sec,timezoneoffset) +file.write("print(\"Config from "..time.year.."-"..time.month.."-"..time.day.." "..time.hour..":"..time.minute..":"..time.second.."\")\n") +end +if(e.threequater~=nil)then +file.write("threequater=true\n") +threequater=true +else +file.write("threequater=nil\n") +threequater=nil +end +file.close() +collectgarbage() +sec=nil +file.remove(o) +print("Rename config") +if(file.rename(o..".new",o))then +print("Successfully") +local e=tmr.create() +e:register(50,tmr.ALARM_SINGLE,function(e) +replaceMap=fillDynamicMap() +replaceMap["$ADDITIONAL_LINE"]="

New configuration saved

" +print("Send success to client") +sendPage(t,"webpage.html",replaceMap) +e:unregister() +end) +e:start() +else +local e=tmr.create() +e:register(50,tmr.ALARM_SINGLE,function(e) +replaceMap=fillDynamicMap() +replaceMap["$ADDITIONAL_LINE"]="

ERROR

" +sendPage(t,"webpage.html",replaceMap) +e:unregister() +end) +e:start() +end +else +replaceMap=fillDynamicMap() +replaceMap["$ADDITIONAL_LINE"]="

Not all parameters set

" +sendPage(t,"webpage.html",replaceMap) +end +else +print("Hello via telnet") +global_c=t +function s_output(e) +if(global_c~=nil) +then global_c:send(e) +end +end +node.output(s_output,0) +global_c:on("receive",function(t,e) +node.input(e) +end) +global_c:on("disconnection",function(e) +node.output(nil) +global_c=nil +end) +print("Welcome to Word Clock") +end +end +end) +i:on("disconnection",function(e) +print("Goodbye") +node.output(nil) +collectgarbage() +t=0 +end) +end) +end +collectgarbage() +wifi.setmode(wifi.SOFTAP) +cfg={} +cfg.ssid="wordclock" +cfg.pwd="wordclock" +wifi.ap.config(cfg) +local t=string.char(0,128,0) +local e=string.char(0,0,0) +local a=e:rep(6)..t..e:rep(7)..t:rep(3)..e:rep(44)..t:rep(3)..e:rep(50) +ws2812.write(a) +t=nil +e=nil +a=nil +print("Waiting in access point >wordclock< for Clients") +print("Please visit 192.168.4.1") +startWebServer() +collectgarbage() \ No newline at end of file diff --git a/displayword.lua b/displayword.lua index c1358e7..c3c1038 100644 --- a/displayword.lua +++ b/displayword.lua @@ -1,29 +1,8 @@ -- Module filling a buffer, sent to the LEDs local M do -local updateColor = function (data, inverseRow) - --FIXME magic missing to start on the left side - return data.colorFg -end -local drawLEDs = function(data, numberNewChars, inverseRow) - if (inverseRow == nil) then - inverseRow=false - end - if (numberNewChars == nil) then - numberNewChars=0 - end - local tmpBuf=nil - for i=1,numberNewChars do - if (tmpBuf == nil) then - tmpBuf = updateColor(data, inverseRow) - else - tmpBuf=tmpBuf .. updateColor(data, inverseRow) - end - data.drawnCharacters=data.drawnCharacters+1 - end - return tmpBuf -end +local data={} -- Utility function for round local round = function(num) @@ -38,220 +17,356 @@ local round = function(num) end end -local data={} - +-- @fn generateLEDs -- Module displaying of the words -local generateLEDs = function(words, colorForground, colorMin1, colorMin2, colorMin3, colorMin4) - -- Set the local variables needed for the colored progress bar - data={} +-- @param data struct with the following paramter: +-- aoC amount of characters for the complete message +-- mC amout of minutes to show +-- dC drawn characters +local updateColor = function (data) + if (data.aoC > 0) and (data.mC ~= nil) then + local specialChar = data.dC + if (data.mC < 1) then + specialChar = 0 + elseif (data.dC > data.mC) then + specialChar = 0 + end + if (specialChar < 1) then + return data.colorFg + elseif (specialChar < 2) then + return data.colorM1 + elseif (specialChar < 3) then + return data.colorM2 + elseif (specialChar < 4) then + return data.colorM3 + elseif (specialChar < 5) then + return data.colorM4 + else + return data.colorFg + end + else + return data.colorFg + end +end +local drawLEDs = function(data, offset, numberNewChars) + if (numberNewChars == nil) then + numberNewChars=0 + end + if (data.rgbBuffer == nil) then + return + end + for i=1,numberNewChars do + data.dC=data.dC+1 + data.rgbBuffer:set(tonumber(offset + i - 1), updateColor(data)) + end +end + +-- @fn swapLine +-- @param lineOffset offset (starting at 1) where the line is located to be swapped +-- works on the rgbBuffer, defined in data struct +-- @return false on errors, else true +local swapLine = function(data, lineOffset) + if (data.rgbBuffer == nil) then + return false + end + for i = 0,4 do + local num=tonumber(lineOffset)+i + local num2=tonumber(tonumber(lineOffset)+10-i) + local tmpC1, tmpC2, tmpC3=data.rgbBuffer:get(num) + local c1, c2, c3 =data.rgbBuffer:get(num2) + data.rgbBuffer:set(num, c1, c2, c3) + data.rgbBuffer:set(num2, tmpC1, tmpC2, tmpC3) + end + return true +end + +-- @fn generateLEDs +-- Module displaying of the words +-- @param rgbBuffer OutputBuffer with 114 LEDs +-- @param words +-- @param colorBg background color +-- @param colorFg foreground color +-- @param colorM1 foreground color if one minute after a displayable time is present +-- @param colorM2 foreground color if two minutes after a displayable time is present +-- @param colorM3 foreground color if three minutes after a displayable time is present +-- @param colorM4 foreground color if four minutes after a displayable time is present +-- @param invertRows wheather line 4,5 and 6 shall be inverted or not +-- @param aoC Amount of characters to be displayed +local generateLEDs = function(rgbBuffer, words, colorBg, colorFg, colorM1, colorM2, colorM3, colorM4, invertRows, aoC) + -- Set the local variables needed for the colored progress bar if (words == nil) then return nil end + if (invertRows == nil) then + invertRows=false + end - local minutes=1 - if (words.min1 == 1) then + local minutes=0 + if (words.m1 == 1) then minutes = minutes + 1 - elseif (words.min2 == 1) then + elseif (words.m2 == 1) then minutes = minutes + 2 - elseif (words.min3 == 1) then + elseif (words.m3 == 1) then minutes = minutes + 3 - elseif (words.min4 == 1) then + elseif (words.m4 == 1) then minutes = minutes + 4 end - if ( (adc ~= nil) and (words.briPercent ~= nil) ) then + -- always set a foreground value + if (colorFg == nil) then + colorFg = string.char(255,255,255) + end + + if (aoC ~= nil) then + data.aoC = aoC + data.mC = minutes + else + data.aoC = 0 + end + data.rgbBuffer = rgbBuffer + + if ( (adc ~= nil) and (words.briPer ~= nil) ) then local per = math.floor(100*adc.read(0)/1000) - words.briPercent = tonumber( ((words.briPercent * 4) + per) / 5) - print("Minutes : " .. tostring(minutes) .. " bright: " .. tostring(words.briPercent) .. "% current: " .. tostring(per) .. "%") - data.colorFg = string.char(string.byte(colorForground,1) * briPercent / 100, string.byte(colorForground,2) * briPercent / 100, string.byte(colorForground,3) * briPercent / 100) - data.colorMin1 = string.char(string.byte(colorMin1,1) * briPercent / 100, string.byte(colorMin1,2) * briPercent / 100, string.byte(colorMin1,3) * briPercent / 100) - data.colorMin2 = string.char(string.byte(colorMin2,1) * briPercent / 100, string.byte(colorMin2,2) * briPercent / 100, string.byte(colorMin2,3) * briPercent / 100) - data.colorMin3 = string.char(string.byte(colorMin3,1) * briPercent / 100, string.byte(colorMin3,2) * briPercent / 100, string.byte(colorMin3,3) * briPercent / 100) - data.colorMin4 = string.char(string.byte(colorMin4,1) * briPercent / 100, string.byte(colorMin4,2) * briPercent / 100, string.byte(colorMin4,3) * briPercent / 100) + words.briPer = tonumber( ((words.briPer * 4) + per) / 5) + print("Minutes : " .. tostring(minutes) .. " bright: " .. tostring(words.briPer) .. "% current: " .. tostring(per) .. "%") + data.colorFg = string.char(string.byte(colorFg,1) * briPer / 100, string.byte(colorFg,2) * briPer / 100, string.byte(colorFg,3) * briPer / 100) + data.colorM1 = string.char(string.byte(colorM1,1) * briPer / 100, string.byte(colorM1,2) * briPer / 100, string.byte(colorM1,3) * briPer / 100) + data.colorM2 = string.char(string.byte(colorM2,1) * briPer / 100, string.byte(colorM2,2) * briPer / 100, string.byte(colorM2,3) * briPer / 100) + data.colorM3 = string.char(string.byte(colorM3,1) * briPer / 100, string.byte(colorM3,2) * briPer / 100, string.byte(colorM3,3) * briPer / 100) + data.colorM4 = string.char(string.byte(colorM4,1) * briPer / 100, string.byte(colorM4,2) * briPer / 100, string.byte(colorM4,3) * briPer / 100) else -- devide by five (Minute 0, Minute 1 to Minute 4 takes the last chars) - data.colorFg=colorForground - data.colorMin1=colorMin1 - data.colorMin2=colorMin2 - data.colorMin3=colorMin3 - data.colorMin4=colorMin4 + data.colorFg=colorFg + data.colorM1=colorM1 + data.colorM2=colorM2 + data.colorM3=colorM3 + data.colorM4=colorM4 end - data.words=words - data.drawnCharacters=0 - data.drawnWords=0 + data.dC=0 -- drawn characters local charsPerLine=11 - -- Space / background has no color by default - local space=string.char(0,0,0) - -- set FG to fix value: - colorFg = string.char(255,255,255) - - -- Set the foreground color as the default color - local buf=colorFg - - -- line 1---------------------------------------------- - if (words.it==1) then - buf=drawLEDs(data,2) -- ES - else - buf=space:rep(2) + + -- Background color must always be set + if (colorBg ~= nil) then + rgbBuffer:fill(string.byte(colorBg,1), string.byte(colorBg,2), string.byte(colorBg,3)) -- draw the background end --- K fill character -buf=buf .. space:rep(1) + + local lineIdx=1 + -- line 1---------------------------------------------- + if (rowbgColor[1] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[1]) end + end + if (words.it==1) then + drawLEDs(data, lineIdx, 2) -- ES + end + -- K fill character if (words.is == 1) then - buf=buf .. drawLEDs(data,3) -- IST - else - buf=buf .. space:rep(3) + drawLEDs(data, lineIdx+3, 3) -- IST end -- L fill character -buf=buf .. space:rep(1) -if (words.fiveMin== 1) then - buf= buf .. drawLEDs(data,4) -- FUENF - else - buf= buf .. space:rep(4) + if (words.m5== 1) then + drawLEDs(data, lineIdx+7, 4) -- FUENF end -- line 2-- even row (so inverted) -------------------- - if (words.twenty == 1) then - buf= buf .. drawLEDs(data,7,true) -- ZWANZIG - else - buf= buf .. space:rep(7) + lineIdx=12 + if (rowbgColor[2] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[2]) end + end + if (words.m10 == 1) then + drawLEDs(data, lineIdx, 4) -- ZEHN end - if (words.tenMin == 1) then - buf= buf .. drawLEDs(data,4,true) -- ZEHN - else - buf= buf .. space:rep(4) + if (words.m20 == 1) then + drawLEDs(data, lineIdx + 4, 7) -- ZWANZIG end + -- swap line + swapLine(data,lineIdx) -- line3---------------------------------------------- - if (words.threequater == 1) then - buf= buf .. drawLEDs(data,11) -- Dreiviertel - elseif (words.quater == 1) then - buf= buf .. space:rep(4) - buf= buf .. drawLEDs(data,7) -- VIERTEL - else - buf= buf .. space:rep(11) + lineIdx=23 + if (rowbgColor[3] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[3]) end + end + if (words.h3q == 1) then + drawLEDs(data,lineIdx, 11) -- DREIVIERTEL + elseif (words.hq == 1) then + drawLEDs(data, lineIdx + 4, 7) -- VIERTEL end --line 4-------- even row (so inverted) ------------- - if (words.before == 1) then - buf=buf .. space:rep(2) - buf= buf .. drawLEDs(data,3,true) -- VOR - else - buf= buf .. space:rep(5) + lineIdx=34 + if (rowbgColor[4] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[4]) end + end + if (words.ha == 1) then + -- TG + drawLEDs(data, lineIdx + 2, 4) -- NACH end - if (words.after == 1) then - buf= buf .. drawLEDs(data,4,true) -- NACH - buf= buf .. space:rep(2) -- TG - else - buf= buf .. space:rep(6) + if (words.hb == 1) then + drawLEDs(data, lineIdx + 6, 3) -- VOR end - ------------------------------------------------ + if (invertRows ~= true) then + swapLine(data,lineIdx) + end + -- line 5 ---------------------------------------------- + lineIdx=45 + if (rowbgColor[5] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[5]) end + end if (words.half == 1) then - buf= buf .. drawLEDs(data,4) -- HALB - buf= buf .. space:rep(1) -- X - else - buf= buf .. space:rep(5) + drawLEDs(data, lineIdx, 4) -- HALB + -- X end - if (words.twelve == 1) then - buf= buf .. drawLEDs(data,5) -- ZWOELF - buf= buf .. space:rep(1) -- P - else - buf= buf .. space:rep(6) + if (words.h12 == 1) then + drawLEDs(data, lineIdx + 5,5) -- ZWOELF + -- P + end + if (invertRows == true) then + swapLine(data,lineIdx) end ------------even row (so inverted) --------------------- - if (words.seven == 1) then - buf= buf .. drawLEDs(data,6,true) -- SIEBEN - buf= buf .. space:rep(5) - elseif (words.oneLong == 1) then - buf= buf .. space:rep(5) - buf= buf .. drawLEDs(data,4,true) -- EINS - buf= buf .. space:rep(2) - elseif (words.one == 1) then - buf= buf .. space:rep(6) - buf= buf .. drawLEDs(data,3,true) -- EIN - buf= buf .. space:rep(2) - elseif (words.two == 1) then - buf= buf .. space:rep(7) - buf= buf .. drawLEDs(data,4,true) -- ZWEI - else - buf= buf .. space:rep(11) + lineIdx=56 + if (rowbgColor[6] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[6]) end + end + if (words.h7 == 1) then + drawLEDs(data, lineIdx + 5, 6) -- SIEBEN + elseif (words.h1l == 1) then + drawLEDs(data, lineIdx + 2,4) -- EINS + elseif (words.h1 == 1) then + drawLEDs(data, lineIdx + 2, 3) -- EIN + elseif (words.h2 == 1) then + drawLEDs(data, lineIdx, 4) -- ZWEI + end + if (invertRows ~= true) then + swapLine(data,lineIdx) end ------------------------------------------------ - if (words.three == 1) then - buf= buf .. space:rep(1) - buf= buf .. drawLEDs(data,4) -- DREI - buf= buf .. space:rep(6) - elseif (words.five == 1) then - buf= buf .. space:rep(7) - buf= buf .. drawLEDs(data,4) -- FUENF - else - buf= buf .. space:rep(11) + lineIdx=67 + if (rowbgColor[7] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[7]) end + end + if (words.h3 == 1) then + drawLEDs(data, lineIdx + 1,4) -- DREI + elseif (words.h5 == 1) then + drawLEDs(data, lineIdx + 7, 4) -- FUENF end ------------even row (so inverted) --------------------- - if (words.four == 1) then - buf= buf .. drawLEDs(data,4,true) -- VIER - buf= buf .. space:rep(7) - elseif (words.nine == 1) then - buf= buf .. space:rep(4) - buf= buf .. drawLEDs(data,4,true) -- NEUN - buf= buf .. space:rep(3) - elseif (words.eleven == 1) then - buf= buf .. space:rep(8) - buf= buf .. drawLEDs(data,3,true) -- ELEVEN - else - buf= buf .. space:rep(11) + lineIdx=78 + if (rowbgColor[8] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[8]) end + end + if (words.h4 == 1) then + drawLEDs(data, lineIdx + 7, 4) -- VIER + elseif (words.h9 == 1) then + drawLEDs(data, lineIdx + 3, 4) -- NEUN + elseif (words.h11 == 1) then + drawLEDs(data, lineIdx, 3) -- ELF end + swapLine(data,lineIdx) ------------------------------------------------ - if (words.eight == 1) then - buf= buf .. space:rep(1) - buf= buf .. drawLEDs(data,4) -- ACHT - buf= buf .. space:rep(6) - elseif (words.ten == 1) then - buf= buf .. space:rep(5) - buf= buf .. drawLEDs(data,4) -- ZEHN - buf= buf .. space:rep(2) - else - buf= buf .. space:rep(11) + lineIdx=89 + if (rowbgColor[9] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[9]) end + end + if (words.h8 == 1) then + drawLEDs(data, lineIdx + 1, 4) -- ACHT + elseif (words.h10 == 1) then + drawLEDs(data, lineIdx + 5, 4) -- ZEHN end + ------------even row (so inverted) --------------------- - if (words.clock == 1) then - buf= buf .. drawLEDs(data,3,true) -- UHR - else - buf= buf .. space:rep(3) - end - if (words.six == 1) then - buf= buf .. space:rep(2) - buf= buf .. drawLEDs(data,5,true) -- SECHS - buf= buf .. space:rep(1) - else - buf= buf .. space:rep(8) - end - - if (words.min1 == 1) then - buf= buf .. colorFg - else - buf= buf .. space:rep(1) - end - if (words.min2 == 1) then - buf= buf .. colorFg - else - buf= buf .. space:rep(1) + lineIdx=100 + if (rowbgColor[10] ~= nil) then + for i=lineIdx,lineIdx+10, 1 do data.rgbBuffer:set(i, rowbgColor[10]) end end - if (words.min3 == 1) then - buf= buf .. colorFg - else - buf= buf .. space:rep(1) + if (words.h6 == 1) then + drawLEDs(data, lineIdx + 1, 5) -- SECHS + end + if (words.cl == 1) then + drawLEDs(data, lineIdx + 8, 3) -- UHR + end + swapLine(data,lineIdx) +------ Minutes ----------- + if (words.m1 == 1) then + data.rgbBuffer:set(111, colorFg) + end + if (words.m2 == 1) then + data.rgbBuffer:set(112, colorFg) end - if (words.min4 == 1) then - buf= buf .. colorFg - else - buf= buf .. space:rep(1) + if (words.m3 == 1) then + data.rgbBuffer:set(113, colorFg) + end + if (words.m4 == 1) then + data.rgbBuffer:set(114, colorFg) end collectgarbage() - return buf end + +-- Count amount of characters to display +local countChars = function(words) + local characters = 0 + for key,value in pairs(words) do + if (value > 0) then + if (key == "it") then + characters = characters + 2 + elseif (key == "is") then + characters = characters + 3 + elseif (key == "m5") then + characters = characters + 4 + elseif (key == "m10") then + characters = characters + 4 + elseif (key == "ha") then + characters = characters + 4 + elseif (key == "hb") then + characters = characters + 3 + elseif (key == "h3") then + characters = characters + 4 + elseif (key == "hq") then + characters = characters + 7 + elseif (key == "h3q") then + characters = characters + 11 + elseif (key == "half") then + characters = characters + 4 + elseif (key == "h1") then + characters = characters + 3 + elseif (key == "h1l") then + characters = characters + 4 + elseif (key == "h2") then + characters = characters + 4 + elseif (key == "h3") then + characters = characters + 4 + elseif (key == "h4") then + characters = characters + 4 + elseif (key == "h5") then + characters = characters + 4 + elseif (key == "h6") then + characters = characters + 4 + elseif (key == "h7") then + characters = characters + 6 + elseif (key == "h8") then + characters = characters + 4 + elseif (key == "h9") then + characters = characters + 4 + elseif (key == "h10") then + characters = characters + 4 + elseif (key == "h11") then + characters = characters + 3 + elseif (key == "h12") then + characters = characters + 5 + elseif (key == "m20") then + characters = characters + 7 + elseif (key == "cl") then + characters = characters + 3 + end + end + end + return characters +end + M = { generateLEDs = generateLEDs, round = round, drawLEDs = drawLEDs, updateColor = updateColor, data = data, + countChars = countChars } end -displayword = M +dw = M diff --git a/ds18b20.lua b/ds18b20.lua new file mode 100644 index 0000000..fac19b4 --- /dev/null +++ b/ds18b20.lua @@ -0,0 +1,138 @@ +-------------------------------------------------------------------------------- +-- DS18B20 one wire module for NODEMCU +-- NODEMCU TEAM +-- LICENCE: http://opensource.org/licenses/MIT +-- Vowstar +-- 2015/02/14 sza2 Fix for negative values +-------------------------------------------------------------------------------- + +-- Set module name as parameter of require +local modname = ... +local M = {} +_G[modname] = M +-------------------------------------------------------------------------------- +-- Local used variables +-------------------------------------------------------------------------------- +-- DS18B20 dq pin +local pin = nil +-- DS18B20 default pin +local defaultPin = 9 +-------------------------------------------------------------------------------- +-- Local used modules +-------------------------------------------------------------------------------- +-- Table module +local table = table +-- String module +local string = string +-- One wire module +local ow = ow +-- Timer module +local tmr = tmr +-- Limited to local environment +setfenv(1,M) +-------------------------------------------------------------------------------- +-- Implementation +-------------------------------------------------------------------------------- +C = 0 +F = 1 +K = 2 +function setup(dq) + pin = dq + if(pin == nil) then + pin = defaultPin + end + ow.setup(pin) +end + +function addrs() + setup(pin) + tbl = {} + ow.reset_search(pin) + repeat + addr = ow.search(pin) + if(addr ~= nil) then + table.insert(tbl, addr) + end + tmr.wdclr() + until (addr == nil) + ow.reset_search(pin) + return tbl +end + +function readNumber(addr, unit) + result = nil + setup(pin) + flag = false + if(addr == nil) then + ow.reset_search(pin) + count = 0 + repeat + count = count + 1 + addr = ow.search(pin) + tmr.wdclr() + until((addr ~= nil) or (count > 100)) + ow.reset_search(pin) + end + if(addr == nil) then + return result + end + crc = ow.crc8(string.sub(addr,1,7)) + if (crc == addr:byte(8)) then + if ((addr:byte(1) == 0x10) or (addr:byte(1) == 0x28)) then + -- print("Device is a DS18S20 family device.") + ow.reset(pin) + ow.select(pin, addr) + ow.write(pin, 0x44, 1) + -- tmr.delay(1000000) + present = ow.reset(pin) + ow.select(pin, addr) + ow.write(pin,0xBE,1) + -- print("P="..present) + data = nil + data = string.char(ow.read(pin)) + for i = 1, 8 do + data = data .. string.char(ow.read(pin)) + end + -- print(data:byte(1,9)) + crc = ow.crc8(string.sub(data,1,8)) + -- print("CRC="..crc) + if (crc == data:byte(9)) then + t = (data:byte(1) + data:byte(2) * 256) + if (t > 32767) then + t = t - 65536 + end + if(unit == nil or unit == C) then + t = t * 625 + elseif(unit == F) then + t = t * 1125 + 320000 + elseif(unit == K) then + t = t * 625 + 2731500 + else + return nil + end + t = t / 100 + -- print("Temperature="..t1.."."..t2.." Centigrade") + -- result = t1.."."..t2 + return t + end + tmr.wdclr() + else + -- print("Device family is not recognized.") + end + else + -- print("CRC is not valid!") + end + return result +end + +function read(addr, unit) + t = readNumber(addr, unit) + if (t == nil) then + return nil + else + return t + end +end + +-- Return module table +return M diff --git a/index.html b/index.html deleted file mode 100644 index a938ea3..0000000 --- a/index.html +++ /dev/null @@ -1,22 +0,0 @@ - -WordClock Setup Page - -

Welcome to the WordClock

-
- - - - - - -" -" -" -" - - - - -
WIFI-SSID
WIFI-Password
SNTP Serverntp server to sync the time
Offset to UTC timeDefine the offset to UTC time in hours. E.g +1
Color
1. Minute Color
2. Minute Color
3. Minute Color
4. Minute Color
Three quaterDreiviertel Joa/nei
ColorModeIf checked, words are dark, rest is colored
-$ADDITIONAL_LINE - diff --git a/init.lua b/init.lua index 5a107f2..68aff81 100644 --- a/init.lua +++ b/init.lua @@ -2,67 +2,64 @@ uart.setup(0, 115200, 8, 0, 1, 1 ) print("Autostart in 5 seconds...") ws2812.init() -- WS2812 LEDs initialized on GPIO2 -MAXLEDS=110 -counter1=0 +local MAXLEDS=110 +local counter1=0 ws2812.write(string.char(0,0,0):rep(114)) -tmr.alarm(2, 85, 1, function() +local bootledtimer = tmr.create() +bootledtimer:register(75, tmr.ALARM_AUTO, function (t) counter1=counter1+1 spaceLeds = math.max(MAXLEDS - (counter1*2), 0) - ws2812.write(string.char(128,0,0):rep(counter1) .. string.char(0,0,0):rep(spaceLeds) .. string.char(0,0,64):rep(counter1)) -end) - -local blacklistfile="init.lua config.lua config.lua.new webpage.html" -function recompileAll() - for i=0,5 do tmr.stop(i) end - -- compile all files - l = file.list(); - for k,_ in pairs(l) do - if (string.find(k, ".lc", -3)) then - print ("Skipping " .. k) - elseif (string.find(blacklistfile, k) == nil) then - -- Only look at lua files - if (string.find(k, ".lua") ~= nil) then - print("Compiling and deleting " .. k) - node.compile(k) - -- remove the lua file - file.remove(k) - else - print("No code: " .. k) - end - end + ws2812.write(string.char(16,0,0):rep(counter1) .. string.char(0,0,0):rep(spaceLeds) .. string.char(0,0,8):rep(counter1)) + if ((counter1*2) > 114) then + t:unregister() end -end +end) +bootledtimer:start() function mydofile(mod) if (file.open(mod .. ".lua")) then dofile( mod .. ".lua") + elseif (file.open(mod .. "_diet.lua")) then + dofile(mod .. "_diet.lua") + elseif (file.open(mod .. "_diet.lc")) then + dofile(mod .. "_diet.lc") + elseif (file.open(mod)) then + dofile(mod) else - dofile(mod .. ".lc") + print("NA: " .. mod) end end - -tmr.alarm(1, 5000, 0, function() - tmr.stop(2) - if ( - (file.open("main.lua")) or - (file.open("timecore.lua")) or - (file.open("wordclock.lua")) or - (file.open("displayword.lua")) or - (file.open("webserver.lua")) - ) then - c = string.char(0,128,0) - w = string.char(0,0,0) - ws2812.write(w:rep(4) .. c .. w:rep(15) .. c .. w:rep(9) .. c .. w:rep(30) .. c .. w:rep(41) .. c ) - recompileAll() - print("Rebooting ...") - -- reboot repairs everything - node.restart() - elseif (file.open("main.lc")) then +initTimer = tmr.create() +initTimer:register(5000, tmr.ALARM_SINGLE, function (t) + bootledtimer:unregister() + initTimer:unregister() + initTimer=nil + bootledtimer=nil + local modlist = { "timecore" , "displayword", "ds18b20", "mqtt", "main" } + for i,mod in pairs(modlist) do + if (file.open(mod .. "_diet.lua")) then + file.remove(mod .. "_diet.lc") + print(tostring(i) .. ". Compile " .. mod) + ws2812.write(string.char(0,0,0):rep(11*i)..string.char(128,0,0):rep(11)) + node.compile(mod .. "_diet.lua") + print("cleanup..") + file.remove(mod .. "_diet.lua") + node.restart() + return + end + end + + if ( file.open("config.lua") ) then + --- Normal operation print("Starting main") - dofile("main.lc") + mydofile("main") + wifi.setmode(wifi.STATION) + dofile("config.lua") + normalOperation() else - print("No Main file found") + -- Logic for inital setup + mydofile("webserver") end end) -print("Init file end reached") +initTimer:start() diff --git a/main.lua b/main.lua index 2c0f00c..323479d 100644 --- a/main.lua +++ b/main.lua @@ -1,93 +1,75 @@ -- Main Module - -function startSetupMode() - tmr.stop(0) - tmr.stop(1) - -- start the webserver module - mydofile("webserver") - - wifi.setmode(wifi.SOFTAP) - cfg={} - cfg.ssid="wordclock" - cfg.pwd="wordclock" - wifi.ap.config(cfg) - - -- Write the buffer to the LEDs - local color=string.char(0,128,0) - local white=string.char(0,0,0) - local ledBuf= white:rep(6) .. color .. white:rep(7) .. color:rep(3) .. white:rep(44) .. color:rep(3) .. white:rep(50) - ws2812.write(ledBuf) - color=nil - white=nil - ledBuf=nil - - print("Waiting in access point >wordclock< for Clients") - print("Please visit 192.168.4.1") - startWebServer() - collectgarbage() -end - +mlt = tmr.create() -- Main loop timer +rowbgColor= {} +-- Buffer of the clock +rgbBuffer = ws2812.newBuffer(114, 3) +-- 110 Character plus one LED for each minute, +-- that cannot be displayed, as the clock as only a resolution of 5 minutes function syncTimeFromInternet() ---ptbtime1.ptb.de + if (syncRunning == nil) then + syncRunning=true sntp.sync(sntpserverhostname, function(sec,usec,server) - print('sync', sec, usec, server) - displayTime() + syncRunning=nil end, function() - print('failed!') + print('NTP failed!') + syncRunning=nil end ) + end end -briPercent = 50 + function displayTime() + collectgarbage() local sec, usec = rtctime.get() -- Handle lazy programmer: if (timezoneoffset == nil) then timezoneoffset=0 end - local time = getTime(sec, timezoneoffset) - local words = display_timestat(time.hour, time.minute) - if ((dim ~= nil) and (dim == "on")) then - words.briPercent=briPercent - else - words.briPercent=nil + mydofile("timecore") + if (tc == nil) then + return end - dp = dofile("displayword.lc") - if (dp ~= nil) then - ledBuf = dp.generateLEDs(words, color, color1, color2, color3, color4) - print("Local time : " .. time.year .. "-" .. time.month .. "-" .. time.day .. " " .. time.hour .. ":" .. time.minute .. ":" .. time.second .. " char: " .. tostring(displayword.data.drawnCharacters)) + local time = tc.getTime(sec, timezoneoffset) + tc = nil + collectgarbage() + mydofile("wordclock") + if (wc ~= nil) then + words = wc.timestat(time.hour, time.minute) + if ((dim ~= nil) and (dim == "on")) then + words.briPer=briPer + if (words.briPer ~= nil and words.briPer < 3) then + words.briPer=3 + end + else + words.briPer=nil + end end - dp = nil - if (ledBuf ~= nil) then - --if lines 4 to 6 are inverted due to hardware-fuckup, unfuck it here - if ((inv46 ~= nil) and (inv46 == "on")) then - tempstring = ledBuf:sub(1,99) -- first 33 leds - rowend = {44,55,66} - for _, startled in ipairs(rowend) do - for i = 0,10 do - tempstring = tempstring .. ledBuf:sub((startled-i)*3-2,(startled-i)*3) - end - end - tempstring = tempstring .. ledBuf:sub((67*3)-2,ledBuf:len()) - ws2812.write(tempstring) - tempstring=nil - else - ws2812.write(ledBuf) - ledBuf=nil - end - end - -- Used for debugging - if (clockdebug ~= nil) then - for key,value in pairs(words) do - if (value > 0) then - print(key,value) - end - end + wc = nil + collectgarbage() + print("wc: " .. tostring(node.heap())) + mydofile("displayword") + if (dw ~= nil) then + --if lines 4 to 6 are inverted due to hardware-fuckup, unfuck it here + local invertRows=false + if ((inv46 ~= nil) and (inv46 == "on")) then + invertRows=true + end + local c = dw.countChars(words) + dw.generateLEDs(rgbBuffer, words, colorBg, color, color1, color2, color3, color4, invertRows, c) end + if ( (tw ~= nil) and (tcol ~= nil) ) then + local c1 = dw.countChars(tw) + dw.generateLEDs(rgbBuffer, tw, nil, tcol, nil, nil, nil, nil, invertRows, c1) + end + dw = nil + collectgarbage() + -- cleanup - briPercent=words.briPercent + i=nil + briPer=words.briPer words=nil time=nil collectgarbage() @@ -99,106 +81,157 @@ function normalOperation() -- Color is defined as GREEN, RED, BLUE color=string.char(0,0,250) end - - connect_counter=0 + print("start: " , node.heap()) + ------------------------------------------------------------- + -- Define the main loop + local setupCounter=5 + local alive=0 + mlt:register(1000, tmr.ALARM_AUTO, function (lt) + if (setupCounter > 4) then + if (colorBg ~= nil) then + rgbBuffer:fill(string.byte(colorBg,1), string.byte(colorBg,2), string.byte(colorBg,3)) -- disable all LEDs + else + rgbBuffer:fill(0,0,0) -- disable all LEDs + end + syncTimeFromInternet() + setupCounter=setupCounter-1 + alive = 1 + rgbBuffer:set(19, color) -- N + rgbBuffer:set(31, color) -- T + if ((inv46 ~= nil) and (inv46 == "on")) then + rgbBuffer:set(45, color) -- P + else + rgbBuffer:set(55, color) -- P + end + elseif (setupCounter > 3) then + -- Here the WLAN is found, and something is done + mydofile("mqtt") + rgbBuffer:fill(0,0,0) -- disable all LEDs + if (startMqttClient ~= nil) then + if ((inv46 ~= nil) and (inv46 == "on")) then + rgbBuffer:set(34, color) -- M + else + rgbBuffer:set(44, color) -- M + end + rgbBuffer:set(82, color) -- T + startMqttClient() + else + print("NO Mqtt found") + mydofile("telnet") + end + setupCounter=setupCounter-1 + elseif (setupCounter > 2) then + if (startTelnetServer ~= nil) then + startTelnetServer() + else + displayTime() + end + setupCounter=setupCounter-1 + elseif ( (alive % 120) == 0) then + -- sync the time every 5 minutes + local heapusage = node.heap() + if (heapusage > 12000) then + syncTimeFromInternet() + end + heapusage=nil + alive = alive + 1 + else + displayTime() + alive = alive + 1 + end + if (rgbBuffer ~= nil) then + -- show Mqtt status + if (startMqttClient ~= nil) then + if (not connectedMqtt()) then + rgbBuffer:set(103, 0, 64,0) + -- check every thirty seconds, if reconnecting is necessary + if (((tmr.now() / 1000000) % 100) == 30) then + print("MQTT reconnecting... ") + reConnectMqtt() + end + end + end + ws2812.write(rgbBuffer) + else + -- set FG to fix value: RED + local color = string.char(255,0,0) + rgbBuffer:fill(0,0,0) -- disable all LEDs + for i=108,110, 1 do rgbBuffer:set(i, color) end + ws2812.write(rgbBuffer) + print("Fallback no time displayed") + end + collectgarbage() + -- Feed the system watchdog. + tmr.wdclr() + end) + + ------------------------------------------------------------- + -- Connect to Wifi + local connect_counter=0 -- Wait to be connect to the WiFi access point. - tmr.alarm(0, 500, 1, function() + local wifitimer = tmr.create() + wifitimer:register(500, tmr.ALARM_AUTO, function (timer) connect_counter=connect_counter+1 if wifi.sta.status() ~= 5 then print(connect_counter .. "/60 Connecting to AP...") + rgbBuffer:fill(0,0,0) -- clear all LEDs if (connect_counter % 5 ~= 4) then local wlanColor=string.char((connect_counter % 6)*20,(connect_counter % 5)*20,(connect_counter % 3)*20) - local space=string.char(0,0,0) - local buf=space:rep(6) if ((connect_counter % 5) >= 1) then - buf = buf .. wlanColor - else - buf = buf .. space + rgbBuffer:set(7, wlanColor) end - buf = buf .. space:rep(4) - buf= buf .. space:rep(3) if ((connect_counter % 5) >= 3) then - buf = buf .. wlanColor - else - buf = buf .. space + rgbBuffer:set(15, wlanColor) end if ((connect_counter % 5) >= 2) then - buf = buf .. wlanColor - else - buf = buf .. space + rgbBuffer:set(16, wlanColor) end if ((connect_counter % 5) >= 0) then - buf = buf .. wlanColor - else - buf = buf .. space + rgbBuffer:set(17, wlanColor) end - buf = buf .. space:rep(100) - ws2812.write(buf) - else - ws2812.write(string.char(0,0,0):rep(114)) end + ws2812.write(rgbBuffer) else - tmr.stop(0) - print('IP: ',wifi.sta.getip()) - -- Here the WLAN is found, and something is done - print("Solving dependencies") - local dependModules = { "timecore" , "wordclock" } - for _,mod in pairs(dependModules) do - print("Loading " .. mod) - mydofile(mod) - end - - tmr.alarm(2, 500, 0 ,function() - syncTimeFromInternet() - end) - tmr.alarm(3, 2000, 0 ,function() - print("Start webserver...") - mydofile("webserver") - startWebServer() - end) - displayTime() - -- Start the time Thread - tmr.alarm(1, 10000, 1 ,function() - displayTime() - collectgarbage() - end) - - end - -- when no wifi available, open an accesspoint and ask the user - if (connect_counter >= 60) then -- 300 is 30 sec in 100ms cycle - startSetupMode() + wifitimer:unregister() + wifitimer=nil + connect_counter=nil + print('IP: ',wifi.sta.getip(), " heap: ", node.heap()) + rgbBuffer:fill(0,0,0) -- clear all LEDs + rgbBuffer:set(13, color) -- I + if ((inv46 ~= nil) and (inv46 == "on")) then + rgbBuffer:set(45, color) -- P + else + rgbBuffer:set(55, color) -- P + end + ws2812.write(rgbBuffer) + mlt:start() end end) - + wifitimer:start() end -------------------main program ----------------------------- +briPer = 50 -- Default brightness is set to 50% ws2812.init() -- WS2812 LEDs initialized on GPIO2 -if ( file.open("config.lua") ) then - --- Normal operation - wifi.setmode(wifi.STATION) - dofile("config.lua") - normalOperation() -else - -- Logic for inital setup - startSetupMode() -end ----------- button --------- gpio.mode(3, gpio.INPUT) -btnCounter=0 --- Start the time Thread -tmr.alarm(4, 500, 1 ,function() +local btnCounter=0 +-- Start the time Thread handling the button +local btntimer = tmr.create() +btntimer:register(5000, tmr.ALARM_AUTO, function (t) if (gpio.read(3) == 0) then - tmr.stop(1) -- stop the LED thread + mlt:unregister() print("Button pressed " .. tostring(btnCounter)) btnCounter = btnCounter + 5 - local ledBuf= string.char(128,0,0):rep(btnCounter) .. string.char(0,0,0):rep(110 - btnCounter) - ws2812.write(ledBuf) + for i=1,btnCounter do rgbBuffer:set(i, 128, 0, 0) end + ws2812.write(rgbBuffer) if (btnCounter >= 110) then file.remove("config.lua") + file.remove("config.lc") node.restart() end end end) +btntimer:start() diff --git a/mqtt.lua b/mqtt.lua new file mode 100644 index 0000000..330488e --- /dev/null +++ b/mqtt.lua @@ -0,0 +1,267 @@ +-- Global Variables +-- Display Temp +tw=nil +tcol=nil +-- Module Variables +-- Mqtt variable +local mMqttClient=nil +local mMqttConnected = false + +function handleSingleCommand(client, topic, data) + if (data == "ON") then + briPer=100 + mMqttClient:publish(mqttPrefix .. "/clock", "ON", 0, 0) + elseif (data == "OFF") then + briPer=0 + mMqttClient:publish(mqttPrefix .. "/clock", "OFF", 0, 0) + elseif ((data:sub(1,1) == "#" and data:len() == 7) or (string.match(data, "%d+,%d+,%d+"))) then + local red=0 + local green=0 + local blue=0 + if (data:sub(1,1) == "#") then + red = tonumber(data:sub(2,3), 16) + green = tonumber(data:sub(4,5), 16) + blue = tonumber(data:sub(6,7), 16) + else + red, green, blue = string.match(data, "(%d+),(%d+),(%d+)") + end + colorBg=string.char(green * briPer / 100, red * briPer / 100, blue * briPer / 100) + print("Updated BG: " .. tostring(red) .. "," .. tostring(green) .. "," .. tostring(blue) ) + mMqttClient:publish(mqttPrefix .. "/background", tostring(red) .. "," .. tostring(green) .. "," .. tostring(blue), 0, 0) + else + if (tonumber(data) >= 0 and tonumber(data) <= 100) then + briPer=tonumber(data) + mMqttClient:publish(mqttPrefix .. "/clock", tostring(data), 0, 0) + else + print "Unknown MQTT command" + end + end + +end + +-- Parse MQTT data and extract color +-- @param data MQTT information +-- @param row string of the row e.g. "row1" used to publish current state +-- @param per percent the color should be dimmed +function parseBgColor(data, row, per) + local red=nil + local green=nil + local blue=nil + if (data:sub(1,1) == "#") then + red = tonumber(data:sub(2,3), 16) + green = tonumber(data:sub(4,5), 16) + blue = tonumber(data:sub(6,7), 16) + else + red, green, blue = string.match(data, "(%d+),(%d+),(%d+)") + end + if ((red ~= nil) and (green ~= nil) and (blue ~= nil) ) then + mMqttClient:publish(mqttPrefix .. "/"..row, tostring(red) .. "," .. tostring(green) .. "," .. tostring(blue), 0, 0) + if (per ~= nil) then + return string.char(green * per / 100, red * per / 100, blue * per / 100) + else + return string.char(green , red , blue ) + end + else + return nil + end +end + +function readTemp(ds18b20) + if (ds18b20 ~= nil) then + local addrs=ds18b20.addrs() + -- Total DS18B20 numbers + local sensors=table.getn(addrs) + local temp1=0 + if (sensors >= 1) then + temp1=ds18b20.read(addrs[0]) + else + print("No sensor DS18B20 found") + end + return temp1 + else + print("No DS18B20 lib") + return nil + end +end + +-- Connect or reconnect to mqtt server +function reConnectMqtt() + if (not mMqttConnected) then + mMqttClient:connect(mqttServer, 1883, false, function(c) + print("MQTT is connected") + mMqttConnected = true + -- subscribe topic with qos = 0 + mMqttClient:subscribe(mqttPrefix .. "/cmd/#", 0) + local tmr1 = tmr.create() + tmr1:register(1000, tmr.ALARM_SINGLE, function (dummyTemp) + -- publish a message with data = hello, QoS = 0, retain = 0 + mMqttClient:publish(mqttPrefix .. "/ip", tostring(wifi.sta.getip()), 0, 0) + local red = string.byte(colorBg,2) + local green = string.byte(colorBg,1) + local blue = string.byte(colorBg,3) + mMqttClient:publish(mqttPrefix .. "/background", tostring(red) .. "," .. tostring(green) .. "," .. tostring(blue), 0, 0) + tmr1:unregister() + tmr1=nil + end) + tmr1:start() + end, + function(client, reason) + print("failed reason: " .. reason) + mMqttConnected = false + end) + end +end + +-- MQTT extension +function registerMqtt() + mMqttClient = mqtt.Client("wordclock", 120) + -- on publish message receive event + mMqttClient:on("message", function(client, topic, data) + collectgarbage() + if data ~= nil then + local heapusage = node.heap() + if (heapusage < 11000) then + print("Skip " .. topic .. ":" .. data .. "; heap:" .. heapusage) + heapusage=nil + return + end + heapusage=nil + print("MQTT " .. topic .. ":" .. data) + if (topic == (mqttPrefix .. "/cmd/single")) then + handleSingleCommand(client, topic, data) + elseif (topic == (mqttPrefix .. "/cmd/temp")) then + if (( data == "" ) or (data == nil)) then + tw=nil + tcol=nil + print("MQTT | wordclock failed") + else + -- generate the temperatur to display, once as it will not change + local dispTemp = tonumber(data) + collectgarbage() + mydofile("wordclock") + if (wc ~= nil) then + tw, tcol = wc.temp(dw, rgbBuffer, invertRows) + wc = nil + print("MQTT | generated words for: " + tostring(dispTemp)) + else + print("MQTT | wordclock failed") + end + end + else + -- Handle here the /cmd/# sublevel + if (string.match(topic, "telnet$")) then + client:publish(mqttPrefix .. "/telnet", tostring(wifi.sta.getip()), 0, 0) + ws2812.write(string.char(0,0,0):rep(114)) + print("Stop Mqtt and Temp") + m=nil + t=nil + mMqttConnected = false + if (mlt ~= nil) then + mlt:unregister() + else + print("main loop unstoppable") + end + collectgarbage() + mydofile("telnet") + if (startTelnetServer ~= nil) then + startTelnetServer() + else + print("Cannost start Telnet Server!") + end + elseif (string.match(topic, "color$")) then + color = parseBgColor(data, "color") + print("Updated color" ) + elseif (string.match(topic, "color1$")) then + color1 = parseBgColor(data, "color1") + print("Updated color1" ) + elseif (string.match(topic, "color2$")) then + color2 = parseBgColor(data, "color2") + print("Updated color2" ) + elseif (string.match(topic, "color3$")) then + color3 = parseBgColor(data, "color3") + print("Updated color3" ) + elseif (string.match(topic, "color4$")) then + color4 = parseBgColor(data, "color4") + print("Updated color4" ) + else + for i=1,10,1 do + if (string.match(topic, "row".. tostring(i) .."$")) then + rowbgColor[i] = parseBgColor(data, "row" .. tostring(i), briPer) + return + end + end + end + end + end + end) + -- do something + mMqttClient:on("offline", function(client) + print("MQTT Disconnected") + mMqttConnected = false + collectgarbage() + end) + reConnectMqtt() +end + +function connectedMqtt() + return mMqttConnected +end + +function startMqttClient() + if (mqttServer ~= nil and mqttPrefix ~= nil) then + registerMqtt() + print "Started MQTT client" + local lSetupTimer = tmr.create() + lSetupTimer:register(123, tmr.ALARM_SINGLE, function (kTemp) + if (file.open("ds18b20_diet.lc")) then + t=true + print "Setup temperatur" + end + end) + lSetupTimer:start() + local loldBrightness=0 + local lMqttTimer = tmr.create() + local tempCounter=0 + -- Check every 12 seconds + lMqttTimer:register(6321, tmr.ALARM_AUTO, function (kTemp) + if (mMqttConnected) then + -- Cleanup the initialization stuff + lSetupTimer=nil + -- Do the action + local heapusage = node.heap() + local temperatur = nil + if (loldBrightness ~= briPer) then + mMqttClient:publish(mqttPrefix .. "/brightness", tostring(briPer), 0, 0) + loldBrightness = briPer + else + if ((t ~= nil) and (heapusage > 11900) and (tempCounter > 10)) then + local ds18b20=require("ds18b20_diet") + ds18b20.setup(2) -- GPIO4 + readTemp(ds18b20) -- read once, to setup chip + temperatur=readTemp(ds18b20) + if (temperatur == 8500) then + temperatur=nil + end + ds18b20=nil + ds18b20_diet=nil + package.loaded["ds18b20_diet"]=nil + tempCounter = 0 + else + tempCounter = tempCounter + 1 + end + collectgarbage() + if (temperatur ~= nil) then + mMqttClient:publish(mqttPrefix .. "/temp", tostring(temperatur/100)..".".. tostring(temperatur%100), 0, 0) + else + mMqttClient:publish(mqttPrefix .. "/heap", tostring(heapusage), 0, 0) + collectgarbage() + end + end + heapusage=nil + temperatur=nil + end + end) + lMqttTimer:start() + end +end + diff --git a/os/0x00000.bin b/os/0x00000.bin index 47bcfd4..8629185 100644 Binary files a/os/0x00000.bin and b/os/0x00000.bin differ diff --git a/os/0x10000.bin b/os/0x10000.bin index ac44e0e..62a7c13 100644 Binary files a/os/0x10000.bin and b/os/0x10000.bin differ diff --git a/os/Readme.md b/os/Readme.md index c8a60f3..2f9f276 100644 --- a/os/Readme.md +++ b/os/Readme.md @@ -1,22 +1,26 @@ -# Firmware was compiled with the following modules: +# Nodemcu-firmware - * LUA_USE_MODULES_ADC - * LUA_USE_MODULES_BIT - * LUA_USE_MODULES_DHT - * LUA_USE_MODULES_FILE - * LUA_USE_MODULES_GPIO - * LUA_USE_MODULES_I2C - * LUA_USE_MODULES_MQTT - * LUA_USE_MODULES_NET - * LUA_USE_MODULES_NODE - * LUA_USE_MODULES_OW - * LUA_USE_MODULES_RTCFIFO - * LUA_USE_MODULES_RTCMEM - * LUA_USE_MODULES_RTCTIME - * LUA_USE_MODULES_SNTP - * LUA_USE_MODULES_SPI - * LUA_USE_MODULES_TMR - * LUA_USE_MODULES_UART - * LUA_USE_MODULES_WIFI - * LUA_USE_MODULES_WS2812 +## Release +* 3.0-release_20201107 + +## Modules +Firmware was compiled with the following modules: + * LUA_USE_MODULES_ADC + * LUA_USE_MODULES_FILE + * LUA_USE_MODULES_GPIO + * LUA_USE_MODULES_MQTT + * LUA_USE_MODULES_NET + * LUA_USE_MODULES_NODE + * LUA_USE_MODULES_OW + * LUA_USE_MODULES_PIPE + * LUA_USE_MODULES_RTCTIME + * LUA_USE_MODULES_SNTP + * LUA_USE_MODULES_TMR + * LUA_USE_MODULES_UART + * LUA_USE_MODULES_WIFI + * LUA_USE_MODULES_WS2812 + +## Numbers +Floating point calcuation was disabled: + * LUA_NUMBER_INTEGRAL diff --git a/os/esptool.py b/os/esptool.py index 38ffb72..6a9a97f 100755 --- a/os/esptool.py +++ b/os/esptool.py @@ -1,10 +1,8 @@ #!/usr/bin/env python -# NB: Before sending a PR to change the above line to '#!/usr/bin/env python2', please read https://github.com/themadinventor/esptool/issues/21 # -# ESP8266 ROM Bootloader Utility -# https://github.com/themadinventor/esptool -# -# Copyright (C) 2014-2016 Fredrik Ahlberg, Angus Gratton, other contributors as noted. +# ESP8266 & ESP32 family ROM Bootloader Utility +# Copyright (C) 2014-2016 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) PTE LTD, other contributors as noted. +# https://github.com/espressif/esptool # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software @@ -18,24 +16,179 @@ # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin # Street, Fifth Floor, Boston, MA 02110-1301 USA. +from __future__ import division, print_function + import argparse +import base64 +import binascii +import copy import hashlib import inspect -import json +import io +import itertools import os -import serial +import shlex +import string import struct -import subprocess import sys -import tempfile import time +import zlib + +try: + import serial +except ImportError: + print("Pyserial is not installed for %s. Check the README for installation instructions." % (sys.executable)) + raise + +# check 'serial' is 'pyserial' and not 'serial' https://github.com/espressif/esptool/issues/269 +try: + if "serialization" in serial.__doc__ and "deserialization" in serial.__doc__: + raise ImportError(""" +esptool.py depends on pyserial, but there is a conflict with a currently installed package named 'serial'. + +You may be able to work around this by 'pip uninstall serial; pip install pyserial' \ +but this may break other installed Python software that depends on 'serial'. + +There is no good fix for this right now, apart from configuring virtualenvs. \ +See https://github.com/espressif/esptool/issues/269#issuecomment-385298196 for discussion of the underlying issue(s).""") +except TypeError: + pass # __doc__ returns None for pyserial + +try: + import serial.tools.list_ports as list_ports +except ImportError: + print("The installed version (%s) of pyserial appears to be too old for esptool.py (Python interpreter %s). " + "Check the README for installation instructions." % (sys.VERSION, sys.executable)) + raise +except Exception: + if sys.platform == "darwin": + # swallow the exception, this is a known issue in pyserial+macOS Big Sur preview ref https://github.com/espressif/esptool/issues/540 + list_ports = None + else: + raise -__version__ = "1.2-dev" +__version__ = "3.1-dev" + +MAX_UINT32 = 0xffffffff +MAX_UINT24 = 0xffffff + +DEFAULT_TIMEOUT = 3 # timeout for most flash operations +START_FLASH_TIMEOUT = 20 # timeout for starting flash (may perform erase) +CHIP_ERASE_TIMEOUT = 120 # timeout for full chip erase +MAX_TIMEOUT = CHIP_ERASE_TIMEOUT * 2 # longest any command can run +SYNC_TIMEOUT = 0.1 # timeout for syncing with bootloader +MD5_TIMEOUT_PER_MB = 8 # timeout (per megabyte) for calculating md5sum +ERASE_REGION_TIMEOUT_PER_MB = 30 # timeout (per megabyte) for erasing a region +COMP_BLOCK_WRITE_TIMEOUT_PER_MB = 3 # timeout (per megabyte) for writing compressed data +MEM_END_ROM_TIMEOUT = 0.05 # special short timeout for ESP_MEM_END, as it may never respond +DEFAULT_SERIAL_WRITE_TIMEOUT = 10 # timeout for serial port write +DEFAULT_CONNECT_ATTEMPTS = 7 # default number of times to try connection -class ESPROM(object): - # These are the currently known commands supported by the ROM +def timeout_per_mb(seconds_per_mb, size_bytes): + """ Scales timeouts which are size-specific """ + result = seconds_per_mb * (size_bytes / 1e6) + if result < DEFAULT_TIMEOUT: + return DEFAULT_TIMEOUT + return result + + +DETECTED_FLASH_SIZES = {0x12: '256KB', 0x13: '512KB', 0x14: '1MB', + 0x15: '2MB', 0x16: '4MB', 0x17: '8MB', 0x18: '16MB'} + + +def check_supported_function(func, check_func): + """ + Decorator implementation that wraps a check around an ESPLoader + bootloader function to check if it's supported. + + This is used to capture the multidimensional differences in + functionality between the ESP8266 & ESP32/32S2/32S3/32C3 ROM loaders, and the + software stub that runs on both. Not possible to do this cleanly + via inheritance alone. + """ + def inner(*args, **kwargs): + obj = args[0] + if check_func(obj): + return func(*args, **kwargs) + else: + raise NotImplementedInROMError(obj, func) + return inner + + +def stub_function_only(func): + """ Attribute for a function only supported in the software stub loader """ + return check_supported_function(func, lambda o: o.IS_STUB) + + +def stub_and_esp32_function_only(func): + """ Attribute for a function only supported by software stubs or ESP32/32S2/32S3/32C3 ROM """ + return check_supported_function(func, lambda o: o.IS_STUB or isinstance(o, ESP32ROM)) + + +PYTHON2 = sys.version_info[0] < 3 # True if on pre-Python 3 + +# Function to return nth byte of a bitstring +# Different behaviour on Python 2 vs 3 +if PYTHON2: + def byte(bitstr, index): + return ord(bitstr[index]) +else: + def byte(bitstr, index): + return bitstr[index] + +# Provide a 'basestring' class on Python 3 +try: + basestring +except NameError: + basestring = str + + +def print_overwrite(message, last_line=False): + """ Print a message, overwriting the currently printed line. + + If last_line is False, don't append a newline at the end (expecting another subsequent call will overwrite this one.) + + After a sequence of calls with last_line=False, call once with last_line=True. + + If output is not a TTY (for example redirected a pipe), no overwriting happens and this function is the same as print(). + """ + if sys.stdout.isatty(): + print("\r%s" % message, end='\n' if last_line else '') + else: + print(message) + + +def _mask_to_shift(mask): + """ Return the index of the least significant bit in the mask """ + shift = 0 + while mask & 0x1 == 0: + shift += 1 + mask >>= 1 + return shift + + +def esp8266_function_only(func): + """ Attribute for a function only supported on ESP8266 """ + return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266") + + +class ESPLoader(object): + """ Base class providing access to ESP ROM & software stub bootloaders. + Subclasses provide ESP8266 & ESP32 specific functionality. + + Don't instantiate this base class directly, either instantiate a subclass or + call ESPLoader.detect_chip() which will interrogate the chip and return the + appropriate subclass instance. + + """ + CHIP_NAME = "Espressif device" + IS_STUB = False + + DEFAULT_PORT = "/dev/ttyUSB0" + + # Commands supported by ESP8266 ROM bootloader ESP_FLASH_BEGIN = 0x02 ESP_FLASH_DATA = 0x03 ESP_FLASH_END = 0x04 @@ -46,9 +199,35 @@ class ESPROM(object): ESP_WRITE_REG = 0x09 ESP_READ_REG = 0x0a + # Some comands supported by ESP32 ROM bootloader (or -8266 w/ stub) + ESP_SPI_SET_PARAMS = 0x0B + ESP_SPI_ATTACH = 0x0D + ESP_READ_FLASH_SLOW = 0x0e # ROM only, much slower than the stub flash read + ESP_CHANGE_BAUDRATE = 0x0F + ESP_FLASH_DEFL_BEGIN = 0x10 + ESP_FLASH_DEFL_DATA = 0x11 + ESP_FLASH_DEFL_END = 0x12 + ESP_SPI_FLASH_MD5 = 0x13 + + # Commands supported by ESP32-S2/S3/C3 ROM bootloader only + ESP_GET_SECURITY_INFO = 0x14 + + # Some commands supported by stub only + ESP_ERASE_FLASH = 0xD0 + ESP_ERASE_REGION = 0xD1 + ESP_READ_FLASH = 0xD2 + ESP_RUN_USER_CODE = 0xD3 + + # Flash encryption encrypted data command + ESP_FLASH_ENCRYPT_DATA = 0xD4 + + # Response code(s) sent by ROM + ROM_INVALID_RECV_MSG = 0x05 # response if an invalid message is received + # Maximum block sized for RAM and Flash writes, respectively. ESP_RAM_BLOCK = 0x1800 - ESP_FLASH_BLOCK = 0x400 + + FLASH_WRITE_SIZE = 0x400 # Default baudrate. The ROM auto-bauds, so we can use more or less whatever we want. ESP_ROM_BAUD = 115200 @@ -59,174 +238,444 @@ class ESPROM(object): # Initial state for the checksum routine ESP_CHECKSUM_MAGIC = 0xef - # OTP ROM addresses - ESP_OTP_MAC0 = 0x3ff00050 - ESP_OTP_MAC1 = 0x3ff00054 - ESP_OTP_MAC3 = 0x3ff0005c - # Flash sector size, minimum unit of erase. - ESP_FLASH_SECTOR = 0x1000 + FLASH_SECTOR_SIZE = 0x1000 - def __init__(self, port=0, baud=ESP_ROM_BAUD): - self._port = serial.Serial(port) - self._slip_reader = slip_reader(port) + UART_DATE_REG_ADDR = 0x60000078 + + CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000 # This ROM address has a different value on each chip model + + UART_CLKDIV_MASK = 0xFFFFF + + # Memory addresses + IROM_MAP_START = 0x40200000 + IROM_MAP_END = 0x40300000 + + # The number of bytes in the UART response that signify command status + STATUS_BYTES_LENGTH = 2 + + def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): + """Base constructor for ESPLoader bootloader interaction + + Don't call this constructor, either instantiate ESP8266ROM + or ESP32ROM, or use ESPLoader.detect_chip(). + + This base class has all of the instance methods for bootloader + functionality supported across various chips & stub + loaders. Subclasses replace the functions they don't support + with ones which throw NotImplementedInROMError(). + + """ + self.secure_download_mode = False # flag is set to True if esptool detects the ROM is in Secure Download Mode + + if isinstance(port, basestring): + self._port = serial.serial_for_url(port) + else: + self._port = port + self._slip_reader = slip_reader(self._port, self.trace) # setting baud rate in a separate step is a workaround for # CH341 driver on some Linux versions (this opens at 9600 then # sets), shouldn't matter for other platforms/drivers. See - # https://github.com/themadinventor/esptool/issues/44#issuecomment-107094446 - self._port.baudrate = baud + # https://github.com/espressif/esptool/issues/44#issuecomment-107094446 + self._set_port_baudrate(baud) + self._trace_enabled = trace_enabled + # set write timeout, to prevent esptool blocked at write forever. + try: + self._port.write_timeout = DEFAULT_SERIAL_WRITE_TIMEOUT + except NotImplementedError: + # no write timeout for RFC2217 ports + # need to set the property back to None or it will continue to fail + self._port.write_timeout = None + + def _set_port_baudrate(self, baud): + try: + self._port.baudrate = baud + except IOError: + raise FatalError("Failed to set baud rate %d. The driver may not support this rate." % baud) + + @staticmethod + def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset', trace_enabled=False, + connect_attempts=DEFAULT_CONNECT_ATTEMPTS): + """ Use serial access to detect the chip type. + + We use the UART's datecode register for this, it's mapped at + the same address on ESP8266 & ESP32 so we can use one + memory read and compare to the datecode register for each chip + type. + + This routine automatically performs ESPLoader.connect() (passing + connect_mode parameter) as part of querying the chip. + """ + detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) + detect_port.connect(connect_mode, connect_attempts, detecting=True) + try: + print('Detecting chip type...', end='') + sys.stdout.flush() + chip_magic_value = detect_port.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR) + + for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32C3ROM]: + if chip_magic_value == cls.CHIP_DETECT_MAGIC_VALUE: + # don't connect a second time + inst = cls(detect_port._port, baud, trace_enabled=trace_enabled) + inst._post_connect() + print(' %s' % inst.CHIP_NAME, end='') + return inst + except UnsupportedCommandError: + raise FatalError("Unsupported Command Error received. Probably this means Secure Download Mode is enabled, " + "autodetection will not work. Need to manually specify the chip.") + finally: + print('') # end line + raise FatalError("Unexpected CHIP magic value 0x%08x. Failed to autodetect chip type." % (chip_magic_value)) """ Read a SLIP packet from the serial port """ def read(self): - return self._slip_reader.next() + return next(self._slip_reader) """ Write bytes to the serial port while performing SLIP escaping """ def write(self, packet): - buf = '\xc0' \ - + (packet.replace('\xdb','\xdb\xdd').replace('\xc0','\xdb\xdc')) \ - + '\xc0' + buf = b'\xc0' \ + + (packet.replace(b'\xdb', b'\xdb\xdd').replace(b'\xc0', b'\xdb\xdc')) \ + + b'\xc0' + self.trace("Write %d bytes: %s", len(buf), HexFormatter(buf)) self._port.write(buf) + def trace(self, message, *format_args): + if self._trace_enabled: + now = time.time() + try: + + delta = now - self._last_trace + except AttributeError: + delta = 0.0 + self._last_trace = now + prefix = "TRACE +%.3f " % delta + print(prefix + (message % format_args)) + """ Calculate checksum of a blob, as it is defined by the ROM """ @staticmethod def checksum(data, state=ESP_CHECKSUM_MAGIC): for b in data: - state ^= ord(b) + if type(b) is int: # python 2/3 compat + state ^= b + else: + state ^= ord(b) + return state """ Send a request and read the response """ - def command(self, op=None, data=None, chk=0): - if op is not None: - pkt = struct.pack(' self.STATUS_BYTES_LENGTH: + return data[:-self.STATUS_BYTES_LENGTH] + else: # otherwise, just return the 'val' field which comes from the reply header (this is used by read_reg) + return val + + def flush_input(self): + self._port.flushInput() + self._slip_reader = slip_reader(self._port, self.trace) + def sync(self): - self.command(ESPROM.ESP_SYNC, '\x07\x07\x12\x20' + 32 * '\x55') - for i in xrange(7): + self.command(self.ESP_SYNC, b'\x07\x07\x12\x20' + 32 * b'\x55', + timeout=SYNC_TIMEOUT) + for i in range(7): self.command() - """ Try connecting repeatedly until successful, or giving up """ - def connect(self): - print 'Connecting...' + def _setDTR(self, state): + self._port.setDTR(state) - for _ in xrange(4): - # issue reset-to-bootloader: - # RTS = either CH_PD or nRESET (both active low = chip in reset) - # DTR = GPIO0 (active low = boot to flasher) - self._port.setDTR(False) - self._port.setRTS(True) + def _setRTS(self, state): + self._port.setRTS(state) + # Work-around for adapters on Windows using the usbser.sys driver: + # generate a dummy change to DTR so that the set-control-line-state + # request is sent with the updated RTS state and the same DTR state + self._port.setDTR(self._port.dtr) + + def _connect_attempt(self, mode='default_reset', esp32r0_delay=False): + """ A single connection attempt, with esp32r0 workaround options """ + # esp32r0_delay is a workaround for bugs with the most common auto reset + # circuit and Windows, if the EN pin on the dev board does not have + # enough capacitance. + # + # Newer dev boards shouldn't have this problem (higher value capacitor + # on the EN pin), and ESP32 revision 1 can't use this workaround as it + # relies on a silicon bug. + # + # Details: https://github.com/espressif/esptool/issues/136 + last_error = None + + # If we're doing no_sync, we're likely communicating as a pass through + # with an intermediate device to the ESP32 + if mode == "no_reset_no_sync": + return last_error + + # issue reset-to-bootloader: + # RTS = either CH_PD/EN or nRESET (both active low = chip in reset + # DTR = GPIO0 (active low = boot to flasher) + # + # DTR & RTS are active low signals, + # ie True = pin @ 0V, False = pin @ VCC. + if mode != 'no_reset': + self._setDTR(False) # IO0=HIGH + self._setRTS(True) # EN=LOW, chip in reset + time.sleep(0.1) + if esp32r0_delay: + # Some chips are more likely to trigger the esp32r0 + # watchdog reset silicon bug if they're held with EN=LOW + # for a longer period + time.sleep(1.2) + self._setDTR(True) # IO0=LOW + self._setRTS(False) # EN=HIGH, chip out of reset + if esp32r0_delay: + # Sleep longer after reset. + # This workaround only works on revision 0 ESP32 chips, + # it exploits a silicon bug spurious watchdog reset. + time.sleep(0.4) # allow watchdog reset to occur time.sleep(0.05) - self._port.setDTR(True) - self._port.setRTS(False) - time.sleep(0.05) - self._port.setDTR(False) + self._setDTR(False) # IO0=HIGH, done - # worst-case latency timer should be 255ms (probably <20ms) - self._port.timeout = 0.3 - for _ in xrange(4): - try: - self._port.flushInput() - self._slip_reader = slip_reader(self._port) - self._port.flushOutput() - self.sync() - self._port.timeout = 5 - return - except: - time.sleep(0.05) - raise FatalError('Failed to connect to ESP8266') + for _ in range(5): + try: + self.flush_input() + self._port.flushOutput() + self.sync() + return None + except FatalError as e: + if esp32r0_delay: + print('_', end='') + else: + print('.', end='') + sys.stdout.flush() + time.sleep(0.05) + last_error = e + return last_error + + def connect(self, mode='default_reset', attempts=DEFAULT_CONNECT_ATTEMPTS, detecting=False): + """ Try connecting repeatedly until successful, or giving up """ + print('Connecting...', end='') + sys.stdout.flush() + last_error = None + + try: + for _ in range(attempts) if attempts > 0 else itertools.count(): + last_error = self._connect_attempt(mode=mode, esp32r0_delay=False) + if last_error is None: + break + last_error = self._connect_attempt(mode=mode, esp32r0_delay=True) + if last_error is None: + break + finally: + print('') # end 'Connecting...' line + + if last_error is not None: + raise FatalError('Failed to connect to %s: %s' % (self.CHIP_NAME, last_error)) + + if not detecting: + try: + # check the date code registers match what we expect to see + chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR) + if chip_magic_value != self.CHIP_DETECT_MAGIC_VALUE: + actually = None + for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32C3ROM]: + if chip_magic_value == cls.CHIP_DETECT_MAGIC_VALUE: + actually = cls + break + if actually is None: + print(("WARNING: This chip doesn't appear to be a %s (chip magic value 0x%08x). " + "Probably it is unsupported by this version of esptool.") % (self.CHIP_NAME, chip_magic_value)) + else: + raise FatalError("This chip is %s not %s. Wrong --chip argument?" % (actually.CHIP_NAME, self.CHIP_NAME)) + except UnsupportedCommandError: + self.secure_download_mode = True + self._post_connect() + + def _post_connect(self): + """ + Additional initialization hook, may be overridden by the chip-specific class. + Gets called after connect, and after auto-detection. + """ + pass - """ Read memory address in target """ def read_reg(self, addr): - res = self.command(ESPROM.ESP_READ_REG, struct.pack(' 0: + # add a dummy write to a date register as an excuse to have a delay + command += struct.pack(' start: + raise FatalError(("Software loader is resident at 0x%08x-0x%08x. " + "Can't load binary at overlapping address range 0x%08x-0x%08x. " + "Either change binary loading address, or use the --no-stub " + "option to disable the software loader.") % (start, end, load_start, load_end)) + + return self.check_command("enter RAM download mode", self.ESP_MEM_BEGIN, + struct.pack(' length: + raise FatalError('Read more than expected') + + digest_frame = self.read() + if len(digest_frame) != 16: + raise FatalError('Expected digest, got: %s' % hexify(digest_frame)) + expected_digest = hexify(digest_frame).upper() + digest = hashlib.md5(data).hexdigest().upper() + if digest != expected_digest: + raise FatalError('Digest mismatch: expected %s, got %s' % (expected_digest, digest)) + return data + + def flash_spi_attach(self, hspi_arg): + """Send SPI attach command to enable the SPI flash pins + + ESP8266 ROM does this when you send flash_begin, ESP32 ROM + has it as a SPI command. + """ + # last 3 bytes in ESP_SPI_ATTACH argument are reserved values + arg = struct.pack(' 0: + self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1) + if miso_bits > 0: + self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1) + else: + + def set_data_lengths(mosi_bits, miso_bits): + SPI_DATA_LEN_REG = SPI_USR1_REG + SPI_MOSI_BITLEN_S = 17 + SPI_MISO_BITLEN_S = 8 + mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1) + miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1) + self.write_reg(SPI_DATA_LEN_REG, + (miso_mask << SPI_MISO_BITLEN_S) | ( + mosi_mask << SPI_MOSI_BITLEN_S)) + + # SPI peripheral "command" bitmasks for SPI_CMD_REG + SPI_CMD_USR = (1 << 18) + + # shift values + SPI_USR2_COMMAND_LEN_SHIFT = 28 + + if read_bits > 32: + raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported") + if len(data) > 64: + raise FatalError("Writing more than 64 bytes of data with one SPI command is unsupported") + + data_bits = len(data) * 8 + old_spi_usr = self.read_reg(SPI_USR_REG) + old_spi_usr2 = self.read_reg(SPI_USR2_REG) + flags = SPI_USR_COMMAND + if read_bits > 0: + flags |= SPI_USR_MISO + if data_bits > 0: + flags |= SPI_USR_MOSI + set_data_lengths(data_bits, read_bits) + self.write_reg(SPI_USR_REG, flags) + self.write_reg(SPI_USR2_REG, + (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command) + if data_bits == 0: + self.write_reg(SPI_W0_REG, 0) # clear data register before we read it + else: + data = pad_to(data, 4, b'\00') # pad to 32-bit multiple + words = struct.unpack("I" * (len(data) // 4), data) + next_reg = SPI_W0_REG + for word in words: + self.write_reg(next_reg, word) + next_reg += 4 + self.write_reg(SPI_CMD_REG, SPI_CMD_USR) + + def wait_done(): + for _ in range(10): + if (self.read_reg(SPI_CMD_REG) & SPI_CMD_USR) == 0: + return + raise FatalError("SPI command did not complete in time") + wait_done() + + status = self.read_reg(SPI_W0_REG) + # restore some SPI controller registers + self.write_reg(SPI_USR_REG, old_spi_usr) + self.write_reg(SPI_USR2_REG, old_spi_usr2) + return status + + def read_status(self, num_bytes=2): + """Read up to 24 bits (num_bytes) of SPI flash status register contents + via RDSR, RDSR2, RDSR3 commands + + Not all SPI flash supports all three commands. The upper 1 or 2 + bytes may be 0xFF. + """ + SPIFLASH_RDSR = 0x05 + SPIFLASH_RDSR2 = 0x35 + SPIFLASH_RDSR3 = 0x15 + + status = 0 + shift = 0 + for cmd in [SPIFLASH_RDSR, SPIFLASH_RDSR2, SPIFLASH_RDSR3][0:num_bytes]: + status += self.run_spiflash_command(cmd, read_bits=8) << shift + shift += 8 + return status + + def write_status(self, new_status, num_bytes=2, set_non_volatile=False): + """Write up to 24 bits (num_bytes) of new status register + + num_bytes can be 1, 2 or 3. + + Not all flash supports the additional commands to write the + second and third byte of the status register. When writing 2 + bytes, esptool also sends a 16-byte WRSR command (as some + flash types use this instead of WRSR2.) + + If the set_non_volatile flag is set, non-volatile bits will + be set as well as volatile ones (WREN used instead of WEVSR). + + """ + SPIFLASH_WRSR = 0x01 + SPIFLASH_WRSR2 = 0x31 + SPIFLASH_WRSR3 = 0x11 + SPIFLASH_WEVSR = 0x50 + SPIFLASH_WREN = 0x06 + SPIFLASH_WRDI = 0x04 + + enable_cmd = SPIFLASH_WREN if set_non_volatile else SPIFLASH_WEVSR + + # try using a 16-bit WRSR (not supported by all chips) + # this may be redundant, but shouldn't hurt + if num_bytes == 2: + self.run_spiflash_command(enable_cmd) + self.run_spiflash_command(SPIFLASH_WRSR, struct.pack(">= 8 + + self.run_spiflash_command(SPIFLASH_WRDI) + + def get_crystal_freq(self): + # Figure out the crystal frequency from the UART clock divider + # Returns a normalized value in integer MHz (40 or 26 are the only supported values) + # + # The logic here is: + # - We know that our baud rate and the ESP UART baud rate are roughly the same, or we couldn't communicate + # - We can read the UART clock divider register to know how the ESP derives this from the APB bus frequency + # - Multiplying these two together gives us the bus frequency which is either the crystal frequency (ESP32) + # or double the crystal frequency (ESP8266). See the self.XTAL_CLK_DIVIDER parameter for this factor. + uart_div = self.read_reg(self.UART_CLKDIV_REG) & self.UART_CLKDIV_MASK + est_xtal = (self._port.baudrate * uart_div) / 1e6 / self.XTAL_CLK_DIVIDER + norm_xtal = 40 if est_xtal > 33 else 26 + if abs(norm_xtal - est_xtal) > 1: + print("WARNING: Detected crystal freq %.2fMHz is quite different to normalized freq %dMHz. Unsupported crystal in use?" % (est_xtal, norm_xtal)) + return norm_xtal + + def hard_reset(self): + self._setRTS(True) # EN->LOW + time.sleep(0.1) + self._setRTS(False) + + def soft_reset(self, stay_in_bootloader): + if not self.IS_STUB: + if stay_in_bootloader: + return # ROM bootloader is already in bootloader! + else: + # 'run user code' is as close to a soft reset as we can do + self.flash_begin(0, 0) + self.flash_finish(False) + else: + if stay_in_bootloader: + # soft resetting from the stub loader + # will re-load the ROM bootloader + self.flash_begin(0, 0) + self.flash_finish(True) + elif self.CHIP_NAME != "ESP8266": + raise FatalError("Soft resetting is currently only supported on ESP8266") + else: + # running user code from stub loader requires some hacks + # in the stub loader + self.command(self.ESP_RUN_USER_CODE, wait_response=False) + + +class ESP8266ROM(ESPLoader): + """ Access class for ESP8266 ROM bootloader + """ + CHIP_NAME = "ESP8266" + IS_STUB = False + + CHIP_DETECT_MAGIC_VALUE = 0xfff0c101 + + # OTP ROM addresses + ESP_OTP_MAC0 = 0x3ff00050 + ESP_OTP_MAC1 = 0x3ff00054 + ESP_OTP_MAC3 = 0x3ff0005c + + SPI_REG_BASE = 0x60000200 + SPI_USR_OFFS = 0x1c + SPI_USR1_OFFS = 0x20 + SPI_USR2_OFFS = 0x24 + SPI_MOSI_DLEN_OFFS = None + SPI_MISO_DLEN_OFFS = None + SPI_W0_OFFS = 0x40 + + UART_CLKDIV_REG = 0x60000014 + + XTAL_CLK_DIVIDER = 2 + + FLASH_SIZES = { + '512KB': 0x00, + '256KB': 0x10, + '1MB': 0x20, + '2MB': 0x30, + '4MB': 0x40, + '2MB-c1': 0x50, + '4MB-c1': 0x60, + '8MB': 0x80, + '16MB': 0x90, + } + + BOOTLOADER_FLASH_OFFSET = 0 + + MEMORY_MAP = [[0x3FF00000, 0x3FF00010, "DPORT"], + [0x3FFE8000, 0x40000000, "DRAM"], + [0x40100000, 0x40108000, "IRAM"], + [0x40201010, 0x402E1010, "IROM"]] + + def get_efuses(self): + # Return the 128 bits of ESP8266 efuse as a single Python integer + result = self.read_reg(0x3ff0005c) << 96 + result |= self.read_reg(0x3ff00058) << 64 + result |= self.read_reg(0x3ff00054) << 32 + result |= self.read_reg(0x3ff00050) + return result + + def get_chip_description(self): + efuses = self.get_efuses() + is_8285 = (efuses & ((1 << 4) | 1 << 80)) != 0 # One or the other efuse bit is set for ESP8285 + return "ESP8285" if is_8285 else "ESP8266EX" + + def get_chip_features(self): + features = ["WiFi"] + if self.get_chip_description() == "ESP8285": + features += ["Embedded Flash"] + return features + + def flash_spi_attach(self, hspi_arg): + if self.IS_STUB: + super(ESP8266ROM, self).flash_spi_attach(hspi_arg) + else: + # ESP8266 ROM has no flash_spi_attach command in serial protocol, + # but flash_begin will do it + self.flash_begin(0, 0) + + def flash_set_parameters(self, size): + # not implemented in ROM, but OK to silently skip for ROM + if self.IS_STUB: + super(ESP8266ROM, self).flash_set_parameters(size) + + def chip_id(self): + """ Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() function """ + id0 = self.read_reg(self.ESP_OTP_MAC0) + id1 = self.read_reg(self.ESP_OTP_MAC1) + return (id0 >> 24) | ((id1 & MAX_UINT24) << 8) + def read_mac(self): + """ Read MAC from OTP ROM """ mac0 = self.read_reg(self.ESP_OTP_MAC0) mac1 = self.read_reg(self.ESP_OTP_MAC1) mac3 = self.read_reg(self.ESP_OTP_MAC3) @@ -249,72 +1176,740 @@ class ESPROM(object): raise FatalError("Unknown OUI") return oui + ((mac1 >> 8) & 0xff, mac1 & 0xff, (mac0 >> 24) & 0xff) - """ Read Chip ID from OTP ROM - see http://esp8266-re.foogod.com/wiki/System_get_chip_id_%28IoT_RTOS_SDK_0.9.9%29 """ + def get_erase_size(self, offset, size): + """ Calculate an erase size given a specific size in bytes. + + Provides a workaround for the bootloader erase bug.""" + + sectors_per_block = 16 + sector_size = self.FLASH_SECTOR_SIZE + num_sectors = (size + sector_size - 1) // sector_size + start_sector = offset // sector_size + + head_sectors = sectors_per_block - (start_sector % sectors_per_block) + if num_sectors < head_sectors: + head_sectors = num_sectors + + if num_sectors < 2 * head_sectors: + return (num_sectors + 1) // 2 * sector_size + else: + return (num_sectors - head_sectors) * sector_size + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("Overriding VDDSDIO setting only applies to ESP32") + + +class ESP8266StubLoader(ESP8266ROM): + """ Access class for ESP8266 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + def get_erase_size(self, offset, size): + return size # stub doesn't have same size bug as ROM loader + + +ESP8266ROM.STUB_CLASS = ESP8266StubLoader + + +class ESP32ROM(ESPLoader): + """Access class for ESP32 ROM bootloader + + """ + CHIP_NAME = "ESP32" + IMAGE_CHIP_ID = 0 + IS_STUB = False + + CHIP_DETECT_MAGIC_VALUE = 0x00f01d83 + + IROM_MAP_START = 0x400d0000 + IROM_MAP_END = 0x40400000 + + DROM_MAP_START = 0x3F400000 + DROM_MAP_END = 0x3F800000 + + # ESP32 uses a 4 byte status reply + STATUS_BYTES_LENGTH = 4 + + SPI_REG_BASE = 0x3ff42000 + SPI_USR_OFFS = 0x1c + SPI_USR1_OFFS = 0x20 + SPI_USR2_OFFS = 0x24 + SPI_MOSI_DLEN_OFFS = 0x28 + SPI_MISO_DLEN_OFFS = 0x2c + EFUSE_RD_REG_BASE = 0x3ff5a000 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18 + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = (1 << 7) # EFUSE_RD_DISABLE_DL_ENCRYPT + + DR_REG_SYSCON_BASE = 0x3ff66000 + + SPI_W0_OFFS = 0x80 + + UART_CLKDIV_REG = 0x3ff40014 + + XTAL_CLK_DIVIDER = 1 + + FLASH_SIZES = { + '1MB': 0x00, + '2MB': 0x10, + '4MB': 0x20, + '8MB': 0x30, + '16MB': 0x40 + } + + BOOTLOADER_FLASH_OFFSET = 0x1000 + + OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"] + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3F400000, 0x3F800000, "DROM"], + [0x3F800000, 0x3FC00000, "EXTRAM_DATA"], + [0x3FF80000, 0x3FF82000, "RTC_DRAM"], + [0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"], + [0x3FFAE000, 0x40000000, "DRAM"], + [0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"], + [0x40000000, 0x40070000, "IROM"], + [0x40070000, 0x40078000, "CACHE_PRO"], + [0x40078000, 0x40080000, "CACHE_APP"], + [0x40080000, 0x400A0000, "IRAM"], + [0x400A0000, 0x400BFFFC, "DIRAM_IRAM"], + [0x400C0000, 0x400C2000, "RTC_IRAM"], + [0x400D0000, 0x40400000, "IROM"], + [0x50000000, 0x50002000, "RTC_DATA"]] + + FLASH_ENCRYPTED_WRITE_ALIGN = 32 + + """ Try to read the BLOCK1 (encryption key) and check if it is valid """ + + def is_flash_encryption_key_valid(self): + + """ Bit 0 of efuse_rd_disable[3:0] is mapped to BLOCK1 + this bit is at position 16 in EFUSE_BLK0_RDATA0_REG """ + word0 = self.read_efuse(0) + rd_disable = (word0 >> 16) & 0x1 + + # reading of BLOCK1 is NOT ALLOWED so we assume valid key is programmed + if rd_disable: + return True + else: + # reading of BLOCK1 is ALLOWED so we will read and verify for non-zero. + # When ESP32 has not generated AES/encryption key in BLOCK1, the contents will be readable and 0. + # If the flash encryption is enabled it is expected to have a valid non-zero key. We break out on + # first occurance of non-zero value + key_word = [0] * 7 + for i in range(len(key_word)): + key_word[i] = self.read_efuse(14 + i) + # key is non-zero so break & return + if key_word[i] != 0: + return True + return False + + def get_flash_crypt_config(self): + """ For flash encryption related commands we need to make sure + user has programmed all the relevant efuse correctly so before + writing encrypted write_flash_encrypt esptool will verify the values + of flash_crypt_config to be non zero if they are not read + protected. If the values are zero a warning will be printed + + bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config + this bit is at position 19 in EFUSE_BLK0_RDATA0_REG """ + word0 = self.read_efuse(0) + rd_disable = (word0 >> 19) & 0x1 + + if rd_disable == 0: + """ we can read the flash_crypt_config efuse value + so go & read it (EFUSE_BLK0_RDATA5_REG[31:28]) """ + word5 = self.read_efuse(5) + word5 = (word5 >> 28) & 0xF + return word5 + else: + # if read of the efuse is disabled we assume it is set correctly + return 0xF + + def get_encrypted_download_disabled(self): + if self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG) & self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT: + return True + else: + return False + + def get_pkg_version(self): + word3 = self.read_efuse(3) + pkg_version = (word3 >> 9) & 0x07 + pkg_version += ((word3 >> 2) & 0x1) << 3 + return pkg_version + + def get_chip_revision(self): + word3 = self.read_efuse(3) + word5 = self.read_efuse(5) + apb_ctl_date = self.read_reg(self.DR_REG_SYSCON_BASE + 0x7C) + + rev_bit0 = (word3 >> 15) & 0x1 + rev_bit1 = (word5 >> 20) & 0x1 + rev_bit2 = (apb_ctl_date >> 31) & 0x1 + if rev_bit0: + if rev_bit1: + if rev_bit2: + return 3 + else: + return 2 + else: + return 1 + return 0 + + def get_chip_description(self): + pkg_version = self.get_pkg_version() + chip_revision = self.get_chip_revision() + rev3 = (chip_revision == 3) + single_core = self.read_efuse(3) & (1 << 0) # CHIP_VER DIS_APP_CPU + + chip_name = { + 0: "ESP32-S0WDQ6" if single_core else "ESP32-D0WDQ6", + 1: "ESP32-S0WD" if single_core else "ESP32-D0WD", + 2: "ESP32-D2WD", + 4: "ESP32-U4WDH", + 5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4", + 6: "ESP32-PICO-V3-02", + }.get(pkg_version, "unknown ESP32") + + # ESP32-D0WD-V3, ESP32-D0WDQ6-V3 + if chip_name.startswith("ESP32-D0WD") and rev3: + chip_name += "-V3" + + return "%s (revision %d)" % (chip_name, chip_revision) + + def get_chip_features(self): + features = ["WiFi"] + word3 = self.read_efuse(3) + + # names of variables in this section are lowercase + # versions of EFUSE names as documented in TRM and + # ESP-IDF efuse_reg.h + + chip_ver_dis_bt = word3 & (1 << 1) + if chip_ver_dis_bt == 0: + features += ["BT"] + + chip_ver_dis_app_cpu = word3 & (1 << 0) + if chip_ver_dis_app_cpu: + features += ["Single Core"] + else: + features += ["Dual Core"] + + chip_cpu_freq_rated = word3 & (1 << 13) + if chip_cpu_freq_rated: + chip_cpu_freq_low = word3 & (1 << 12) + if chip_cpu_freq_low: + features += ["160MHz"] + else: + features += ["240MHz"] + + pkg_version = self.get_pkg_version() + if pkg_version in [2, 4, 5, 6]: + features += ["Embedded Flash"] + + if pkg_version == 6: + features += ["Embedded PSRAM"] + + word4 = self.read_efuse(4) + adc_vref = (word4 >> 8) & 0x1F + if adc_vref: + features += ["VRef calibration in efuse"] + + blk3_part_res = word3 >> 14 & 0x1 + if blk3_part_res: + features += ["BLK3 partially reserved"] + + word6 = self.read_efuse(6) + coding_scheme = word6 & 0x3 + features += ["Coding Scheme %s" % { + 0: "None", + 1: "3/4", + 2: "Repeat (UNSUPPORTED)", + 3: "Invalid"}[coding_scheme]] + + return features + + def read_efuse(self, n): + """ Read the nth word of the ESP3x EFUSE region. """ + return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n)) + def chip_id(self): - id0 = self.read_reg(self.ESP_OTP_MAC0) - id1 = self.read_reg(self.ESP_OTP_MAC1) - return (id0 >> 24) | ((id1 & 0xffffff) << 8) + raise NotSupportedError(self, "chip_id") - """ Read SPI flash manufacturer and device id """ - def flash_id(self): - self.flash_begin(0, 0) - self.write_reg(0x60000240, 0x0, 0xffffffff) - self.write_reg(0x60000200, 0x10000000, 0xffffffff) - flash_id = self.read_reg(0x60000240) - self.flash_finish(False) - return flash_id + def read_mac(self): + """ Read MAC from EFUSE region """ + words = [self.read_efuse(2), self.read_efuse(1)] + bitstring = struct.pack(">II", *words) + bitstring = bitstring[2:8] # trim the 2 byte CRC + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) - """ Abuse the loader protocol to force flash to be left in write mode """ - def flash_unlock_dio(self): - # Enable flash write mode - self.flash_begin(0, 0) - # Reset the chip rather than call flash_finish(), which would have - # write protected the chip again (why oh why does it do that?!) - self.mem_begin(0,0,0,0x40100000) - self.mem_finish(0x40000080) + def get_erase_size(self, offset, size): + return size - """ Perform a chip erase of SPI flash """ - def flash_erase(self): - # Trick ROM to initialize SFlash - self.flash_begin(0, 0) + def override_vddsdio(self, new_voltage): + new_voltage = new_voltage.upper() + if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES: + raise FatalError("The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'") + RTC_CNTL_SDIO_CONF_REG = 0x3ff48074 + RTC_CNTL_XPD_SDIO_REG = (1 << 31) + RTC_CNTL_DREFH_SDIO_M = (3 << 29) + RTC_CNTL_DREFM_SDIO_M = (3 << 27) + RTC_CNTL_DREFL_SDIO_M = (3 << 25) + # RTC_CNTL_SDIO_TIEH = (1 << 23) # not used here, setting TIEH=1 would set 3.3V output, not safe for esptool.py to do + RTC_CNTL_SDIO_FORCE = (1 << 22) + RTC_CNTL_SDIO_PD_EN = (1 << 21) - # This is hacky: we don't have a custom stub, instead we trick - # the bootloader to jump to the SPIEraseChip() routine and then halt/crash - # when it tries to boot an unconfigured system. - self.mem_begin(0,0,0,0x40100000) - self.mem_finish(0x40004984) + reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting + reg_val |= RTC_CNTL_SDIO_PD_EN + if new_voltage != "OFF": + reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO + if new_voltage == "1.9V": + reg_val |= (RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M) # boost voltage + self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val) + print("VDDSDIO regulator set to %s" % new_voltage) - # Yup - there's no good way to detect if we succeeded. - # It it on the other hand unlikely to fail. + def read_flash_slow(self, offset, length, progress_fn): + BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow) - def run_stub(self, stub, params, read_output=True): - stub = dict(stub) - stub['code'] = unhexify(stub['code']) - if 'data' in stub: - stub['data'] = unhexify(stub['data']) + data = b'' + while len(data) < length: + block_len = min(BLOCK_LEN, length - len(data)) + r = self.check_command("read flash block", self.ESP_READ_FLASH_SLOW, + struct.pack('> 21) & 0x0F + return pkg_version + + def get_chip_description(self): + chip_name = { + 0: "ESP32-S2", + 1: "ESP32-S2FH16", + 2: "ESP32-S2FH32", + }.get(self.get_pkg_version(), "unknown ESP32-S2") + + return "%s" % (chip_name) + + def get_chip_features(self): + features = ["WiFi"] + + if self.secure_download_mode: + features += ["Secure Download Mode Enabled"] + + pkg_version = self.get_pkg_version() + + if pkg_version in [1, 2]: + if pkg_version == 1: + features += ["Embedded 2MB Flash"] + elif pkg_version == 2: + features += ["Embedded 4MB Flash"] + features += ["105C temp rating"] + + num_word = 4 + block2_addr = self.EFUSE_BASE + 0x05C + word4 = self.read_reg(block2_addr + (4 * num_word)) + block2_version = (word4 >> 4) & 0x07 + + if block2_version == 1: + features += ["ADC and temperature sensor calibration in BLK2 of efuse"] + return features + + def get_crystal_freq(self): + # ESP32-S2 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-S2") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-S2 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see either an AES-128 key or two AES-256 keys + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes): + return True + + return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) \ + and any(p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes) + + def uses_usb(self, _cache=[]): + if self.secure_download_mode: + return False # can't detect native USB in secure download mode + if not _cache: + buf_no = self.read_reg(self.UARTDEV_BUF_NO) & 0xff + _cache.append(buf_no == self.UARTDEV_BUF_NO_USB) + return _cache[0] + + def _post_connect(self): + if self.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + + def _check_if_can_reset(self): + """ + Check the strapping register to see if we can reset out of download mode. + """ + if os.getenv("ESPTOOL_TESTING") is not None: + print("ESPTOOL_TESTING is set, ignoring strapping mode check") + # Esptool tests over USB CDC run with GPIO0 strapped low, don't complain in this case. + return + strap_reg = self.read_reg(self.GPIO_STRAP_REG) + force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG) + if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0: + print("ERROR: {} chip was placed into download mode using GPIO0.\n" + "esptool.py can not exit the download mode over USB. " + "To run the app, reset the chip manually.\n" + "To suppress this error, set --after option to 'no_reset'.".format(self.get_chip_description())) + raise SystemExit(1) + + def hard_reset(self): + if self.uses_usb(): + self._check_if_can_reset() + + self._setRTS(True) # EN->LOW + if self.uses_usb(): + # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions + time.sleep(0.2) + self._setRTS(False) + time.sleep(0.2) + else: + self._setRTS(False) + + +class ESP32S3BETA2ROM(ESP32ROM): + CHIP_NAME = "ESP32-S3(beta2)" + IMAGE_CHIP_ID = 4 + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x44000000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3e000000 + + UART_DATE_REG_ADDR = 0x60000080 + + CHIP_DETECT_MAGIC_VALUE = 0xeb004136 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1c + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + EFUSE_REG_BASE = 0x6001A030 # BLOCK0 read base address + + MAC_EFUSE_REG = 0x6001A000 # ESP32S3 has special block for MAC efuses + + UART_CLKDIV_REG = 0x60000014 + + GPIO_STRAP_REG = 0x60004038 + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3C000000, 0x3D000000, "DROM"], + [0x3D000000, 0x3E000000, "EXTRAM_DATA"], + [0x600FE000, 0x60100000, "RTC_DRAM"], + [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"], + [0x3FC88000, 0x403E2000, "MEM_INTERNAL"], + [0x3FC88000, 0x3FD00000, "DRAM"], + [0x40000000, 0x4001A100, "IROM_MASK"], + [0x40370000, 0x403E0000, "IRAM"], + [0x600FE000, 0x60100000, "RTC_IRAM"], + [0x42000000, 0x42800000, "IROM"], + [0x50000000, 0x50002000, "RTC_DATA"]] + + def get_chip_description(self): + return "ESP32-S3(beta2)" + + def get_chip_features(self): + return ["WiFi", "BLE"] + + def get_crystal_freq(self): + # ESP32S3 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-S3") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + +class ESP32C3ROM(ESP32ROM): + CHIP_NAME = "ESP32-C3" + IMAGE_CHIP_ID = 5 + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42800000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c800000 + + SPI_REG_BASE = 0x3f402000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1c + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0xA8 + + BOOTLOADER_FLASH_OFFSET = 0x0 + + CHIP_DETECT_MAGIC_VALUE = 0x6921506f + + UART_DATE_REG_ADDR = 0x60000000 + 0x7c + + EFUSE_BASE = 0x60008800 + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + GPIO_STRAP_REG = 0x3f404038 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3C000000, 0x3C800000, "DROM"], + [0x3FC80000, 0x3FCE0000, "DRAM"], + [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"], + [0x3FF00000, 0x3FF20000, "DROM_MASK"], + [0x40000000, 0x40060000, "IROM_MASK"], + [0x42000000, 0x42800000, "IROM"], + [0x4037C000, 0x403E0000, "IRAM"], + [0x50000000, 0x50002000, "RTC_IRAM"], + [0x50000000, 0x50002000, "RTC_DRAM"], + [0x600FE000, 0x60100000, "MEM_INTERNAL2"]] + + def get_pkg_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C3", + }.get(self.get_pkg_version(), "unknown ESP32-C3") + + return "%s" % (chip_name) + + def get_chip_features(self): + return ["Wi-Fi"] + + def get_crystal_freq(self): + # ESP32C3 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-C3") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + +class ESP32StubLoader(ESP32ROM): + """ Access class for ESP32 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32ROM.STUB_CLASS = ESP32StubLoader + + +class ESP32S2StubLoader(ESP32S2ROM): + """ Access class for ESP32-S2 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + if rom_loader.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK + + +ESP32S2ROM.STUB_CLASS = ESP32S2StubLoader + + +class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM): + """ Access class for ESP32S3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader + + +class ESP32C3StubLoader(ESP32C3ROM): + """ Access class for ESP32C3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader class ESPBOOTLOADER(object): @@ -327,125 +1922,274 @@ class ESPBOOTLOADER(object): IMAGE_V2_SEGMENT = 4 -def LoadFirmwareImage(filename): - """ Load a firmware image, without knowing what kind of file (v1 or v2) it is. +def LoadFirmwareImage(chip, filename): + """ Load a firmware image. Can be for any supported SoC. - Returns a BaseFirmwareImage subclass, either ESPFirmwareImage (v1) or OTAFirmwareImage (v2). + ESP8266 images will be examined to determine if they are original ROM firmware images (ESP8266ROMFirmwareImage) + or "v2" OTA bootloader images. + + Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1) or ESP8266V2FirmwareImage (v2). """ + chip = chip.lower().replace("-", "") with open(filename, 'rb') as f: - magic = ord(f.read(1)) - f.seek(0) - if magic == ESPROM.ESP_IMAGE_MAGIC: - return ESPFirmwareImage(f) - elif magic == ESPBOOTLOADER.IMAGE_V2_MAGIC: - return OTAFirmwareImage(f) - else: - raise FatalError("Invalid image magic number: %d" % magic) + if chip == 'esp32': + return ESP32FirmwareImage(f) + elif chip == "esp32s2": + return ESP32S2FirmwareImage(f) + elif chip == "esp32s3beta2": + return ESP32S3BETA2FirmwareImage(f) + elif chip == 'esp32c3': + return ESP32C3FirmwareImage(f) + else: # Otherwise, ESP8266 so look at magic to determine the image type + magic = ord(f.read(1)) + f.seek(0) + if magic == ESPLoader.ESP_IMAGE_MAGIC: + return ESP8266ROMFirmwareImage(f) + elif magic == ESPBOOTLOADER.IMAGE_V2_MAGIC: + return ESP8266V2FirmwareImage(f) + else: + raise FatalError("Invalid image magic number: %d" % magic) + + +class ImageSegment(object): + """ Wrapper class for a segment in an ESP image + (very similar to a section in an ELFImage also) """ + def __init__(self, addr, data, file_offs=None): + self.addr = addr + self.data = data + self.file_offs = file_offs + self.include_in_checksum = True + if self.addr != 0: + self.pad_to_alignment(4) # pad all "real" ImageSegments 4 byte aligned length + + def copy_with_new_addr(self, new_addr): + """ Return a new ImageSegment with same data, but mapped at + a new address. """ + return ImageSegment(new_addr, self.data, 0) + + def split_image(self, split_len): + """ Return a new ImageSegment which splits "split_len" bytes + from the beginning of the data. Remaining bytes are kept in + this segment object (and the start address is adjusted to match.) """ + result = copy.copy(self) + result.data = self.data[:split_len] + self.data = self.data[split_len:] + self.addr += split_len + self.file_offs = None + result.file_offs = None + return result + + def __repr__(self): + r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr) + if self.file_offs is not None: + r += " file_offs 0x%08x" % (self.file_offs) + return r + + def pad_to_alignment(self, alignment): + self.data = pad_to(self.data, alignment, b'\x00') + + +class ELFSection(ImageSegment): + """ Wrapper class for a section in an ELF image, has a section + name as well as the common properties of an ImageSegment. """ + def __init__(self, name, addr, data): + super(ELFSection, self).__init__(addr, data) + self.name = name.decode("utf-8") + + def __repr__(self): + return "%s %s" % (self.name, super(ELFSection, self).__repr__()) class BaseFirmwareImage(object): + SEG_HEADER_LEN = 8 + SHA256_DIGEST_LEN = 32 + """ Base class with common firmware image functions """ def __init__(self): self.segments = [] self.entrypoint = 0 + self.elf_sha256 = None + self.elf_sha256_offset = 0 - def add_segment(self, addr, data, pad_to=4): - """ Add a segment to the image, with specified address & data - (padded to a boundary of pad_to size) """ - # Data should be aligned on word boundary - l = len(data) - if l % pad_to: - data += b"\x00" * (pad_to - l % pad_to) - if l > 0: - self.segments.append((addr, len(data), data)) + def load_common_header(self, load_file, expected_magic): + (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack(' 16: + raise FatalError('Invalid segment count %d (max 16). Usually this indicates a linker script problem.' % len(self.segments)) def load_segment(self, f, is_irom_segment=False): """ Load the next segment from the image file """ + file_offs = f.tell() (offset, size) = struct.unpack(' 0x40200000 or offset < 0x3ffe0000 or size > 65536: - raise FatalError('Suspicious segment 0x%x, length %d' % (offset, size)) + self.warn_if_unusual_segment(offset, size, is_irom_segment) segment_data = f.read(size) if len(segment_data) < size: raise FatalError('End of file reading segment 0x%x, length %d (actual length %d)' % (offset, size, len(segment_data))) - segment = (offset, size, segment_data) + segment = ImageSegment(offset, segment_data, file_offs) self.segments.append(segment) return segment + def warn_if_unusual_segment(self, offset, size, is_irom_segment): + if not is_irom_segment: + if offset > 0x40200000 or offset < 0x3ffe0000 or size > 65536: + print('WARNING: Suspicious segment 0x%x, length %d' % (offset, size)) + + def maybe_patch_segment_data(self, f, segment_data): + """If SHA256 digest of the ELF file needs to be inserted into this segment, do so. Returns segment data.""" + segment_len = len(segment_data) + file_pos = f.tell() # file_pos is position in the .bin file + if self.elf_sha256_offset >= file_pos and self.elf_sha256_offset < file_pos + segment_len: + # SHA256 digest needs to be patched into this binary segment, + # calculate offset of the digest inside the binary segment. + patch_offset = self.elf_sha256_offset - file_pos + # Sanity checks + if patch_offset < self.SEG_HEADER_LEN or patch_offset + self.SHA256_DIGEST_LEN > segment_len: + raise FatalError('Cannot place SHA256 digest on segment boundary' + '(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)' % + (self.elf_sha256_offset, file_pos, segment_len)) + # offset relative to the data part + patch_offset -= self.SEG_HEADER_LEN + if segment_data[patch_offset:patch_offset + self.SHA256_DIGEST_LEN] != b'\x00' * self.SHA256_DIGEST_LEN: + raise FatalError('Contents of segment at SHA256 digest offset 0x%x are not all zero. Refusing to overwrite.' % + self.elf_sha256_offset) + assert(len(self.elf_sha256) == self.SHA256_DIGEST_LEN) + segment_data = segment_data[0:patch_offset] + self.elf_sha256 + \ + segment_data[patch_offset + self.SHA256_DIGEST_LEN:] + return segment_data + def save_segment(self, f, segment, checksum=None): """ Save the next segment to the image file, return next checksum value if provided """ - (offset, size, data) = segment - f.write(struct.pack(' 0: + if len(irom_segments) != 1: + raise FatalError('Found %d segments that could be irom0. Bad ELF file?' % len(irom_segments)) + return irom_segments[0] + return None + + def get_non_irom_segments(self): + irom_segment = self.get_irom_segment() + return [s for s in self.segments if s != irom_segment] + + +class ESP8266ROMFirmwareImage(BaseFirmwareImage): """ 'Version 1' firmware image, segments loaded directly by the ROM bootloader. """ + + ROM_LOADER = ESP8266ROM + def __init__(self, load_file=None): - super(ESPFirmwareImage, self).__init__() + super(ESP8266ROMFirmwareImage, self).__init__() self.flash_mode = 0 self.flash_size_freq = 0 self.version = 1 if load_file is not None: - (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack(' 16: - raise FatalError('Invalid firmware image magic=%d segments=%d' % (magic, segments)) - - for i in xrange(segments): + for _ in range(segments): self.load_segment(load_file) self.checksum = self.read_checksum(load_file) - def save(self, filename): - with open(filename, 'wb') as f: - self.write_v1_header(f, self.segments) - checksum = ESPROM.ESP_CHECKSUM_MAGIC - for segment in self.segments: + self.verify() + + def default_output_name(self, input_file): + """ Derive a default output name from the ELF name. """ + return input_file + '-' + + def save(self, basename): + """ Save a set of V1 images for flashing. Parameter is a base filename. """ + # IROM data goes in its own plain binary file + irom_segment = self.get_irom_segment() + if irom_segment is not None: + with open("%s0x%05x.bin" % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START), "wb") as f: + f.write(irom_segment.data) + + # everything but IROM goes at 0x00000 in an image file + normal_segments = self.get_non_irom_segments() + with open("%s0x00000.bin" % basename, 'wb') as f: + self.write_common_header(f, normal_segments) + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + for segment in normal_segments: checksum = self.save_segment(f, segment, checksum) self.append_checksum(f, checksum) -class OTAFirmwareImage(BaseFirmwareImage): +ESP8266ROM.BOOTLOADER_IMAGE = ESP8266ROMFirmwareImage + + +class ESP8266V2FirmwareImage(BaseFirmwareImage): """ 'Version 2' firmware image, segments loaded by software bootloader stub (ie Espressif bootloader or rboot) """ + + ROM_LOADER = ESP8266ROM + def __init__(self, load_file=None): - super(OTAFirmwareImage, self).__init__() + super(ESP8266V2FirmwareImage, self).__init__() self.version = 2 if load_file is not None: - (magic, segments, first_flash_mode, first_flash_size_freq, first_entrypoint) = struct.unpack(' 16: - raise FatalError('Invalid V2 second header magic=%d segments=%d' % (magic, segments)) - # load all the usual segments - for _ in xrange(segments): + for _ in range(segments): self.load_segment(load_file) self.checksum = self.read_checksum(load_file) + self.verify() + + def default_output_name(self, input_file): + """ Derive a default output name from the ELF name. """ + irom_segment = self.get_irom_segment() + if irom_segment is not None: + irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START + else: + irom_offs = 0 + return "%s-0x%05x.bin" % (os.path.splitext(input_file)[0], + irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) + def save(self, filename): with open(filename, 'wb') as f: # Save first header for irom0 segment - f.write(struct.pack(' 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % + (segment.addr, last_addr)) + last_addr = segment.addr + + def get_alignment_data_needed(segment): + # Actual alignment (in data bytes) required for a segment header: positioned so that + # after we write the next 8 byte header, file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN + # + # (this is because the segment's vaddr may not be IROM_ALIGNed, more likely is aligned + # IROM_ALIGN+0x18 to account for the binary file header + align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN + pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past + if pad_len == 0 or pad_len == self.IROM_ALIGN: + return 0 # already aligned + + # subtract SEG_HEADER_LEN a second time, as the padding block has a header as well + pad_len -= self.SEG_HEADER_LEN + if pad_len < 0: + pad_len += self.IROM_ALIGN + return pad_len + + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + pad_len = get_alignment_data_needed(segment) + if pad_len > 0: # need to pad + if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN: + pad_segment = ram_segments[0].split_image(pad_len) + if len(ram_segments[0].data) == 0: + ram_segments.pop(0) + else: + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + else: + # write the flash segment + assert (f.tell() + 8) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN + checksum = self.save_flash_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + if self.secure_pad: + # pad the image so that after signing it will end on a a 64KB boundary. + # This ensures all mapped flash content will be verified. + if not self.append_digest: + raise FatalError("secure_pad only applies if a SHA-256 digest is also appended to the image") + align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN + # 16 byte aligned checksum (force the alignment to simplify calculations) + checksum_space = 16 + if self.secure_pad == '1': + # after checksum: SHA-256 digest + (to be added by signing process) version, signature + 12 trailing bytes due to alignment + space_after_checksum = 32 + 4 + 64 + 12 + elif self.secure_pad == '2': # Secure Boot V2 + # after checksum: SHA-256 digest + signature sector, but we place signature sector after the 64KB boundary + space_after_checksum = 32 + pad_len = (self.IROM_ALIGN - align_past - checksum_space - space_after_checksum) % self.IROM_ALIGN + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) + + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + if self.secure_pad: + assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0 + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + try: + f.write(chr(total_segments)) + except TypeError: # Python 3 + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + with open(filename, 'wb') as real_file: + real_file.write(f.getvalue()) + + def save_flash_segment(self, f, segment, checksum=None): + """ Save the next segment to the image file, return next checksum value if provided """ + segment_end_pos = f.tell() + len(segment.data) + self.SEG_HEADER_LEN + segment_len_remainder = segment_end_pos % self.IROM_ALIGN + if segment_len_remainder < 0x24: + # Work around a bug in ESP-IDF 2nd stage bootloader, that it didn't map the + # last MMU page, if an IROM/DROM segment was < 0x24 bytes over the page boundary. + segment.data += b'\x00' * (0x24 - segment_len_remainder) + return self.save_segment(f, segment, checksum) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + chip_id = fields[4] + if chip_id != self.ROM_LOADER.IMAGE_CHIP_ID: + print(("Unexpected chip id in image. Expected %d but value was %d. " + "Is this image for a different chip model?") % (self.ROM_LOADER.IMAGE_CHIP_ID, chip_id)) + + # reserved fields in the middle should all be zero + if any(f for f in fields[6:-1] if f != 0): + print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?") + + append_digest = fields[-1] # last byte is append_digest + if append_digest in [0, 1]: + self.append_digest = (append_digest == 1) + else: + raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", append_digest) + + def save_extended_header(self, save_file): + def join_byte(ln, hn): + return (ln & 0x0F) + ((hn & 0x0F) << 4) + + append_digest = 1 if self.append_digest else 0 + + fields = [self.wp_pin, + join_byte(self.clk_drv, self.q_drv), + join_byte(self.d_drv, self.cs_drv), + join_byte(self.hd_drv, self.wp_drv), + self.ROM_LOADER.IMAGE_CHIP_ID, + self.min_rev] + fields += [0] * 8 # padding + fields += [append_digest] + + packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields) + save_file.write(packed) + + +ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage + + +class ESP32S2FirmwareImage(ESP32FirmwareImage): + """ ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S2ROM + + +ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage + + +class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage): + """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S3BETA2ROM + + +ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage + + +class ESP32C3FirmwareImage(ESP32FirmwareImage): + """ ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C3ROM + + +ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage + class ELFFile(object): + SEC_TYPE_PROGBITS = 0x01 + SEC_TYPE_STRTAB = 0x03 + + LEN_SEC_HEADER = 0x28 + def __init__(self, name): - self.name = binutils_safe_path(name) - self.symbols = None + # Load sections from the ELF file + self.name = name + with open(self.name, 'rb') as f: + self._read_elf_file(f) - def _fetch_symbols(self): - if self.symbols is not None: - return - self.symbols = {} + def get_section(self, section_name): + for s in self.sections: + if s.name == section_name: + return s + raise ValueError("No section %s in ELF file" % section_name) + + def _read_elf_file(self, f): + # read the ELF file header + LEN_FILE_HEADER = 0x34 try: - tool_nm = "xtensa-lx106-elf-nm" - if os.getenv('XTENSA_CORE') == 'lx106': - tool_nm = "xt-nm" - proc = subprocess.Popen([tool_nm, self.name], stdout=subprocess.PIPE) - except OSError: - print "Error calling %s, do you have Xtensa toolchain in PATH?" % tool_nm - sys.exit(1) - for l in proc.stdout: - fields = l.strip().split() - try: - if fields[0] == "U": - print "Warning: ELF binary has undefined symbol %s" % fields[1] - continue - if fields[0] == "w": - continue # can skip weak symbols - self.symbols[fields[2]] = int(fields[0], 16) - except ValueError: - raise FatalError("Failed to strip symbol output from nm: %s" % fields) + (ident, _type, machine, _version, + self.entrypoint, _phoff, shoff, _flags, + _ehsize, _phentsize, _phnum, shentsize, + shnum, shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) + except struct.error as e: + raise FatalError("Failed to read a valid ELF header from %s: %s" % (self.name, e)) - def get_symbol_addr(self, sym): - self._fetch_symbols() - return self.symbols[sym] + if byte(ident, 0) != 0x7f or ident[1:4] != b'ELF': + raise FatalError("%s has invalid ELF magic header" % self.name) + if machine not in [0x5e, 0xf3]: + raise FatalError("%s does not appear to be an Xtensa or an RISCV ELF file. e_machine=%04x" % (self.name, machine)) + if shentsize != self.LEN_SEC_HEADER: + raise FatalError("%s has unexpected section header entry size 0x%x (not 0x%x)" % (self.name, shentsize, self.LEN_SEC_HEADER)) + if shnum == 0: + raise FatalError("%s has 0 section headers" % (self.name)) + self._read_sections(f, shoff, shnum, shstrndx) - def get_entry_point(self): - tool_readelf = "xtensa-lx106-elf-readelf" - if os.getenv('XTENSA_CORE') == 'lx106': - tool_readelf = "xt-readelf" - try: - proc = subprocess.Popen([tool_readelf, "-h", self.name], stdout=subprocess.PIPE) - except OSError: - print "Error calling %s, do you have Xtensa toolchain in PATH?" % tool_readelf - sys.exit(1) - for l in proc.stdout: - fields = l.strip().split() - if fields[0] == "Entry": - return int(fields[3], 0) + def _read_sections(self, f, section_header_offs, section_header_count, shstrndx): + f.seek(section_header_offs) + len_bytes = section_header_count * self.LEN_SEC_HEADER + section_header = f.read(len_bytes) + if len(section_header) == 0: + raise FatalError("No section header found at offset %04x in ELF file." % section_header_offs) + if len(section_header) != (len_bytes): + raise FatalError("Only read 0x%x bytes from section header (expected 0x%x.) Truncated ELF file?" % (len(section_header), len_bytes)) - def load_section(self, section): - tool_objcopy = "xtensa-lx106-elf-objcopy" - if os.getenv('XTENSA_CORE') == 'lx106': - tool_objcopy = "xt-objcopy" - tmpsection = binutils_safe_path(tempfile.mktemp(suffix=".section")) - try: - subprocess.check_call([tool_objcopy, "--only-section", section, "-Obinary", self.name, tmpsection]) - with open(tmpsection, "rb") as f: - data = f.read() - finally: - os.remove(tmpsection) - return data + # walk through the section header and extract all sections + section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER) + + def read_section_header(offs): + name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from(" 0] + self.sections = prog_sections + + def sha256(self): + # return SHA256 hash of the input ELF file + sha256 = hashlib.sha256() + with open(self.name, 'rb') as f: + sha256.update(f.read()) + return sha256.digest() -class CesantaFlasher(object): - - # From stub_flasher.h - CMD_FLASH_WRITE = 1 - CMD_FLASH_READ = 2 - CMD_FLASH_DIGEST = 3 - CMD_BOOT_FW = 6 - - def __init__(self, esp, baud_rate=0): - print 'Running Cesanta flasher stub...' - if baud_rate <= ESPROM.ESP_ROM_BAUD: # don't change baud rates if we already synced at that rate - baud_rate = 0 - self._esp = esp - esp.run_stub(json.loads(_CESANTA_FLASHER_STUB), [baud_rate], read_output=False) - if baud_rate > 0: - esp._port.baudrate = baud_rate - # Read the greeting. - p = esp.read() - if p != 'OHAI': - raise FatalError('Failed to connect to the flasher (got %s)' % hexify(p)) - - def flash_write(self, addr, data, show_progress=False): - assert addr % self._esp.ESP_FLASH_SECTOR == 0, 'Address must be sector-aligned' - assert len(data) % self._esp.ESP_FLASH_SECTOR == 0, 'Length must be sector-aligned' - sys.stdout.write('Writing %d @ 0x%x... ' % (len(data), addr)) - sys.stdout.flush() - self._esp.write(struct.pack(' length: - raise FatalError('Read more than expected') - p = self._esp.read() - if len(p) != 16: - raise FatalError('Expected digest, got: %s' % hexify(p)) - expected_digest = hexify(p).upper() - digest = hashlib.md5(data).hexdigest().upper() - print - if digest != expected_digest: - raise FatalError('Digest mismatch: expected %s, got %s' % (expected_digest, digest)) - p = self._esp.read() - if len(p) != 1: - raise FatalError('Expected status, got: %s' % hexify(p)) - status_code = struct.unpack(' 16 bytes) will be + printed as separately indented lines, with ASCII decoding at the end + of each line. + """ + def __init__(self, binary_string, auto_split=True): + self._s = binary_string + self._auto_split = auto_split + + def __str__(self): + if self._auto_split and len(self._s) > 16: + result = "" + s = self._s + while len(s) > 0: + line = s[:16] + ascii_line = "".join(c if (c == ' ' or (c in string.printable and c not in string.whitespace)) + else '.' for c in line.decode('ascii', 'replace')) + s = s[16:] + result += "\n %-16s %-16s | %s" % (hexify(line[:8], False), hexify(line[8:], False), ascii_line) + return result + else: + return hexify(self._s, False) + + +def pad_to(data, alignment, pad_character=b'\xFF'): + """ Pad to the next alignment boundary """ + pad_mod = len(data) % alignment + if pad_mod != 0: + data += pad_character * (alignment - pad_mod) + return data class FatalError(RuntimeError): @@ -770,170 +2759,439 @@ class FatalError(RuntimeError): @staticmethod def WithResult(message, result): """ - Return a fatal error object that includes the hex values of + Return a fatal error object that appends the hex values of 'result' as a string formatted argument. """ - return FatalError(message % ", ".join(hex(ord(x)) for x in result)) + message += " (result was %s)" % hexify(result) + return FatalError(message) +class NotImplementedInROMError(FatalError): + """ + Wrapper class for the error thrown when a particular ESP bootloader function + is not implemented in the ROM bootloader. + """ + def __init__(self, bootloader, func): + FatalError.__init__(self, "%s ROM does not support function %s." % (bootloader.CHIP_NAME, func.__name__)) + + +class NotSupportedError(FatalError): + def __init__(self, esp, function_name): + FatalError.__init__(self, "Function %s is not supported for %s." % (function_name, esp.CHIP_NAME)) + # "Operation" commands, executable at command line. One function each # -# Each function takes either two args (, ) or a single +# Each function takes either two args (, ) or a single # argument. -def load_ram(esp, args): - image = LoadFirmwareImage(args.filename) - print 'RAM boot...' - for (offset, size, data) in image.segments: - print 'Downloading %d bytes at %08x...' % (size, offset), +class UnsupportedCommandError(RuntimeError): + """ + Wrapper class for when ROM loader returns an invalid command response. + + Usually this indicates the loader is running in Secure Download Mode. + """ + def __init__(self, esp, op): + if esp.secure_download_mode: + msg = "This command (0x%x) is not supported in Secure Download Mode" % op + else: + msg = "Invalid (unsupported) command 0x%x" % op + RuntimeError.__init__(self, msg) + + +def load_ram(esp, args): + image = LoadFirmwareImage(esp.CHIP_NAME, args.filename) + + print('RAM boot...') + for seg in image.segments: + size = len(seg.data) + print('Downloading %d bytes at %08x...' % (size, seg.addr), end=' ') sys.stdout.flush() - esp.mem_begin(size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, offset) + esp.mem_begin(size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr) seq = 0 - while len(data) > 0: - esp.mem_block(data[0:esp.ESP_RAM_BLOCK], seq) - data = data[esp.ESP_RAM_BLOCK:] + while len(seg.data) > 0: + esp.mem_block(seg.data[0:esp.ESP_RAM_BLOCK], seq) + seg.data = seg.data[esp.ESP_RAM_BLOCK:] seq += 1 - print 'done!' + print('done!') - print 'All segments done, executing at %08x' % image.entrypoint + print('All segments done, executing at %08x' % image.entrypoint) esp.mem_finish(image.entrypoint) def read_mem(esp, args): - print '0x%08x = 0x%08x' % (args.address, esp.read_reg(args.address)) + print('0x%08x = 0x%08x' % (args.address, esp.read_reg(args.address))) def write_mem(esp, args): esp.write_reg(args.address, args.value, args.mask, 0) - print 'Wrote %08x, mask %08x to %08x' % (args.value, args.mask, args.address) + print('Wrote %08x, mask %08x to %08x' % (args.value, args.mask, args.address)) def dump_mem(esp, args): - f = file(args.filename, 'wb') - for i in xrange(args.size / 4): - d = esp.read_reg(args.address + (i * 4)) - f.write(struct.pack('> 16 + args.flash_size = DETECTED_FLASH_SIZES.get(size_id) + if args.flash_size is None: + print('Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x), defaulting to 4MB' % (flash_id, size_id)) + args.flash_size = '4MB' + else: + print('Auto-detected Flash size:', args.flash_size) + + +def _update_image_flash_params(esp, address, args, image): + """ Modify the flash mode & size bytes if this looks like an executable bootloader image """ + if len(image) < 8: + return image # not long enough to be a bootloader image + + # unpack the (potential) image header + magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4]) + if address != esp.BOOTLOADER_FLASH_OFFSET: + return image # not flashing bootloader offset, so don't modify this + + if (args.flash_mode, args.flash_freq, args.flash_size) == ('keep',) * 3: + return image # all settings are 'keep', not modifying anything + + # easy check if this is an image: does it start with a magic byte? + if magic != esp.ESP_IMAGE_MAGIC: + print("Warning: Image file at 0x%x doesn't look like an image file, so not changing any flash settings." % address) + return image + + # make sure this really is an image, and not just data that + # starts with esp.ESP_IMAGE_MAGIC (mostly a problem for encrypted + # images that happen to start with a magic byte + try: + test_image = esp.BOOTLOADER_IMAGE(io.BytesIO(image)) + test_image.verify() + except Exception: + print("Warning: Image file at 0x%x is not a valid %s image, so not changing any flash settings." % (address, esp.CHIP_NAME)) + return image + + if args.flash_mode != 'keep': + flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] + + flash_freq = flash_size_freq & 0x0F + if args.flash_freq != 'keep': + flash_freq = {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] + + flash_size = flash_size_freq & 0xF0 + if args.flash_size != 'keep': + flash_size = esp.parse_flash_size_arg(args.flash_size) + + flash_params = struct.pack(b'BB', flash_mode, flash_size + flash_freq) + if flash_params != image[2:4]: + print('Flash params set to 0x%04x' % struct.unpack(">H", flash_params)) + image = image[0:2] + flash_params + image[4:] + return image def write_flash(esp, args): - flash_mode = {'qio':0, 'qout':1, 'dio':2, 'dout': 3}[args.flash_mode] - flash_size_freq = {'4m':0x00, '2m':0x10, '8m':0x20, '16m':0x30, '32m':0x40, '16m-c1': 0x50, '32m-c1':0x60, '32m-c2':0x70, '64m':0x80, '128m':0x90}[args.flash_size] - flash_size_freq += {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq] - flash_params = struct.pack('BB', flash_mode, flash_size_freq) + # set args.compress based on default behaviour: + # -> if either --compress or --no-compress is set, honour that + # -> otherwise, set --compress unless --no-stub is set + if args.compress is None and not args.no_compress: + args.compress = not args.no_stub - flasher = CesantaFlasher(esp, args.baud) + # In case we have encrypted files to write, we first do few sanity checks before actual flash + if args.encrypt or args.encrypt_files is not None: + do_write = True - for address, argfile in args.addr_filename: - image = argfile.read() - argfile.seek(0) # rewind in case we need it again - # Fix sflash config data. - if address == 0 and image[0] == '\xe9': - print 'Flash params set to 0x%02x%02x' % (flash_mode, flash_size_freq) - image = image[0:2] + flash_params + image[4:] - # Pad to sector size, which is the minimum unit of writing (erasing really). - if len(image) % esp.ESP_FLASH_SECTOR != 0: - image += '\xff' * (esp.ESP_FLASH_SECTOR - (len(image) % esp.ESP_FLASH_SECTOR)) + if not esp.secure_download_mode: + if esp.get_encrypted_download_disabled(): + raise FatalError("This chip has encrypt functionality in UART download mode disabled. " + "This is the Flash Encryption configuration for Production mode instead of Development mode.") + + crypt_cfg_efuse = esp.get_flash_crypt_config() + + if crypt_cfg_efuse is not None and crypt_cfg_efuse != 0xF: + print('Unexpected FLASH_CRYPT_CONFIG value: 0x%x' % (crypt_cfg_efuse)) + do_write = False + + enc_key_valid = esp.is_flash_encryption_key_valid() + + if not enc_key_valid: + print('Flash encryption key is not programmed') + do_write = False + + # Determine which files list contain the ones to encrypt + files_to_encrypt = args.addr_filename if args.encrypt else args.encrypt_files + + for address, argfile in files_to_encrypt: + if address % esp.FLASH_ENCRYPTED_WRITE_ALIGN: + print("File %s address 0x%x is not %d byte aligned, can't flash encrypted" % + (argfile.name, address, esp.FLASH_ENCRYPTED_WRITE_ALIGN)) + do_write = False + + if not do_write and not args.ignore_flash_encryption_efuse_setting: + raise FatalError("Can't perform encrypted flash write, consult Flash Encryption documentation for more information") + + # verify file sizes fit in flash + if args.flash_size != 'keep': # TODO: check this even with 'keep' + flash_end = flash_size_bytes(args.flash_size) + for address, argfile in args.addr_filename: + argfile.seek(0, 2) # seek to end + if address + argfile.tell() > flash_end: + raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " + "Use --flash-size argument, or change flashing address.") + % (argfile.name, argfile.tell(), address, flash_end)) + argfile.seek(0) + + if args.erase_all: + erase_flash(esp, args) + + """ Create a list describing all the files we have to flash. Each entry holds an "encrypt" flag + marking whether the file needs encryption or not. This list needs to be sorted. + + First, append to each entry of our addr_filename list the flag args.encrypt + For example, if addr_filename is [(0x1000, "partition.bin"), (0x8000, "bootloader")], + all_files will be [(0x1000, "partition.bin", args.encrypt), (0x8000, "bootloader", args.encrypt)], + where, of course, args.encrypt is either True or False + """ + all_files = [(offs, filename, args.encrypt) for (offs, filename) in args.addr_filename] + + """Now do the same with encrypt_files list, if defined. + In this case, the flag is True + """ + if args.encrypt_files is not None: + encrypted_files_flag = [(offs, filename, True) for (offs, filename) in args.encrypt_files] + + # Concatenate both lists and sort them. + # As both list are already sorted, we could simply do a merge instead, + # but for the sake of simplicity and because the lists are very small, + # let's use sorted. + all_files = sorted(all_files + encrypted_files_flag, key=lambda x: x[0]) + + for address, argfile, encrypted in all_files: + compress = args.compress + + # Check whether we can compress the current file before flashing + if compress and encrypted: + print('\nWARNING: - compress and encrypt options are mutually exclusive ') + print('Will flash %s uncompressed' % argfile.name) + compress = False + + if args.no_stub: + print('Erasing flash...') + image = pad_to(argfile.read(), esp.FLASH_ENCRYPTED_WRITE_ALIGN if encrypted else 4) + if len(image) == 0: + print('WARNING: File %s is empty' % argfile.name) + continue + image = _update_image_flash_params(esp, address, args, image) + calcmd5 = hashlib.md5(image).hexdigest() + uncsize = len(image) + if compress: + uncimage = image + image = zlib.compress(uncimage, 9) + blocks = esp.flash_defl_begin(uncsize, len(image), address) + else: + blocks = esp.flash_begin(uncsize, address, begin_rom_encrypted=encrypted) + argfile.seek(0) # in case we need it again + seq = 0 + written = 0 t = time.time() - flasher.flash_write(address, image, not args.no_progress) + while len(image) > 0: + print_overwrite('Writing at 0x%08x... (%d %%)' % (address + seq * esp.FLASH_WRITE_SIZE, 100 * (seq + 1) // blocks)) + sys.stdout.flush() + block = image[0:esp.FLASH_WRITE_SIZE] + if compress: + esp.flash_defl_block(block, seq, timeout=timeout_per_mb(COMP_BLOCK_WRITE_TIMEOUT_PER_MB, uncsize)) + else: + # Pad the last block + block = block + b'\xff' * (esp.FLASH_WRITE_SIZE - len(block)) + if encrypted: + esp.flash_encrypt_block(block, seq) + else: + esp.flash_block(block, seq) + image = image[esp.FLASH_WRITE_SIZE:] + seq += 1 + written += len(block) t = time.time() - t - print ('\rWrote %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' - % (len(image), address, t, len(image) / t * 8 / 1000)) - print 'Leaving...' + speed_msg = "" + if compress: + if t > 0.0: + speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000) + print_overwrite('Wrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % (uncsize, written, address, t, speed_msg), last_line=True) + else: + if t > 0.0: + speed_msg = " (%.1f kbit/s)" % (written / t * 8 / 1000) + print_overwrite('Wrote %d bytes at 0x%08x in %.1f seconds%s...' % (written, address, t, speed_msg), last_line=True) + + if not encrypted and not esp.secure_download_mode: + try: + res = esp.flash_md5sum(address, uncsize) + if res != calcmd5: + print('File md5: %s' % calcmd5) + print('Flash md5: %s' % res) + print('MD5 of 0xFF is %s' % (hashlib.md5(b'\xFF' * uncsize).hexdigest())) + raise FatalError("MD5 of file does not match data in flash!") + else: + print('Hash of data verified.') + except NotImplementedInROMError: + pass + + print('\nLeaving...') + + if esp.IS_STUB: + # skip sending flash_finish to ROM loader here, + # as it causes the loader to exit and run user code + esp.flash_begin(0, 0) + + # Get the "encrypted" flag for the last file flashed + # Note: all_files list contains triplets like: + # (address: Integer, filename: String, encrypted: Boolean) + last_file_encrypted = all_files[-1][2] + + # Check whether the last file flashed was compressed or not + if args.compress and not last_file_encrypted: + esp.flash_defl_finish(False) + else: + esp.flash_finish(False) + if args.verify: - print 'Verifying just-written flash...' - _verify_flash(flasher, args, flash_params) - flasher.boot_fw() + print('Verifying just-written flash...') + print('(This option is deprecated, flash contents are now always read back after flashing.)') + # If some encrypted files have been flashed print a warning saying that we won't check them + if args.encrypt or args.encrypt_files is not None: + print('WARNING: - cannot verify encrypted files, they will be ignored') + # Call verify_flash function only if there at least one non-encrypted file flashed + if not args.encrypt: + verify_flash(esp, args) def image_info(args): - image = LoadFirmwareImage(args.filename) + image = LoadFirmwareImage(args.chip, args.filename) print('Image version: %d' % image.version) - print('Entry point: %08x' % image.entrypoint) if image.entrypoint != 0 else 'Entry point not set' - print '%d segments' % len(image.segments) - print - checksum = ESPROM.ESP_CHECKSUM_MAGIC - for (idx, (offset, size, data)) in enumerate(image.segments): - if image.version == 2 and idx == 0: - print 'Segment 1: %d bytes IROM0 (no load address)' % size - else: - print 'Segment %d: %5d bytes at %08x' % (idx + 1, size, offset) - checksum = ESPROM.checksum(data, checksum) - print - print 'Checksum: %02x (%s)' % (image.checksum, 'valid' if image.checksum == checksum else 'invalid!') + print('Entry point: %08x' % image.entrypoint if image.entrypoint != 0 else 'Entry point not set') + print('%d segments' % len(image.segments)) + print() + idx = 0 + for seg in image.segments: + idx += 1 + seg_name = ", ".join([seg_range[2] for seg_range in image.ROM_LOADER.MEMORY_MAP if seg_range[0] <= seg.addr < seg_range[1]]) + print('Segment %d: %r [%s]' % (idx, seg, seg_name)) + calc_checksum = image.calculate_checksum() + print('Checksum: %02x (%s)' % (image.checksum, + 'valid' if image.checksum == calc_checksum else 'invalid - calculated %02x' % calc_checksum)) + try: + digest_msg = 'Not appended' + if image.append_digest: + is_valid = image.stored_digest == image.calc_digest + digest_msg = "%s (%s)" % (hexify(image.calc_digest).lower(), + "valid" if is_valid else "invalid") + print('Validation Hash: %s' % digest_msg) + except AttributeError: + pass # ESP8266 image has no append_digest field def make_image(args): - image = ESPFirmwareImage() + image = ESP8266ROMFirmwareImage() if len(args.segfile) == 0: raise FatalError('No segments specified') if len(args.segfile) != len(args.segaddr): raise FatalError('Number of specified files does not match number of specified addresses') for (seg, addr) in zip(args.segfile, args.segaddr): - data = file(seg, 'rb').read() - image.add_segment(addr, data) + with open(seg, 'rb') as f: + data = f.read() + image.segments.append(ImageSegment(addr, data)) image.entrypoint = args.entrypoint image.save(args.output) def elf2image(args): e = ELFFile(args.input) - if args.version == '1': - image = ESPFirmwareImage() + if args.chip == 'auto': # Default to ESP8266 for backwards compatibility + print("Creating image for ESP8266...") + args.chip = 'esp8266' + + if args.chip == 'esp32': + image = ESP32FirmwareImage() + if args.secure_pad: + image.secure_pad = '1' + elif args.secure_pad_v2: + image.secure_pad = '2' + image.min_rev = int(args.min_rev) + elif args.chip == 'esp32s2': + image = ESP32S2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + image.min_rev = 0 + elif args.chip == 'esp32s3beta2': + image = ESP32S3BETA2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + image.min_rev = 0 + elif args.chip == 'esp32c3': + image = ESP32C3FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + image.min_rev = 0 + elif args.version == '1': # ESP8266 + image = ESP8266ROMFirmwareImage() else: - image = OTAFirmwareImage() - irom_data = e.load_section('.irom0.text') - if len(irom_data) == 0: - raise FatalError(".irom0.text section not found in ELF file - can't create V2 image.") - image.add_segment(0, irom_data, 16) - image.entrypoint = e.get_entry_point() - for section, start in ((".text", "_text_start"), (".data", "_data_start"), (".rodata", "_rodata_start")): - data = e.load_section(section) - image.add_segment(e.get_symbol_addr(start), data) + image = ESP8266V2FirmwareImage() + image.entrypoint = e.entrypoint + image.segments = e.sections # ELFSection is a subclass of ImageSegment + image.flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] + image.flash_size_freq = image.ROM_LOADER.FLASH_SIZES[args.flash_size] + image.flash_size_freq += {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] - image.flash_mode = {'qio':0, 'qout':1, 'dio':2, 'dout': 3}[args.flash_mode] - image.flash_size_freq = {'4m':0x00, '2m':0x10, '8m':0x20, '16m':0x30, '32m':0x40, '16m-c1': 0x50, '32m-c1':0x60, '32m-c2':0x70, '64m':0x80, '128m':0x90}[args.flash_size] - image.flash_size_freq += {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq] + if args.elf_sha256_offset: + image.elf_sha256 = e.sha256() + image.elf_sha256_offset = args.elf_sha256_offset - irom_offs = e.get_symbol_addr("_irom0_text_start") - 0x40200000 + image.verify() - if args.version == '1': - if args.output is None: - args.output = args.input + '-' - image.save(args.output + "0x00000.bin") - data = e.load_section(".irom0.text") - if irom_offs < 0: - raise FatalError('Address of symbol _irom0_text_start in ELF is located before flash mapping address. Bad linker script?') - if (irom_offs & 0xFFF) != 0: # irom0 isn't flash sector aligned - print "WARNING: irom0 section offset is 0x%08x. ELF is probably linked for 'elf2image --version=2'" % irom_offs - with open(args.output + "0x%05x.bin" % irom_offs, "wb") as f: - f.write(data) - f.close() - else: # V2 OTA image - if args.output is None: - args.output = "%s-0x%05x.bin" % (os.path.splitext(args.input)[0], irom_offs & ~(ESPROM.ESP_FLASH_SECTOR - 1)) - image.save(args.output) + if args.output is None: + args.output = image.default_output_name(args.input) + image.save(args.output) def read_mac(esp, args): mac = esp.read_mac() - print 'MAC: %s' % ':'.join(map(lambda x: '%02x' % x, mac)) + + def print_mac(label, mac): + print('%s: %s' % (label, ':'.join(map(lambda x: '%02x' % x, mac)))) + print_mac("MAC", mac) def chip_id(esp, args): - chipid = esp.chip_id() - print 'Chip ID: 0x%08x' % chipid + try: + chipid = esp.chip_id() + print('Chip ID: 0x%08x' % chipid) + except NotSupportedError: + print('Warning: %s has no Chip ID. Reading MAC instead.' % esp.CHIP_NAME) + read_mac(esp, args) def erase_flash(esp, args): - print 'Erasing flash (this may take a while)...' - esp.flash_erase() + print('Erasing flash (this may take a while)...') + t = time.time() + esp.erase_flash() + print('Chip erase completed successfully in %.1fs' % (time.time() - t)) + + +def erase_region(esp, args): + print('Erasing region (may be slow depending on size)...') + t = time.time() + esp.erase_region(args.address, args.size) + print('Erase completed successfully in %.1f seconds.' % (time.time() - t)) def run(esp, args): @@ -942,83 +3200,170 @@ def run(esp, args): def flash_id(esp, args): flash_id = esp.flash_id() - print 'Manufacturer: %02x' % (flash_id & 0xff) - print 'Device: %02x%02x' % ((flash_id >> 8) & 0xff, (flash_id >> 16) & 0xff) + print('Manufacturer: %02x' % (flash_id & 0xff)) + flid_lowbyte = (flash_id >> 16) & 0xFF + print('Device: %02x%02x' % ((flash_id >> 8) & 0xff, flid_lowbyte)) + print('Detected flash size: %s' % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))) def read_flash(esp, args): - flasher = CesantaFlasher(esp, args.baud) + if args.no_progress: + flash_progress = None + else: + def flash_progress(progress, length): + msg = '%d (%d %%)' % (progress, progress * 100.0 / length) + padding = '\b' * len(msg) + if progress == length: + padding = '\n' + sys.stdout.write(msg + padding) + sys.stdout.flush() t = time.time() - data = flasher.flash_read(args.address, args.size, not args.no_progress) + data = esp.read_flash(args.address, args.size, flash_progress) t = time.time() - t - print ('\rRead %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' - % (len(data), args.address, t, len(data) / t * 8 / 1000)) - file(args.filename, 'wb').write(data) + print_overwrite('Read %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' + % (len(data), args.address, t, len(data) / t * 8 / 1000), last_line=True) + with open(args.filename, 'wb') as f: + f.write(data) -def _verify_flash(flasher, args, flash_params=None): +def verify_flash(esp, args): differences = False + for address, argfile in args.addr_filename: - image = argfile.read() + image = pad_to(argfile.read(), 4) argfile.seek(0) # rewind in case we need it again - if address == 0 and image[0] == '\xe9' and flash_params is not None: - image = image[0:2] + flash_params + image[4:] + + image = _update_image_flash_params(esp, address, args, image) + image_size = len(image) - print 'Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s...' % (image_size, image_size, address, argfile.name) + print('Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s...' % (image_size, image_size, address, argfile.name)) # Try digest first, only read if there are differences. - digest, _ = flasher.flash_digest(address, image_size) - digest = hexify(digest).upper() - expected_digest = hashlib.md5(image).hexdigest().upper() + digest = esp.flash_md5sum(address, image_size) + expected_digest = hashlib.md5(image).hexdigest() if digest == expected_digest: - print '-- verify OK (digest matched)' + print('-- verify OK (digest matched)') continue else: differences = True if getattr(args, 'diff', 'no') != 'yes': - print '-- verify FAILED (digest mismatch)' + print('-- verify FAILED (digest mismatch)') continue - flash = flasher.flash_read(address, image_size) + flash = esp.read_flash(address, image_size) assert flash != image - diff = [i for i in xrange(image_size) if flash[i] != image[i]] - print '-- verify FAILED: %d differences, first @ 0x%08x' % (len(diff), address + diff[0]) + diff = [i for i in range(image_size) if flash[i] != image[i]] + print('-- verify FAILED: %d differences, first @ 0x%08x' % (len(diff), address + diff[0])) for d in diff: - print ' %08x %02x %02x' % (address + d, ord(flash[d]), ord(image[d])) + flash_byte = flash[d] + image_byte = image[d] + if PYTHON2: + flash_byte = ord(flash_byte) + image_byte = ord(image_byte) + print(' %08x %02x %02x' % (address + d, flash_byte, image_byte)) if differences: raise FatalError("Verify failed.") -def verify_flash(esp, args, flash_params=None): - flasher = CesantaFlasher(esp) - _verify_flash(flasher, args, flash_params) +def read_flash_status(esp, args): + print('Status value: 0x%04x' % esp.read_status(args.bytes)) + + +def write_flash_status(esp, args): + fmt = "0x%%0%dx" % (args.bytes * 2) + args.value = args.value & ((1 << (args.bytes * 8)) - 1) + print(('Initial flash status: ' + fmt) % esp.read_status(args.bytes)) + print(('Setting flash status: ' + fmt) % args.value) + esp.write_status(args.value, args.bytes, args.non_volatile) + print(('After flash status: ' + fmt) % esp.read_status(args.bytes)) + + +def get_security_info(esp, args): + (flags, flash_crypt_cnt, key_purposes) = esp.get_security_info() + # TODO: better display + print('Flags: 0x%08x (%s)' % (flags, bin(flags))) + print('Flash_Crypt_Cnt: 0x%x' % flash_crypt_cnt) + print('Key_Purposes: %s' % (key_purposes,)) def version(args): - print __version__ + print(__version__) # # End of operations functions # -def main(): +def main(custom_commandline=None): + """ + Main function for esptool + + custom_commandline - Optional override for default arguments parsing (that uses sys.argv), can be a list of custom arguments + as strings. Arguments and their values need to be added as individual items to the list e.g. "-b 115200" thus + becomes ['-b', '115200']. + """ parser = argparse.ArgumentParser(description='esptool.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='esptool') + parser.add_argument('--chip', '-c', + help='Target chip type', + type=lambda c: c.lower().replace('-', ''), # support ESP32-S2, etc. + choices=['auto', 'esp8266', 'esp32', 'esp32s2', 'esp32s3beta2', 'esp32c3'], + default=os.environ.get('ESPTOOL_CHIP', 'auto')) + parser.add_argument( '--port', '-p', help='Serial port device', - default=os.environ.get('ESPTOOL_PORT', '/dev/ttyUSB0')) + default=os.environ.get('ESPTOOL_PORT', None)) parser.add_argument( '--baud', '-b', help='Serial port baud rate used when flashing/reading', type=arg_auto_int, - default=os.environ.get('ESPTOOL_BAUD', ESPROM.ESP_ROM_BAUD)) + default=os.environ.get('ESPTOOL_BAUD', ESPLoader.ESP_ROM_BAUD)) + + parser.add_argument( + '--before', + help='What to do before connecting to the chip', + choices=['default_reset', 'no_reset', 'no_reset_no_sync'], + default=os.environ.get('ESPTOOL_BEFORE', 'default_reset')) + + parser.add_argument( + '--after', '-a', + help='What to do after esptool.py is finished', + choices=['hard_reset', 'soft_reset', 'no_reset'], + default=os.environ.get('ESPTOOL_AFTER', 'hard_reset')) + + parser.add_argument( + '--no-stub', + help="Disable launching the flasher stub, only talk to ROM bootloader. Some features will not be available.", + action='store_true') + + parser.add_argument( + '--trace', '-t', + help="Enable trace-level output of esptool.py interactions.", + action='store_true') + + parser.add_argument( + '--override-vddsdio', + help="Override ESP32 VDDSDIO internal voltage regulator (use with care)", + choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES, + nargs='?') + + parser.add_argument( + '--connect-attempts', + help=('Number of attempts to connect, negative or 0 for infinite. ' + 'Default: %d.' % DEFAULT_CONNECT_ATTEMPTS), + type=int, + default=os.environ.get('ESPTOOL_CONNECT_ATTEMPTS', DEFAULT_CONNECT_ATTEMPTS)) subparsers = parser.add_subparsers( dest='operation', help='Run esptool {command} -h for additional help') + def add_spi_connection_arg(parent): + parent.add_argument('--spi-connection', '-sc', help='ESP32-only argument. Override default SPI Flash connection. ' + 'Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers to use for SPI flash (CLK,Q,D,HD,CS).', + action=SpiConnectionAction) + parser_load_ram = subparsers.add_parser( 'load_ram', help='Download an image to RAM and execute') @@ -1043,26 +3388,56 @@ def main(): parser_write_mem.add_argument('value', help='Value', type=arg_auto_int) parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int) - def add_spi_flash_subparsers(parent): + def add_spi_flash_subparsers(parent, is_elf2image): """ Add common parser arguments for SPI flash properties """ + extra_keep_args = [] if is_elf2image else ['keep'] + auto_detect = not is_elf2image + + if auto_detect: + extra_fs_message = ", detect, or keep" + else: + extra_fs_message = "" + parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency', - choices=['40m', '26m', '20m', '80m'], - default=os.environ.get('ESPTOOL_FF', '40m')) + choices=extra_keep_args + ['40m', '26m', '20m', '80m'], + default=os.environ.get('ESPTOOL_FF', '40m' if is_elf2image else 'keep')) parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode', - choices=['qio', 'qout', 'dio', 'dout'], - default=os.environ.get('ESPTOOL_FM', 'qio')) - parent.add_argument('--flash_size', '-fs', help='SPI Flash size in Mbit', type=str.lower, - choices=['4m', '2m', '8m', '16m', '32m', '16m-c1', '32m-c1', '32m-c2', '64m', '128m'], - default=os.environ.get('ESPTOOL_FS', '4m')) + choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'], + default=os.environ.get('ESPTOOL_FM', 'qio' if is_elf2image else 'keep')) + parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16M)' + ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)' + extra_fs_message, + action=FlashSizeAction, auto_detect=auto_detect, + default=os.environ.get('ESPTOOL_FS', '1MB' if is_elf2image else 'keep')) + add_spi_connection_arg(parent) parser_write_flash = subparsers.add_parser( 'write_flash', help='Write a binary blob to flash') + parser_write_flash.add_argument('addr_filename', metavar='
', help='Address followed by binary filename, separated by space', action=AddrFilenamePairAction) - add_spi_flash_subparsers(parser_write_flash) + parser_write_flash.add_argument('--erase-all', '-e', + help='Erase all regions of flash (not just write areas) before programming', + action="store_true") + + add_spi_flash_subparsers(parser_write_flash, is_elf2image=False) parser_write_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") - parser_write_flash.add_argument('--verify', help='Verify just-written data (only necessary if very cautious, data is already CRCed', action='store_true') + parser_write_flash.add_argument('--verify', help='Verify just-written data on flash ' + '(mostly superfluous, data is read back during flashing)', action='store_true') + parser_write_flash.add_argument('--encrypt', help='Apply flash encryption when writing data (required correct efuse settings)', + action='store_true') + # In order to not break backward compatibility, our list of encrypted files to flash is a new parameter + parser_write_flash.add_argument('--encrypt-files', metavar='
', + help='Files to be encrypted on the flash. Address followed by binary filename, separated by space.', + action=AddrFilenamePairAction) + parser_write_flash.add_argument('--ignore-flash-encryption-efuse-setting', help='Ignore flash encryption efuse settings ', + action='store_true') + + compress_args = parser_write_flash.add_mutually_exclusive_group(required=False) + compress_args.add_argument('--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)', + action="store_true", default=None) + compress_args.add_argument('--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)', + action="store_true") subparsers.add_parser( 'run', @@ -1086,8 +3461,17 @@ def main(): help='Create an application image from ELF file') parser_elf2image.add_argument('input', help='Input ELF file') parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) - parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1','2'], default='1') - add_spi_flash_subparsers(parser_elf2image) + parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2'], default='1') + parser_elf2image.add_argument('--min-rev', '-r', help='Minimum chip revision', choices=['0', '1', '2', '3'], default='0') + parser_elf2image.add_argument('--secure-pad', action='store_true', + help='Pad image so once signed it will end on a 64KB boundary. For Secure Boot v1 images only.') + parser_elf2image.add_argument('--secure-pad-v2', action='store_true', + help='Pad image to 64KB, so once signed its signature sector will start at the next 64K block. ' + 'For Secure Boot v2 images only.') + parser_elf2image.add_argument('--elf-sha256-offset', help='If set, insert SHA256 hash (32 bytes) of the input ELF file at specified offset in the binary.', + type=arg_auto_int, default=None) + + add_spi_flash_subparsers(parser_elf2image, is_elf2image=True) subparsers.add_parser( 'read_mac', @@ -1097,13 +3481,31 @@ def main(): 'chip_id', help='Read Chip ID from OTP ROM') - subparsers.add_parser( + parser_flash_id = subparsers.add_parser( 'flash_id', help='Read SPI flash manufacturer and device ID') + add_spi_connection_arg(parser_flash_id) + + parser_read_status = subparsers.add_parser( + 'read_flash_status', + help='Read SPI flash status register') + + add_spi_connection_arg(parser_read_status) + parser_read_status.add_argument('--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1, 2, 3], default=2) + + parser_write_status = subparsers.add_parser( + 'write_flash_status', + help='Write SPI flash status register') + + add_spi_connection_arg(parser_write_status) + parser_write_status.add_argument('--non-volatile', help='Write non-volatile bits (use with caution)', action='store_true') + parser_write_status.add_argument('--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1, 2, 3], default=2) + parser_write_status.add_argument('value', help='New value', type=arg_auto_int) parser_read_flash = subparsers.add_parser( 'read_flash', help='Read SPI flash content') + add_spi_connection_arg(parser_read_flash) parser_read_flash.add_argument('address', help='Start address', type=arg_auto_int) parser_read_flash.add_argument('size', help='Size of region to dump', type=arg_auto_int) parser_read_flash.add_argument('filename', help='Name of binary dump') @@ -1116,36 +3518,252 @@ def main(): action=AddrFilenamePairAction) parser_verify_flash.add_argument('--diff', '-d', help='Show differences', choices=['no', 'yes'], default='no') + add_spi_flash_subparsers(parser_verify_flash, is_elf2image=False) - subparsers.add_parser( + parser_erase_flash = subparsers.add_parser( 'erase_flash', help='Perform Chip Erase on SPI flash') + add_spi_connection_arg(parser_erase_flash) + + parser_erase_region = subparsers.add_parser( + 'erase_region', + help='Erase a region of the flash') + add_spi_connection_arg(parser_erase_region) + parser_erase_region.add_argument('address', help='Start address (must be multiple of 4096)', type=arg_auto_int) + parser_erase_region.add_argument('size', help='Size of region to erase (must be multiple of 4096)', type=arg_auto_int) subparsers.add_parser( 'version', help='Print esptool version') + subparsers.add_parser('get_security_info', help='Get some security-related data') + # internal sanity check - every operation matches a module function of the same name for operation in subparsers.choices.keys(): assert operation in globals(), "%s should be a module function" % operation - args = parser.parse_args() + expand_file_arguments() - print 'esptool.py v%s' % __version__ + args = parser.parse_args(custom_commandline) + print('esptool.py v%s' % __version__) # operation function can take 1 arg (args), 2 args (esp, arg) - # or be a member function of the ESPROM class. + # or be a member function of the ESPLoader class. + + if args.operation is None: + parser.print_help() + sys.exit(1) + + # Forbid the usage of both --encrypt, which means encrypt all the given files, + # and --encrypt-files, which represents the list of files to encrypt. + # The reason is that allowing both at the same time increases the chances of + # having contradictory lists (e.g. one file not available in one of list). + if args.operation == "write_flash" and args.encrypt and args.encrypt_files is not None: + raise FatalError("Options --encrypt and --encrypt-files must not be specified at the same time.") operation_func = globals()[args.operation] - operation_args,_,_,_ = inspect.getargspec(operation_func) - if operation_args[0] == 'esp': # operation function takes an ESPROM connection object - initial_baud = min(ESPROM.ESP_ROM_BAUD, args.baud) # don't sync faster than the default baud rate - esp = ESPROM(args.port, initial_baud) - esp.connect() - operation_func(esp, args) + + if PYTHON2: + # This function is depreciated in Python3 + operation_args = inspect.getargspec(operation_func).args + else: + operation_args = inspect.getfullargspec(operation_func).args + + if operation_args[0] == 'esp': # operation function takes an ESPLoader connection object + if args.before != "no_reset_no_sync": + initial_baud = min(ESPLoader.ESP_ROM_BAUD, args.baud) # don't sync faster than the default baud rate + else: + initial_baud = args.baud + + if args.port is None: + if list_ports is None: + raise FatalError("Listing all serial ports is currently not available on this operating system version. " + "Specify the port when running esptool.py") + ser_list = sorted(ports.device for ports in list_ports.comports()) + print("Found %d serial ports" % len(ser_list)) + else: + ser_list = [args.port] + esp = None + for each_port in reversed(ser_list): + print("Serial port %s" % each_port) + try: + if args.chip == 'auto': + esp = ESPLoader.detect_chip(each_port, initial_baud, args.before, args.trace, + args.connect_attempts) + else: + chip_class = { + 'esp8266': ESP8266ROM, + 'esp32': ESP32ROM, + 'esp32s2': ESP32S2ROM, + 'esp32s3beta2': ESP32S3BETA2ROM, + 'esp32c3': ESP32C3ROM, + }[args.chip] + esp = chip_class(each_port, initial_baud, args.trace) + esp.connect(args.before, args.connect_attempts) + break + except (FatalError, OSError) as err: + if args.port is not None: + raise + print("%s failed to connect: %s" % (each_port, err)) + esp = None + if esp is None: + raise FatalError("Could not connect to an Espressif device on any of the %d available serial ports." % len(ser_list)) + + if esp.secure_download_mode: + print("Chip is %s in Secure Download Mode" % esp.CHIP_NAME) + else: + print("Chip is %s" % (esp.get_chip_description())) + print("Features: %s" % ", ".join(esp.get_chip_features())) + print("Crystal is %dMHz" % esp.get_crystal_freq()) + read_mac(esp, args) + + if not args.no_stub: + if esp.secure_download_mode: + print("WARNING: Stub loader is not supported in Secure Download Mode, setting --no-stub") + args.no_stub = True + else: + esp = esp.run_stub() + + if args.override_vddsdio: + esp.override_vddsdio(args.override_vddsdio) + + if args.baud > initial_baud: + try: + esp.change_baud(args.baud) + except NotImplementedInROMError: + print("WARNING: ROM doesn't support changing baud rate. Keeping initial baud rate %d" % initial_baud) + + # override common SPI flash parameter stuff if configured to do so + if hasattr(args, "spi_connection") and args.spi_connection is not None: + if esp.CHIP_NAME != "ESP32": + raise FatalError("Chip %s does not support --spi-connection option." % esp.CHIP_NAME) + print("Configuring SPI flash mode...") + esp.flash_spi_attach(args.spi_connection) + elif args.no_stub: + print("Enabling default SPI flash mode...") + # ROM loader doesn't enable flash unless we explicitly do it + esp.flash_spi_attach(0) + + if hasattr(args, "flash_size"): + print("Configuring flash size...") + detect_flash_size(esp, args) + if args.flash_size != 'keep': # TODO: should set this even with 'keep' + esp.flash_set_parameters(flash_size_bytes(args.flash_size)) + + try: + operation_func(esp, args) + finally: + try: # Clean up AddrFilenamePairAction files + for address, argfile in args.addr_filename: + argfile.close() + except AttributeError: + pass + + # Handle post-operation behaviour (reset or other) + if operation_func == load_ram: + # the ESP is now running the loaded image, so let it run + print('Exiting immediately.') + elif args.after == 'hard_reset': + print('Hard resetting via RTS pin...') + esp.hard_reset() + elif args.after == 'soft_reset': + print('Soft resetting...') + # flash_finish will trigger a soft reset + esp.soft_reset(False) + else: + print('Staying in bootloader.') + if esp.IS_STUB: + esp.soft_reset(True) # exit stub back to ROM loader + + esp._port.close() + else: operation_func(args) +def expand_file_arguments(): + """ Any argument starting with "@" gets replaced with all values read from a text file. + Text file arguments can be split by newline or by space. + Values are added "as-is", as if they were specified in this order on the command line. + """ + new_args = [] + expanded = False + for arg in sys.argv: + if arg.startswith("@"): + expanded = True + with open(arg[1:], "r") as f: + for line in f.readlines(): + new_args += shlex.split(line) + else: + new_args.append(arg) + if expanded: + print("esptool.py %s" % (" ".join(new_args[1:]))) + sys.argv = new_args + + +class FlashSizeAction(argparse.Action): + """ Custom flash size parser class to support backwards compatibility with megabit size arguments. + + (At next major relase, remove deprecated sizes and this can become a 'normal' choices= argument again.) + """ + def __init__(self, option_strings, dest, nargs=1, auto_detect=False, **kwargs): + super(FlashSizeAction, self).__init__(option_strings, dest, nargs, **kwargs) + self._auto_detect = auto_detect + + def __call__(self, parser, namespace, values, option_string=None): + try: + value = { + '2m': '256KB', + '4m': '512KB', + '8m': '1MB', + '16m': '2MB', + '32m': '4MB', + '16m-c1': '2MB-c1', + '32m-c1': '4MB-c1', + }[values[0]] + print("WARNING: Flash size arguments in megabits like '%s' are deprecated." % (values[0])) + print("Please use the equivalent size '%s'." % (value)) + print("Megabit arguments may be removed in a future release.") + except KeyError: + value = values[0] + + known_sizes = dict(ESP8266ROM.FLASH_SIZES) + known_sizes.update(ESP32ROM.FLASH_SIZES) + if self._auto_detect: + known_sizes['detect'] = 'detect' + known_sizes['keep'] = 'keep' + if value not in known_sizes: + raise argparse.ArgumentError(self, '%s is not a known flash size. Known sizes: %s' % (value, ", ".join(known_sizes.keys()))) + setattr(namespace, self.dest, value) + + +class SpiConnectionAction(argparse.Action): + """ Custom action to parse 'spi connection' override. Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas. + """ + def __call__(self, parser, namespace, value, option_string=None): + if value.upper() == "SPI": + value = 0 + elif value.upper() == "HSPI": + value = 1 + elif "," in value: + values = value.split(",") + if len(values) != 5: + raise argparse.ArgumentError(self, '%s is not a valid list of comma-separate pin numbers. Must be 5 numbers - CLK,Q,D,HD,CS.' % value) + try: + values = tuple(int(v, 0) for v in values) + except ValueError: + raise argparse.ArgumentError(self, '%s is not a valid argument. All pins must be numeric values' % values) + if any([v for v in values if v > 33 or v < 0]): + raise argparse.ArgumentError(self, 'Pin numbers must be in the range 0-33.') + # encode the pin numbers as a 32-bit integer with packed 6-bit values, the same way ESP32 ROM takes them + # TODO: make this less ESP32 ROM specific somehow... + clk, q, d, hd, cs = values + value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk + else: + raise argparse.ArgumentError(self, '%s is not a valid spi-connection value. ' + 'Values are SPI, HSPI, or a sequence of 5 pin numbers CLK,Q,D,HD,CS).' % value) + setattr(namespace, self.dest, value) + + class AddrFilenamePairAction(argparse.Action): """ Custom parser class for the address/filename pairs passed as arguments """ def __init__(self, option_strings, dest, nargs='+', **kwargs): @@ -1154,83 +3772,247 @@ class AddrFilenamePairAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): # validate pair arguments pairs = [] - for i in range(0,len(values),2): + for i in range(0, len(values), 2): try: - address = int(values[i],0) - except ValueError as e: - raise argparse.ArgumentError(self,'Address "%s" must be a number' % values[i]) + address = int(values[i], 0) + except ValueError: + raise argparse.ArgumentError(self, 'Address "%s" must be a number' % values[i]) try: argfile = open(values[i + 1], 'rb') except IOError as e: raise argparse.ArgumentError(self, e) except IndexError: - raise argparse.ArgumentError(self,'Must be pairs of an address and the binary filename to write there') + raise argparse.ArgumentError(self, 'Must be pairs of an address and the binary filename to write there') pairs.append((address, argfile)) + + # Sort the addresses and check for overlapping + end = 0 + for address, argfile in sorted(pairs, key=lambda x: x[0]): + argfile.seek(0, 2) # seek to end + size = argfile.tell() + argfile.seek(0) + sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) + sector_end = ((address + size + ESPLoader.FLASH_SECTOR_SIZE - 1) & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) - 1 + if sector_start < end: + message = 'Detected overlap at address: 0x%x for file: %s' % (address, argfile.name) + raise argparse.ArgumentError(self, message) + end = sector_end setattr(namespace, self.dest, pairs) -# This is "wrapped" stub_flasher.c, to be loaded using run_stub. -_CESANTA_FLASHER_STUB = """\ -{"code_start": 1074790404, "code": "080000601C000060000000601000006031FCFF71FCFF\ -81FCFFC02000680332D218C020004807404074DCC48608005823C0200098081BA5A9239245005803\ -1B555903582337350129230B446604DFC6F3FF21EEFFC0200069020DF0000000010078480040004A\ -0040B449004012C1F0C921D911E901DD0209312020B4ED033C2C56C2073020B43C3C56420701F5FF\ -C000003C4C569206CD0EEADD860300202C4101F1FFC0000056A204C2DCF0C02DC0CC6CCAE2D1EAFF\ -0606002030F456D3FD86FBFF00002020F501E8FFC00000EC82D0CCC0C02EC0C73DEB2ADC46030020\ -2C4101E1FFC00000DC42C2DCF0C02DC056BCFEC602003C5C8601003C6C4600003C7C08312D0CD811\ -C821E80112C1100DF0000C180000140010400C0000607418000064180000801800008C1800008418\ -0000881800009018000018980040880F0040A80F0040349800404C4A0040740F0040800F0040980F\ -00400099004012C1E091F5FFC961CD0221EFFFE941F9310971D9519011C01A223902E2D1180C0222\ -6E1D21E4FF31E9FF2AF11A332D0F42630001EAFFC00000C030B43C2256A31621E1FF1A2228022030\ -B43C3256B31501ADFFC00000DD023C4256ED1431D6FF4D010C52D90E192E126E0101DDFFC0000021\ -D2FF32A101C020004802303420C0200039022C0201D7FFC00000463300000031CDFF1A333803D023\ -C03199FF27B31ADC7F31CBFF1A3328030198FFC0000056C20E2193FF2ADD060E000031C6FF1A3328\ -030191FFC0000056820DD2DD10460800000021BEFF1A2228029CE231BCFFC020F51A33290331BBFF\ -C02C411A332903C0F0F4222E1D22D204273D9332A3FFC02000280E27B3F721ABFF381E1A2242A400\ -01B5FFC00000381E2D0C42A40001B3FFC0000056120801B2FFC00000C02000280EC2DC0422D2FCC0\ -2000290E01ADFFC00000222E1D22D204226E1D281E22D204E7B204291E860000126E012198FF32A0\ -042A21C54C003198FF222E1D1A33380337B202C6D6FF2C02019FFFC000002191FF318CFF1A223A31\ -019CFFC00000218DFF1C031A22C549000C02060300003C528601003C624600003C72918BFF9A1108\ -71C861D851E841F83112C1200DF00010000068100000581000007010000074100000781000007C10\ -0000801000001C4B0040803C004091FDFF12C1E061F7FFC961E941F9310971D9519011C01A662906\ -21F3FFC2D1101A22390231F2FF0C0F1A33590331EAFFF26C1AED045C2247B3028636002D0C016DFF\ -C0000021E5FF41EAFF2A611A4469040622000021E4FF1A222802F0D2C0D7BE01DD0E31E0FF4D0D1A\ -3328033D0101E2FFC00000561209D03D2010212001DFFFC000004D0D2D0C3D01015DFFC0000041D5\ -FFDAFF1A444804D0648041D2FF1A4462640061D1FF106680622600673F1331D0FF10338028030C43\ -853A002642164613000041CAFF222C1A1A444804202FC047328006F6FF222C1A273F3861C2FF222C\ -1A1A6668066732B921BDFF3D0C1022800148FFC0000021BAFF1C031A2201BFFFC000000C02460300\ -5C3206020000005C424600005C5291B7FF9A110871C861D851E841F83112C1200DF0B0100000C010\ -0000D010000012C1E091FEFFC961D951E9410971F931CD039011C0ED02DD0431A1FF9C1422A06247\ -B302062D0021F4FF1A22490286010021F1FF1A223902219CFF2AF12D0F011FFFC00000461C0022D1\ -10011CFFC0000021E9FFFD0C1A222802C7B20621E6FF1A22F8022D0E3D014D0F0195FFC000008C52\ -22A063C6180000218BFF3D01102280F04F200111FFC00000AC7D22D1103D014D0F010DFFC0000021\ -D6FF32D110102280010EFFC0000021D3FF1C031A220185FFC00000FAEEF0CCC056ACF821CDFF317A\ -FF1A223A310105FFC0000021C9FF1C031A22017CFFC000002D0C91C8FF9A110871C861D851E841F8\ -3112C1200DF0000200600000001040020060FFFFFF0012C1E00C02290131FAFF21FAFF026107C961\ -C02000226300C02000C80320CC10564CFF21F5FFC02000380221F4FF20231029010C432D010163FF\ -C0000008712D0CC86112C1200DF00080FE3F8449004012C1D0C9A109B17CFC22C1110C13C51C0026\ -1202463000220111C24110B68202462B0031F5FF3022A02802A002002D011C03851A0066820A2801\ -32210105A6FF0607003C12C60500000010212032A01085180066A20F2221003811482105B3FF2241\ -10861A004C1206FDFF2D011C03C5160066B20E280138114821583185CFFF06F7FF005C1286F5FF00\ -10212032A01085140066A20D2221003811482105E1FF06EFFF0022A06146EDFF45F0FFC6EBFF0000\ -01D2FFC0000006E9FF000C022241100C1322C110C50F00220111060600000022C1100C13C50E0022\ -011132C2FA303074B6230206C8FF08B1C8A112C1300DF0000000000010404F484149007519031027\ -000000110040A8100040BC0F0040583F0040CC2E00401CE20040D83900408000004021F4FF12C1E0\ -C961C80221F2FF097129010C02D951C91101F4FFC0000001F3FFC00000AC2C22A3E801F2FFC00000\ -21EAFFC031412A233D0C01EFFFC000003D0222A00001EDFFC00000C1E4FF2D0C01E8FFC000002D01\ -32A004450400C5E7FFDD022D0C01E3FFC00000666D1F4B2131DCFF4600004B22C0200048023794F5\ -31D9FFC0200039023DF08601000001DCFFC000000871C861D85112C1200DF000000012C1F0026103\ -01EAFEC00000083112C1100DF000643B004012C1D0E98109B1C9A1D991F97129013911E2A0C001FA\ -FFC00000CD02E792F40C0DE2A0C0F2A0DB860D00000001F4FFC00000204220E71240F7921C226102\ -01EFFFC0000052A0DC482157120952A0DD571205460500004D0C3801DA234242001BDD3811379DC5\ -C6000000000C0DC2A0C001E3FFC00000C792F608B12D0DC8A1D891E881F87112C1300DF00000", "\ -entry": 1074792180, "num_params": 1, "params_start": 1074790400, "data": "FE0510\ -401A0610403B0610405A0610407A061040820610408C0610408C061040", "data_start": 10736\ -43520} -""" -if __name__ == '__main__': +# Binary stub code (see flasher_stub dir for source & details) +ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +eNqNWntz2zYS/yo063fTG4CkSNDTTiTXle2kd7XTVnE6urkjQbLpTOqxHfWsuMl99sO+CJBS2/uDNgmCi93F7m8f0O8Hq3a9OjiJ6oPlWhl3qeW6y54v19oGD3DTP1TZct3W7qGBaf5NPoPbHXdfuatbrq2KYASo\ +Ju5dVw6GD92fLIpWy3XplmoT95i7a+JXUwq+mtBXRrv/+YCCYwVoO3aMIe4rGFOOZKu8OKqOO2DBjRZuKtDIgA5wqgcES5qmGzeqAqlNxKJ3JhTVcQ7fNyOmHDOOA5hp1O7igt7izOr/mTleHS6ton4notGe4GWE\ +oxbUZUW8mkgqS9rwC7OkyFUdKLgccVgmb+nGj6CqFx82RXEUP7rRBKSJVRTR1mwTR6kp8dsKs26e25ey8qy0TaA4O2arHAk05Gr7mnxpf2+U/9oqtmggIBdOzKKReSM3Sczy5V+D1YIkxkvSVvS2moiCzSntA8yC\ +/zq7FkMs2JBrEwNPKXmVtek1qROJWrH0eOrmar3nxtNg5xTfg1hIIRgc7nvq3jTJ4M0rO9jlG5i1+I3YnqYwXp6a+MXXl/FQuaUKFKfMlO9MoFtcNgufp1O5u6Bh/KbMelJiy7UWpUbo1k7HFW9KKTqeDJGhDO57\ +MCh5i01ox3VyFPgwa7JkSx7MLAESjBe4VLRx2u2rLYlry2P9Rza54S8cl2UdYlZyOXapYAE0/cpzI4uJSvG+AA+Y8+TMS94yVlZwz9aI64euYhF06uBTpAcmCTQBFpT6SATgjXYEWj2Hj0I/Cm3qNtRruVz1ZIbj\ +t8OvVsR0EzBqyDlWA+WwIodOffUFxAuGEfenxh28ya5Syx6YghHfgKP99P3Vcjkj6KevnfO0jCnGnDmF5bwD6GG7JDj6bkL/RVUhSoHP6gxkTAHs6iRiW2TfCmOQsScxWaTNjn44hA9P4iP4d5iBqpyDjbHSDIEe\ +neaOAmVXPb/YRflhfkyaqNi8HLcNY2fVEN6ZAEM9V1/BhiAUJaSOVnZAk01WiTd6GNeCILplPsTjEm934kqjIFfhXRSOJ7+IJ+4w62CT3SamhYqs1GcMcOMgDcggkK0EQoAM7CVymhAbJN4FTADh7EzCXBKGaRzR\ +RwY5zGDX4+NUfTnjaJoc3ZQXbCNowF+ANg1os5Kdnoy5fEZM9OEPlMBzFe9Wwu4I01raAnjf1KySeotKZI5lE0+HtPFboWmYTvEndBqek23O2YysJMmJpG+Jf4fmw8+6jhmeao54wE2X/VG6I/c34YMDpQYxfwqo\ +8Td2AECufhjCJMjrHvKdHeKh0Wy6QQo28M3cY9u1Dx81Os/V146ibTiayw4FUSak1NljP7lm59tgIR1/+CKmGLuwlE2iH7C71sHXgMRVxbDfbtlAGC+DfKGWb/a8lZJ55MNESCm0WfvHG+zNw7Ic1v6VebwLN+9t\ ++HAXPqzCh3X4ABr9mcGvUb3zwHpv2Y12Kp+1hhmsrrpLklMjqtVejei/2bPl7RsgdNrxlK17eu1rBJS5Eeo/QlSavHYbZNjK84L11NC6OH+Lu/rk9j7YLmTy6RpZmu8FM3Hbpu9J75oRWuodsrG7tdhoAYmSxCG7\ +NQ65j6p7GuxLg8krDG1P9wwgNqglMKVw+1Uj7UbQHzd/z5dntLxw9G8IqcxGbcZsPMVPZwWhasNiNqj5q9V2MU0B4bggvUpUULSVtyDx/B2T0aE64c2hV6W89IwsONdqKR+qmFMErvwdkalrQPnf2RwLTjtB/Pyb\ +5eonUAx88VJw7kcuMVPvv5C04hoFJQ/WdN8AC6/2YQnQBBS9yWtSCXABnlyijThN1qBJMFz0fRam7Lb4/8SXTUjBbMMC2tdWUGgim/kMA5imLfwzx8Zc1z7/7mJ2SdxyMwByRsCUGnA44+/hQflWApYI2fNRhbWl\ +PANDNYOQMB3Uj+NkFFki4O0fnM4OAgpZ0MMQIxIWAjGksmQitS9xPr7lpVtGpRvJKafvjjEamYSDknb4QnfW0t239A8SzAmTAQQtSZw1RTNFCZCLXjc92n1LkRzQzn2iuQht2t5/b+MKgYyzG/VHAQK+QrjRFDDx\ +8z4QnELUVC/jAlCj2KO9pBVekktYHXF6iclDKyACi4HFtztyM5MC8/izcW+l7h3RzQZUBjpoIHj9dOkjbLNh4jNB4Fdk4MB52E1yJnwV+xzziBN6/d/tKNyYLSqy+UaXKqGtqityJZtNoQyGkIkQ2xf0EfXGIDtV\ +7HLbcYeCSs3yNfoFLxkMIh5ZUg+k2pUOEW97/HXc1zaE9eMUUiDYc7DtJvl8IBYk0fnDhrSpiNOPQlBKT2cg8ykHO42TDnBQP1wvV2+u96mGhyCgbfFIFCBWoI0hg/J1+sA32Cs6Axp3Z1GHNxdxvy6k6Nnl9XyY\ +q2i7e3oNGRtAl/QxgGOwJrJT4LSl4rzr/nzRe/IaF+LuABrOCaV1EuY7QCrzoQEG23ZPBvcoinXd3JusUg8DRaHjlQVdyEBOEK7UY/mP7mdJIYhnNJD8Mep4ss3vZfAcVNkhrCXz16yUfB9uunMcFHzpXvuVKoS+\ +edybEDbQQn7A6vHe8YNLPDKxCvFtThFAQYE1EKH0JOsBybmgFidhpWzeaGGVn9Kqxx2pnrj5TqYvV8gQAwRkdxDhoO0zVCeX9iCH7g2F1h0u97q3ovNw+Ne+FkC/lbYM1kPJWFeJKEqH0xRN0cVuMKhlUBNzKhtG\ +rn5iOpboJCjEYUI2nnDBWRrq40HsGPYs9buii0VzCICx2lZLrwitcCvbkLF9atoht0iFtrHESH4KhpQ8wN/0Drw+8r2qOluQKQBwQbCoMAW+O6Nwjm2mihM5E8RfuKASN/rtRpZ2AtH3geMO1A4VR3kHgQfA/X1Q\ +dPZ9sgW1dLp27rVe8dVh23CjFlwcU4Fq9Evppj5KwfOC82Ba+9FQT8gG9fZIjputcpSbciwot3WMVowcBDuyB7jZn8LNnn8/2ivIG0Emlf8QkFCaXxGJR2m3xMRzhTcJAWmt+4SgL4a3kVEQMahtioESbSG7AiKR\ +cJ6ckwnQ/Fte1kDQhg43GRSbBzVqfyM7tPkvgeUCPtbZswtIbPQz1mZo7LRQJQuhAa8Tv9TdikAIUKrl9miFAWNGEA82Cklyq980p+Bin3/YJdIlGEj6CXWRL5d9p/iBIzJjkKO2CvcM0Cd/AX/Wb4B0fMpRAtK9\ +knvFYSFdqif3J5W4Ls4OSaAZt1MqqibgwmzeUAsax/FMRJ0fc8dSOi8TCX7cQYa5Wk/hJjr1QUfEURQW2YyzfkrBERPbGeos6oXhnBMibomddgSTs/DorgkQOxFPHOjrknHUZDJsFhwIsWuH5NZg2PlXYRpAzcie\ +d4YpRNtMkJrDTouZpgHfA9uwkCzX2B/DIxdNLjrGR0f6trfO+QBU989kBdkdIFQ3/kRCqa2dpqBHObZmNXCzzzEDitdJnw598His6CATq5vuTEK12eYjGIB3A7wg/mWp+Q5vVluIP7xaxxzZsxk3AVpc+xNLl/vT\ +YTxGSHwWDzvQ4FlWb/lUyWJ3DuNXCqHyN7i5o2X0hiZyOMgTRTaYBcurEg8wnEbPv/x2SmO6txuEWOBTGtsbbvREVliaX14+4uq3zF73BN2NOyqGrToviCcEuZJT8JIcr+XDR2zyVINmhJO/I9TWxYxT9HbN/oev\ +exN/E0TXakZSde0doaJJ3/SCkGru5GA7qQCEcJLU6ckODLUU0ffkxR5pt+vAmQr2VspJBdg7zNKn5+gybEcf0KYanxYWKN5+5rO+jss2NISud8eDR64AcNp7GAfKiFUoe/BeYXoDeSWCRBuAQpMgV9/JY4aP7+fc\ +iWuH+RN5MeBSi7qOKfRYPvWoub0zKJIq5r8MaqxhuQc9K1funk3gRUyYWOKNinYjwlvAYVxc9gc6RZZr11qP12XcT/lgred49FuFnKvqQMwG4zSwImfOPoSbeI9b6Vh+1P7LWmJAehrWPcQmxoC6jmgadimZQctn\ +kdXkwXcIGtyjvyikngDsQ6/HMwd1xudxkOvAIR3EW82NMLCGUs5gwuODMqn4/GPyTzm82elPRA64G8/NXUVNswNKo8BLW+zZ/bjlyI4+BultJtKzxNhV27JtGNC5QY7Hd6/h+K54gcd3xWExBZnVZUHeUteMVqQq\ +Dnq9th7Flh4huNLSD+SfnUAIQGsup5/6ETsrTxxJ6dD6oaIQ0GIPQmH/Jz/cFzQBGbGUwuLzxCdAFXsGIFij9xb+HNPqnVvO2Uv6EKwUqpdK/8rdSz7XsFJiW0o28N7QsRG1bv6zqUSLobQ2+7DcvWRdpFL0Gzwh\ +hreScdd0bFjB6azhxiXMBDgBp6VMO6IDZ37E1lnkKWKDraIKDk/BRMUtlWvggXiWKL8mCE7hRGdWdDbhXr6wblFtR/gLHv0V7M+hIZSvCk7uOs03nPk1+RG3ncHw4LQB+t6VlZaUYRdCQysub+bBTyjsl1x+MSxL\ +7wx4t5jzzaCl24idmqMf5tQEF0QwWoUTyqMbbhbQhMN4wdmjaAED8PdbgEw/5wCg/7751uir8eD8U79IwjE1oYrTr/PyrwDzm80JeN93GdjshmdQ/S+1xDr75Jt7XfRjCsrLLChSTsQxcSh8aFfoe+lHSgbwbcvl\ +l8YkeT8m2cyoJtw4i1VHn8m66LOxEO4gjaM14/5M98D/3IlYw9lf8MnItvPeRn6GJaLlg09jz8pQVwfPIvw54L/er6oH+FGgVkU2SZwSM/emvV09fOgHdVbmbrCpVlXw60Humh/wm5BQmqvJJMs+/Q8JS3S9\ +"""))) +ESP32S2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32S3BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32C3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +eNrFWmt727YV/iuOndhttvUBxCu8xJFaybLsOE37dPXch1lDgqSXrtMWR2mcrf7vw3suIiVbSr7tg2wJAIFzfc8F/O/BorlZHBzuVAfFjbHFjQ2fKgvf8TGvT4sbn4dvUXFTuuImp9H9MFi+CH/S78OfOAyl4X+z\ +G/54eTqmp4ubtr7MaI9n4Y95HvaPFmEU021xXdw0JvwaDKvxXjggf8g0VINZcVMPxl/PdsOzJinDwYPwCWvzfBj+RMVBMccJ2O992CGh/WiVy27DaDigCTRbF760YcYH4qs2Kw6Irt/Pwro6rK/42bbNsg0TevSI\ +RUOchk9dZ8Qp7wfiU5HZYCm88Amic+HjI/z/5lZoyS/A4xDET7pzTPifuxGL4P5D3bNbOZpPGPZo0JO639Z/AwXw7uG8la1V26S0bAwJBRW5iLcOSg1qcv5MdUsqJ0W+YFqq8LTPzrMH4V8gvLYZ2GHN89M2PwnL\ +6mkwiTqsaMQsrB9jmGUMm2qi0Yz4WAhdRhaBGOjPizTLhGV+02YiMdMx20B6bVFABHs7mbDXl4D1oCciIz6AZD3bdjjrKBzk9h6K+ZE9igGKMa7YoEmW9BqRoxevsVFP+R4SHIW/tmL6MJpbFkWeTDGNRx/A9GFC\ +ho0Ay1p3cYRF/VMhSThgIwfk7hgjgb1aRtr6gvkFLRAftEJb2o62ysDxQDFtFrGNgb62waZT9ZaeOYRHwEcqrmbE2Gy3RDlubXc6TKem04gwOmrA1tD6u1uAsH/ywVU2IYsUB8CKPJmQxMkXvnvmp8IZbDGb9uwX\ +XKnJ5FHPMwXHaqzGjx50fI0lI15vhXKSncFMiplB0JAbyNZ5CqDZE7tU9vubO3vVkUPHx2C5EfUNhEgCRtXM0tDxbFXiCIHeHgsEr6nwu5lz+i24QUfbINYGP4ygFTMidlfrD7DIRz3gmWCJrDoj2ofq1tFEtqz7\ +WzZCcC32+Cm8IYjB16mCTh9v+tgCMwM2iCmWMNuYwcSTHr4cywy81u3Du7HDJVs2A8yJ/0vYpsMb/wq8QjUIG08JaE+G+yLFhp3RiXSBG8AU8mSzE77UAgSJMjUkfyhobTFk+YTHC6KxGAsUxRgcjjmMfgp4bJ28\ +Z//QSL0SeL2NBGN98+xXDY/YY8zU3k89jzVeMMkmDAbVYMqstGTAO0esh9b+uR9hpmqQw3vOqxmTGWnh4IPJZiLIZIA7YKIUqkgiAoQ9UYw0T1HrN4kaTRqsL2866+v5xYLJg5H6PooRRl7dsseTICMmDzMwibw+\ +ZGuDPeE/gh+APMfiao8nGAVHGdOGE1gYLxkY7wvAExF1uxboV8MInNdIBBnororjm8QJR6ijbrWn1R0RVqCFn7uSTSCewXqoINzOuxCx9UjbHcnQD0FU2Uh9P4abD5TVx+wClTh3RWrxTJ2L+ZhA8y6ygcF4XX7I\ +noysolgQTzJ2xSp7/xboYW/fYeoNTk72RbOBhbphRmskJwCuUijKbmElUHMXmBYsjFwAWZEIhLflERPKSLPLlqqAtNnU99c3N8kv3QmEDOU0sFs5TpGDffZCLX1AqBNJMykbSHRC4urjvdOg0mR3G8CBiD1Z4WG1\ +efJM7NckvhNYVQ4fQ1XluJgH8GrTy/YnCPin2dJ1AajOv8MiVdybUwZZKCJPXnyKEDYyGEJTrzy5lYXXHAERYZxT8H04+tjL2xJQkzZ9hCVkFSNVs9ZMh7H3K/bYjYEBth5wj2h7kAHeW8781zYDuN3rkyUGzNu3\ +t+9+BNj8BMT7Gyy1EhijuLsLtN2Hy59CPgiaLYqzGjxHv3Ci1Yjll2IqqrYVYgZfcWpeQV1VyiAH3PGDTWwOmfQ8uQpLG/uBd8yTWm0015Jwuxt9wgYpvqS3/9m8J5m4P2TUJrv2dxeHg6/5EcJ2TbPs7Ehqv2Q9\ +c8vWXJMyFCvZZQ2V4pzWhOTVVOFb1rLnGgi4zDV92JeE3mUv2GzLT5sth0IKKGAHVbdzfvGGDaVSnHYjz8lMnvVn/jBihedEX/RBymsaWaHhx+00MLq/Yc/7XPkgLHoWe5sjXs4QSc/ZonOnM+22o1HgDm4lMYMj\ +JDH/aEp+rLGp/7azXoSqZvBUCvfNhNIgaf17TuTnnMYToQlAWeuYZfqLloKmv9ZOeRC7IWmhhsb/F6Nf44kOoy+Tdb/eImNatArkLx//AHv7oZhfgu7ZL8CV8vT0DJNnj59j8nkxPwfKvzrvJalVdjGaXb7tJI/8\ +EUyGcHEkriCQXSICx1I6DBica8NrvERoLxGa5kqmHSYN0G0GnDejrAE6wSi8mfOCjYhcVpo55s+yD2vlk2ZdmgFhrIIZN7IFClhnJ39HdkmpYL+QWy3CHlwdkXHtSA6txVuSD7XHhm/80JRzLGOPtdxV3S37ZzaZ\ +0F4SOLelYW3ZtfCKYnrXFCraVTknxjJKMwumfhsSwP+2YPRcxMs9Kf+z8FOyDkW482X/TnoPIBDuRaKDGVoaTPgLd04mW0y4pALoKjsTttAFsWsdGWTRedKOWlVE18ChXCbQuUCMpU5U/Y/suap1i6RLqQ5dpeGH\ +bXbZfciE75z5Q0Cm8bgbD3UXF9BzKZ8JGS1rCoshFMuJ7gJLqUaT+TJaXY9PnX5JzaJU6qD0nH72ivL1/GNbukWVj518wQJ0VKgQUXHHps8eilXadhcSf8Wl39Kv0HmwEqQaGficSua1NLJWyphtsdL1SiRKnZtP\ +cGc/Vxo7vLikik37ddpugVCy99LZ7EvH9KVTSneldlIZayBBIdpIAHLU/PTfHEGRlwIxTvWuesZBVbtuHwm1BKk3+9hfAZxZAtZ1E2Y5kRfXNCxtrRBLS5lJiwXNUOW+S+lRF7Koa9JqAy2W3nuS6ojjkRJR7XMN\ +jQyplFDbO2eVvadkx1iWPfG/Ale/IiF6DjENXNY6+kkt5xfFtQpYYA45cK6ZxB3rtYK8HK9EXwn3sNBoRRZNeGSfSJKJeGyP5cA6gwu75FjOlMYaENtbalhOPLdDKCB40XWdn+w9VGBHndUIUKKjhWS9TLpO746X\ +W5NUgnqtp0kjGBN59OiUuhEAQek7mmTHaxa8qtHXTEsp4axkW6FWCeLl8gAvB0DSJHNqCGfMUe2RE+TPEdJPwPsJgHCMK5vxFtzOqbxyAHpu+lC19Qi2fU5kONU/UJWYjkfHvouzwWh/lQlkH3ekQf1UEGsqIbZc\ +M+dlDWbZozxLu5F+ZjybdupsMjXLqUYK2ERd9dDeSRODk7mdPfZAHK8t6DJGa88+EsSi5uoOtdhzMRlrPEP8Us3SgTXciodYaoFRnbLLbo2TXiyaeqvM0xbJ5LGa+Eya/dq2TGV7UvWRDg6XG2eycbkiAhJ2S9Y8\ +6spY4XLpm73Mqif13E47FghZQLWv5YlGEfSHvruONXLP+sGG52qxtMGdGbmwadZAuJF6BKJueq1mKxSWtXSmkvG5dJ7FCny2nhD8HsVqyN9JIyHiHXLh2Rvq/V6xP6tEHGVMdoT9HxwC5usRulauRl8SxNW3WkjU\ +Qx5qcslbKnJc6Uluoq0xH2mbOVcajTh7FX14JNdeODUbvemO8XMp1Mztx270IwBr62Gv0QHArZs75cNa7QFufmbnN3mGpf9wJJdqrUBnI1YqODqWSFHLbaDpwObjocboO6uTbrXzJ17cOdWs7UTAqkYnKNdri+XT\ +4kJNs3y6pqfnolTdQPr6lPagKnGGS+GFXr7E0mGR68Aqk7uadpkyHPMANRkSqfGtNLZrjv3ku8GVudXAcWgqI0RuI+SWwtUyf75HYddP+6KOuhjVxMsYNe7usvlsO2bqUPPT2bkQQvfzFKYJB5u0S/lc9CHrSnVE\ +FPBGHhl9oc9IGRSQQayupYpJpaRN4iZXVEFnokTgrDmEPpSOvGEMvSNga15oysawTmCBsN2KzZWrsnSdNPrMdelNrumNlcsyOwlUzCWg0/8uxLHdLpXIFr/DG+DOhHtHvQilu7dy97Ay6UzvSTvrMiRo0SvuxxON\ +YjJXNdsSVatG3E+EephJeX3NY/zywuyeCBis41ryY4TxyI93kK75v/L7AVqd/RsMyC1K43rmIQl25bvOCHNbMkX8uETOSqzB63gXteT1A1ypEpfx3dwPcSR+xZFvM7y/6l6FYSZHlJm8ozcHdtkDSP1USVYr4d4Q\ +GFqpo1lUCwqQP3YXkSpChl3/kmx2zu0HSmXgpO17zkhp70T3ztOuc6Bqu5+T4SXlLatXlSL/1vUJmUnMKuW6G0JfXaGC4Bh6MKJC9sttp+Nz8acgnVBjNL0CIXnbE64qyIw3YtZs5UUlINRZdzvefzHDkmAupGlr\ +B1KpwYz1PRQ1a309hCnh4BrJRWbWNdHuEelpHwuONvNuzLFUDv7+TJk1/bPcO1teyIPfsiZyqy9J/KbXn2o1KRv5lqYUu2LeHnF7bWlvFI/w6lTYcM5tOG6Uh0xMu/hxd4JJTuQWvuUs5vrkazTlDqUjJ+bqYa7p\ +7nRwIi90sAkFEy2kmyCJn7ZMrH4igV8krptkWRqAzuCCBHAgIRtW4zVcIEBTulQLz9RkHVwgQlAgpXJDLwsSKXhN78UEoYsuBmq8MqAQTi9pELo4BuYmlVY2M/8vWKR0CZy6Y8IBUuLzQmJjJJcsGUtHz2bUX/FL\ +PK8RuZHXo/Qxszm+g6cnd71tc6qYb/S6J3L1HkvSQ2hnOBKVZisVw8PP9JHcaNETS1hDvyN9uY+WdIpCAG2RNp21aEqnp8dw/PRsHwVo+pz6Xbhme9X0X9qiz8Efd+iFx5/fLcprvPZoTZbF1uaxCTPNfHH9cTkY\ +RXEaButyUer7kbCr4FMHMtzfxdg0dia+/R/imbus\ +"""))) + + +def _main(): try: main() except FatalError as e: - print '\nA fatal error occurred: %s' % e + print('\nA fatal error occurred: %s' % e) sys.exit(2) + + +if __name__ == '__main__': + _main() diff --git a/os/flash.sh b/os/flash.sh index 552208d..a4f1f0b 100755 --- a/os/flash.sh +++ b/os/flash.sh @@ -1,22 +1,15 @@ #!/bin/bash if [ $# -ne 1 ]; then - echo "One parameter required: the device of the serial interface" - echo "$0 " - echo "e.g.:" - echo "$0 ttyUSB0" + DEVICE=$1 +# check the serial connection +if [ ! -c $DEVICE ]; then + echo "$DEVICE does not exist" exit 1 fi -DEVICE=$1 -#BAUD="--baud 57600" -#BAUD="--baud 921600" - -# check the serial connection - -if [ ! -c /dev/$DEVICE ]; then - echo "/dev/$DEVICE does not exist" - exit 1 +else + print "Autodetect serial port" fi if [ ! -f esptool.py ]; then @@ -25,12 +18,16 @@ if [ ! -f esptool.py ]; then exit 1 fi -./esptool.py --port /dev/$DEVICE $BAUD read_mac +CMD="python3 esptool.py " +if [ $# -eq 1 ]; then +CMD="python3 esptool.py --port $DEVICE " +fi + +$CMD read_mac if [ $? -ne 0 ]; then echo "Error reading the MAC -> set the device into the bootloader!" exit 1 fi -echo "Flashing the new" -#./esptool.py --port /dev/$DEVICE $BAUD write_flash -fm dio 0x00000 nodemcu2.bin -./esptool.py --port /dev/$DEVICE $BAUD write_flash -fm dio 0x00000 0x00000.bin 0x10000 0x10000.bin 0x3fc000 esp_init_data_default.bin 0x07e000 blank.bin 0x3fe000 blank.bin +echo "Flashing the new firmware" +$CMD write_flash -fm dio 0x00000 0x00000.bin 0x10000 0x10000.bin 0x3fc000 esp_init_data_default.bin diff --git a/os/nodemcu2.bin b/os/nodemcu2.bin deleted file mode 100644 index 1644bdf..0000000 Binary files a/os/nodemcu2.bin and /dev/null differ diff --git a/simulation/.classpath b/simulation/.classpath index 33968ad..6c08763 100644 --- a/simulation/.classpath +++ b/simulation/.classpath @@ -1,8 +1,13 @@ - + + + + + + diff --git a/simulation/Readme.md b/simulation/Readme.md index 1c1d3ff..b1a7388 100644 --- a/simulation/Readme.md +++ b/simulation/Readme.md @@ -6,7 +6,7 @@ The simualation should be started with the following arguments at this position: # Use it without Eclipse Compiling: - `javac -d bin/ -cp libs/luaj-jme-3.0.1.jar:libs/luaj-jse-3.0.1.jar $(find src -name '*.java')` + `javac -d bin/ -cp libs/luaj-jme-3.0.1.jar:libs/luaj-jse-3.0.1.jar:libs/org.eclipse.paho.client.mqttv3-1.2.5.jar $(find src -name '*.java')` Running: - `java -cp libs/luaj-jme-3.0.1.jar:libs/luaj-jse-3.0.1.jar:bin de.c3ma.ollo.WS2812Simulation ../init.lua ws28128ClockLayout.txt config.lua` + `java -cp libs/luaj-jme-3.0.1.jar:libs/luaj-jse-3.0.1.jar:libs/org.eclipse.paho.client.mqttv3-1.2.5.jar:bin de.c3ma.ollo.WS2812Simulation ../init.lua ws28128ClockLayout.txt config.lua` diff --git a/simulation/config.lua b/simulation/config.lua index bb7c61c..280870d 100644 --- a/simulation/config.lua +++ b/simulation/config.lua @@ -1,15 +1,22 @@ -green=0 -green2=128 -red=128 -blue=0 +green2=200 +red=200 +blue=200 -color=string.char(0, 0, 128) -color1=string.char(128, 0, 0) -color2=string.char(tonumber(green2*0.8), 0, 0) -color3=string.char(tonumber(green2*0.4), 0, 0) -color4=string.char(tonumber(green2*0.2), 0, 0) +color=string.char(0, 0, blue) +color1=string.char(red, 0, 0) +color2=string.char(tonumber(red*0.9), 0, 0) +color3=string.char(tonumber(red*0.8), 0, 0) +color4=string.char(tonumber(red*0.7), 0, 0) colorBg=string.char(0,0,0) -- black is the default background color sntpserverhostname="ptbtime1.ptb.de" timezoneoffset=1 dim="on" +mqttServer="192.168.1.1" +mqttPrefix="test" + +if (file.open("simulation.config.lua")) then + dofile("simulation.config.lua") +else + print("Default configuration, used") +end diff --git a/simulation/libs/Readme.md b/simulation/libs/Readme.md index c76c4af..52fa41b 100644 --- a/simulation/libs/Readme.md +++ b/simulation/libs/Readme.md @@ -1,7 +1,13 @@ # Dependencies +## Lua The following file is expected here: `luaj-3.0.1.zip` It can be downloaded here: https://sourceforge.net/projects/luaj/files/latest/download + +## MQTT + +It can be downloaded here: +https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.eclipse.paho%22%20AND%20a%3A%22org.eclipse.paho.client.mqttv3%22 \ No newline at end of file diff --git a/simulation/libs/org.eclipse.paho.client.mqttv3-1.2.5.jar b/simulation/libs/org.eclipse.paho.client.mqttv3-1.2.5.jar new file mode 100644 index 0000000..66f1278 Binary files /dev/null and b/simulation/libs/org.eclipse.paho.client.mqttv3-1.2.5.jar differ diff --git a/simulation/src/de/c3ma/ollo/LuaThreadTmr.java b/simulation/src/de/c3ma/ollo/LuaThreadTmr.java index 0a958fd..20d61e8 100644 --- a/simulation/src/de/c3ma/ollo/LuaThreadTmr.java +++ b/simulation/src/de/c3ma/ollo/LuaThreadTmr.java @@ -18,7 +18,7 @@ public class LuaThreadTmr extends Thread { private LuaValue code; - private final int delay; + private int delay; private final int timerNumber; @@ -28,7 +28,7 @@ public class LuaThreadTmr extends Thread { this.delay = delay; this.timerNumber = timerNumber; } - + @Override public void run() { try { diff --git a/simulation/src/de/c3ma/ollo/WS2812Simulation.java b/simulation/src/de/c3ma/ollo/WS2812Simulation.java index 147a20e..b7c6ede 100644 --- a/simulation/src/de/c3ma/ollo/WS2812Simulation.java +++ b/simulation/src/de/c3ma/ollo/WS2812Simulation.java @@ -14,6 +14,8 @@ import de.c3ma.ollo.mockup.DoFileFunction; import de.c3ma.ollo.mockup.ESP8266Adc; import de.c3ma.ollo.mockup.ESP8266File; import de.c3ma.ollo.mockup.ESP8266GPIO; +import de.c3ma.ollo.mockup.ESP8266Gpio; +import de.c3ma.ollo.mockup.ESP8266Mqtt; import de.c3ma.ollo.mockup.ESP8266Net; import de.c3ma.ollo.mockup.ESP8266Node; import de.c3ma.ollo.mockup.ESP8266Time; @@ -38,6 +40,8 @@ public class WS2812Simulation implements LuaSimulation { private ESP8266Node espNode = new ESP8266Node(this); private DoFileFunction doFile = new DoFileFunction(globals); private ESP8266Ws2812 ws2812 = new ESP8266Ws2812(); + private ESP8266Gpio gpio = new ESP8266Gpio(); + private ESP8266Mqtt mqtt = new ESP8266Mqtt(); private ESP8266Adc adc = new ESP8266Adc(); private String scriptName; @@ -49,6 +53,8 @@ public class WS2812Simulation implements LuaSimulation { globals.load(espNode); globals.load(new ESP8266GPIO()); globals.load(adc); + globals.load(gpio); + globals.load(mqtt); globals.load(new ESP8266Wifi()); globals.load(new ESP8266Net()); globals.load(new ESP8266Time()); diff --git a/simulation/src/de/c3ma/ollo/mockup/DoFileFunction.java b/simulation/src/de/c3ma/ollo/mockup/DoFileFunction.java index a7b5660..c88d3ed 100644 --- a/simulation/src/de/c3ma/ollo/mockup/DoFileFunction.java +++ b/simulation/src/de/c3ma/ollo/mockup/DoFileFunction.java @@ -26,15 +26,19 @@ public class DoFileFunction extends OneArgFunction { public LuaValue call(LuaValue luaFilename) { String filename = luaFilename.checkjstring(); - //System.out.println("[Nodemcu] dofile " + filename); - File f = new File(workingDir.getAbsolutePath() + File.separator + filename); - - if (f.exists()) { - LuaValue chunk = this.globals.loadfile(f.getAbsolutePath()); - return chunk.call(); - } else { - return LuaValue.valueOf(false); + try { + if (f.exists()) { + LuaValue chunk = this.globals.loadfile(f.getAbsolutePath()); + chunk.call(); + return LuaValue.valueOf(true); + } else { + return LuaValue.valueOf(false); + } + } catch (Exception e) { + System.err.println("Cannot load " + f.getName()); + e.printStackTrace(); + return LuaValue.valueOf(false); } } diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Gpio.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Gpio.java new file mode 100644 index 0000000..93ee00c --- /dev/null +++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Gpio.java @@ -0,0 +1,90 @@ +package de.c3ma.ollo.mockup; + +import java.util.HashMap; + +import javax.swing.SwingUtilities; + +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.Varargs; +import org.luaj.vm2.lib.OneArgFunction; +import org.luaj.vm2.lib.TwoArgFunction; +import org.luaj.vm2.lib.VarArgFunction; + +/** + * created at 18.03.2021 - 21:09:03
+ * creator: ollo
+ * project: Esp8266 GPIO Emulation
+ * $Id: $
+ * @author ollo
+ */ +public class ESP8266Gpio extends TwoArgFunction { + + private static final String DIRECTION_INPUT = "input"; + private HashMap mInputs = new HashMap(); + + @Override + public LuaValue call(LuaValue modname, LuaValue env) { + env.checkglobals(); + final LuaTable gpio = new LuaTable(); + gpio.set("mode", new Mode(this)); + gpio.set("read", new Read(this)); + gpio.set("INPUT", DIRECTION_INPUT); + env.set("gpio", gpio); + env.get("package").get("loaded").set("gpio", gpio); + return gpio; + } + + private class Mode extends VarArgFunction { + + private ESP8266Gpio gpio; + + public Mode(ESP8266Gpio a) { + this.gpio = a; + } + + public Varargs invoke(Varargs varargs) { + if (varargs.narg() == 2) { + final int pin = varargs.arg(1).toint(); + final LuaString lsDirection = varargs.arg(2).checkstring(); + String direction = lsDirection.toString(); + if (direction.equals(DIRECTION_INPUT)) { + gpio.mInputs.put(pin, -1); + } + System.out.println("[GPIO] PIN" + pin +" as " + direction); + return LuaValue.valueOf(true); + } else { + return LuaValue.NIL; + } + } + } + + private class Read extends OneArgFunction { + + private ESP8266Gpio gpio; + + public Read(ESP8266Gpio a) { + this.gpio = a; + } + + @Override + public LuaValue call(LuaValue arg) { + int pin = arg.toint(); + if (mInputs.containsKey(pin)) { + return LuaValue.valueOf(mInputs.get(pin)); + } else { + System.out.println("[GPIO] pin" + pin + " not defined (gpio.mode missing)"); + return LuaValue.NIL; + } + } + } + + public void setPin(int pin, int newValue) { + if (mInputs.containsKey(pin)) { + mInputs.put(pin, newValue); + } else { + System.out.println("[GPIO] PIN" + pin +" not defined (missing gpio.mode)"); + } + } +} diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Mqtt.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Mqtt.java new file mode 100644 index 0000000..194bada --- /dev/null +++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Mqtt.java @@ -0,0 +1,208 @@ +package de.c3ma.ollo.mockup; + +import java.util.UUID; + +import org.eclipse.paho.client.mqttv3.IMqttClient; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.MqttPersistenceException; +import org.eclipse.paho.client.mqttv3.MqttSecurityException; +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.Varargs; +import org.luaj.vm2.lib.TwoArgFunction; +import org.luaj.vm2.lib.VarArgFunction; + +/** + * + * @author ollo + * + */ +public class ESP8266Mqtt extends TwoArgFunction implements IMqttMessageListener { + + private static final String ON_PREFIX = "on_"; + private static final String MESSAGE = "message"; + private IMqttClient mMqttClient = null; + final LuaTable onMqtt = new LuaTable(); + + @Override + public LuaValue call(LuaValue modname, LuaValue env) { + env.checkglobals(); + final LuaTable mqtt = new LuaTable(); + mqtt.set("Client", new LuaMqttClient()); + env.set("mqtt", mqtt); + env.get("package").get("loaded").set("tmr", mqtt); + System.out.println("[MQTT] Modlue loaded"); + return mqtt; + } + + private class LuaMqttClient extends VarArgFunction { + public LuaValue invoke(Varargs varargs) { + final LuaTable dynMqtt = new LuaTable(); + if (varargs.narg() == 2) { + final String client = varargs.arg(1).toString().toString(); + final int timeout = varargs.arg(2).toint(); + dynMqtt.set("on", new OnMqtt(client, timeout)); + dynMqtt.set("publish", new PublishMqtt()); + dynMqtt.set("subscribe", new SubscribeMqtt()); + dynMqtt.set("connect", new ConnectMqtt()); + System.out.println("[MQTT] New client: " + client + "(" + timeout+ "s)"); + } + return dynMqtt; + } + } + + private class OnMqtt extends VarArgFunction { + + private String client=null; + private int timeout = 0; + + private OnMqtt(String client, int timeout) { + this.client = client; + this.timeout = timeout; + } + + public LuaValue invoke(Varargs varargs) { + + if (varargs.narg() == 3) { + final LuaTable table = varargs.arg(1).checktable(); + final String callback = varargs.arg(2).toString().toString(); + final LuaValue code = varargs.arg(3); + System.out.println("[MQTT] On " + this.client + " " + callback); + onMqtt.set(ON_PREFIX + callback, code); + } else { + for(int i=0; i <= varargs.narg(); i++) { + System.err.println("[MQTT] On ["+(i) + "] (" + varargs.arg(i).typename() + ") " + varargs.arg(i).toString() ); + } + return LuaValue.NIL; + } + return onMqtt; + } + } + + private class PublishMqtt extends VarArgFunction { + + public LuaValue invoke(Varargs varargs) { + final LuaTable onMqtt = new LuaTable(); + if (varargs.narg() == 5) { + final String topic = varargs.arg(2).toString().toString(); + final String message = varargs.arg(3).toString().toString(); + final String qos = varargs.arg(4).toString().toString(); + final String retain = varargs.arg(4).toString().toString(); + if ( !mMqttClient.isConnected()) { + return LuaValue.NIL; + } + MqttMessage msg = new MqttMessage(message.getBytes()); + if (qos.equals("0")) { + msg.setQos(0); + } + + msg.setRetained(!retain.contentEquals("0")); + try { + mMqttClient.publish(topic,msg); + System.out.println("[MQTT] publish " + topic); + } catch (MqttPersistenceException e) { + System.err.println("[MQTT] publish " + topic + " failed : " + e.getMessage()); + } catch (MqttException e) { + System.err.println("[MQTT] publish " + topic + " failed : " + e.getMessage()); + } + } else { + for(int i=0; i <= varargs.narg(); i++) { + System.err.println("[MQTT] publish ["+(i) + "] (" + varargs.arg(i).typename() + ") " + varargs.arg(i).toString() ); + } + return LuaValue.NIL; + } + return onMqtt; + } + } + + private class SubscribeMqtt extends VarArgFunction { + + public LuaValue invoke(Varargs varargs) { + final LuaTable onMqtt = new LuaTable(); + final int numberArg = varargs.narg(); + if (numberArg == 3) { + final String topic = varargs.arg(2).toString().toString(); + final int qos = varargs.arg(3).tonumber().toint(); + + try { + if (mMqttClient != null) { + mMqttClient.subscribe(topic, ESP8266Mqtt.this); + System.out.println("[MQTT] subscribe " + topic + " (QoS " + qos + ")"); + } else { + throw new Exception("Client not instantiated"); + } + } catch (MqttSecurityException e) { + System.err.println("[MQTT] subscribe " + topic + " (QoS " + qos + ") failed: " + e.getMessage()); + e.printStackTrace(); + } catch (MqttException e) { + System.err.println("[MQTT] subscribe " + topic + " (QoS " + qos + ") failed: " + e.getMessage()); + e.printStackTrace(); + } catch (Exception e) { + System.err.println("[MQTT] subscribe " + topic + " (QoS " + qos + ") failed: " + e.getMessage()); + } + } else { + for(int i=0; i <= numberArg; i++) { + System.err.println("[MQTT] subscribe ["+(i) + "/" + numberArg + "] (" + varargs.arg(i).typename() + ") " + varargs.arg(i).toString() ); + } + return LuaValue.NIL; + } + return onMqtt; + } + } + + private class ConnectMqtt extends VarArgFunction { + + public LuaValue invoke(Varargs varargs) { + final LuaTable onMqtt = new LuaTable(); + if ((varargs.narg() == 6) && (mMqttClient == null)) { + final LuaTable table = varargs.arg(1).checktable(); + final String targetIP = varargs.arg(2).toString().toString(); + final int portnumber = varargs.arg(3).toint(); + final boolean secureTLS = varargs.arg(4).toboolean(); + final LuaValue codeOnConnected = varargs.arg(5); + final LuaValue codeOnFailed = varargs.arg(6); + String publisherId = "LuaSim" + UUID.randomUUID().toString(); + try { + mMqttClient = new MqttClient("tcp://" + targetIP + ":" + portnumber,publisherId); + MqttConnectOptions options = new MqttConnectOptions(); + options.setAutomaticReconnect(false); + options.setCleanSession(true); + options.setConnectionTimeout(10); + mMqttClient.connect(options); + System.out.println("[MQTT] connected to " + targetIP + ":" + portnumber); + codeOnConnected.call(); + } catch (MqttException e) { + System.err.println("[MQTT] connect failed : " + e.getMessage()); + codeOnFailed.call(); + } + } else if (mMqttClient != null) { + System.err.println("[MQTT] client already exists : " + mMqttClient); + return LuaValue.NIL; + } else { + for(int i=0; i <= varargs.narg(); i++) { + System.err.println("[MQTT] connect ["+(i) + "] (" + varargs.arg(i).typename() + ") " + varargs.arg(i).toString() ); + } + return LuaValue.NIL; + } + return onMqtt; + } + } + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + LuaValue messageCallback = onMqtt.get(ON_PREFIX + MESSAGE); + if (messageCallback != null) { + LuaValue call2 = messageCallback.call(LuaValue.NIL, + LuaValue.valueOf(topic), + LuaValue.valueOf(message.getPayload())); + //FIXME call the LUA code + } else { + System.err.println("[MQTT] message "+ topic + " : " + message + " without callback"); + } + } +} diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Node.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Node.java index 1ff7824..efc1d86 100644 --- a/simulation/src/de/c3ma/ollo/mockup/ESP8266Node.java +++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Node.java @@ -34,6 +34,7 @@ public class ESP8266Node extends TwoArgFunction { final LuaTable node = new LuaTable(); node.set("compile", new CompileFunction()); node.set("restart", new RestartFunction()); + node.set("heap", new HeapFunction()); env.set("node", node); env.get("package").get("loaded").set("node", node); return node; @@ -73,6 +74,18 @@ public class ESP8266Node extends TwoArgFunction { } + private class HeapFunction extends ZeroArgFunction { + + @Override + public LuaValue call() { + System.out.println("[Node] Heap"); + return LuaValue.valueOf(Runtime.getRuntime().freeMemory()); + } + + } + + + public void setWorkingDirectory(File workingDir) { this.workingDir = workingDir; } diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Tmr.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Tmr.java index 4c1782a..116aedd 100644 --- a/simulation/src/de/c3ma/ollo/mockup/ESP8266Tmr.java +++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Tmr.java @@ -6,6 +6,7 @@ import org.luaj.vm2.Varargs; import org.luaj.vm2.lib.OneArgFunction; import org.luaj.vm2.lib.TwoArgFunction; import org.luaj.vm2.lib.VarArgFunction; +import org.luaj.vm2.lib.ZeroArgFunction; import de.c3ma.ollo.LuaThreadTmr; @@ -18,9 +19,11 @@ import de.c3ma.ollo.LuaThreadTmr; */ public class ESP8266Tmr extends TwoArgFunction { - private static final int MAXTHREADS = 7; + private static final int MAXTHREADS = 10; private static LuaThreadTmr[] allThreads = new LuaThreadTmr[MAXTHREADS]; + private static LuaThreadTmr[] dynamicThreads = new LuaThreadTmr[MAXTHREADS]; + private static int dynamicThreadCounter=0; public static int gTimingFactor = 1; @@ -30,6 +33,10 @@ public class ESP8266Tmr extends TwoArgFunction { final LuaTable tmr = new LuaTable(); tmr.set("stop", new stop()); tmr.set("alarm", new alarm()); + tmr.set("create", new create()); + tmr.set("wdclr", new watchDog()); + tmr.set("ALARM_AUTO", "ALARM_AUTO"); + tmr.set("ALARM_SINGLE", "ALARM_SINGLE"); env.set("tmr", tmr); env.get("package").get("loaded").set("tmr", tmr); @@ -37,6 +44,9 @@ public class ESP8266Tmr extends TwoArgFunction { for (Thread t : allThreads) { t = null; } + for (Thread t : dynamicThreads) { + t = null; + } return tmr; } @@ -87,6 +97,79 @@ public class ESP8266Tmr extends TwoArgFunction { } } + private class dynRegister extends VarArgFunction { + private final int dynIndex; + public dynRegister(int index) { + this.dynIndex = index; + } + public Varargs invoke(Varargs varargs) { + if (varargs.narg() == 4) { + final String endlessloop = varargs.arg(3).toString().toString(); + final int delay = varargs.arg(2).toint(); + final LuaValue code = varargs.arg(4); + dynamicThreads[dynIndex] = new LuaThreadTmr(dynIndex, code, (endlessloop.contains("AUTO")), Math.max(delay / gTimingFactor, 1)); + System.out.println("[TMR] DynTimer" + dynIndex + " registered"); + } + return LuaValue.valueOf(true); + } + } + + private class dynStart extends ZeroArgFunction { + private final int dynIndex; + public dynStart(int index) { + this.dynIndex = index; + } + public LuaValue call() { + if (dynamicThreads[dynIndex] != null) { + dynamicThreads[dynIndex].start(); + System.out.println("[TMR] DynTimer" + dynIndex + " started"); + return LuaValue.valueOf(true); + } else { + return LuaValue.valueOf(false); + } + } + } + + private class watchDog extends ZeroArgFunction { + + public LuaValue call() { + System.out.println("[TMR] Watchdog fed"); + return LuaValue.valueOf(true); + + } + } + + private class dynStop extends ZeroArgFunction { + private final int dynIndex; + public dynStop(int index) { + this.dynIndex = index; + } + public LuaValue call() { + boolean status = false; + if (dynamicThreads[dynIndex] != null) { + dynamicThreads[dynIndex].stopThread(); + dynamicThreads[dynIndex] = null; + System.out.println("[TMR] DynTimer" + dynIndex + " stopped"); + status = true; + } + return LuaValue.valueOf(status); + } + } + + private class create extends ZeroArgFunction { + public LuaValue call() { + if (dynamicThreadCounter >= MAXTHREADS) { + return LuaValue.error("[TMR] DynTimer" + dynamicThreadCounter + " exeeded maximum"); + } + final LuaTable dynTimer = new LuaTable(); + dynTimer.set("register", new dynRegister(dynamicThreadCounter)); + dynTimer.set("start", new dynStart(dynamicThreadCounter)); + dynTimer.set("unregister", new dynStop(dynamicThreadCounter)); + dynamicThreadCounter++; + return dynTimer; + } + } + public void stopAllTimer() { for (int i = 0; i < allThreads.length; i++) { stopTmr(i); diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Ws2812.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Ws2812.java index ee5e04e..38da090 100644 --- a/simulation/src/de/c3ma/ollo/mockup/ESP8266Ws2812.java +++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Ws2812.java @@ -1,14 +1,18 @@ package de.c3ma.ollo.mockup; +import java.awt.Color; import java.io.File; +import java.util.ArrayList; import javax.swing.SwingUtilities; import org.luaj.vm2.LuaString; import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaValue; +import org.luaj.vm2.Varargs; import org.luaj.vm2.lib.OneArgFunction; import org.luaj.vm2.lib.TwoArgFunction; +import org.luaj.vm2.lib.VarArgFunction; import org.luaj.vm2.lib.ZeroArgFunction; import de.c3ma.ollo.LuaSimulation; @@ -32,6 +36,7 @@ public class ESP8266Ws2812 extends TwoArgFunction { final LuaTable ws2812 = new LuaTable(); ws2812.set("init", new init()); ws2812.set("write", new write()); + ws2812.set("newBuffer", new newBuffer()); env.set("ws2812", ws2812); env.get("package").get("loaded").set("ws2812", ws2812); return ws2812; @@ -54,6 +59,12 @@ public class ESP8266Ws2812 extends TwoArgFunction { if (arg.isstring()) { LuaString jstring = arg.checkstring(); final int length = jstring.rawlen(); + + if (ESP8266Ws2812.layout == null) { + System.err.println("[WS2812] Not initialized (" + length + "bytes to be updated)"); + return LuaValue.valueOf(false); + } + if ((length % 3) == 0) { final byte[] array = jstring.m_bytes; SwingUtilities.invokeLater(new Runnable() { @@ -70,13 +81,11 @@ public class ESP8266Ws2812 extends TwoArgFunction { } }); } - - if (ESP8266Ws2812.layout == null) { - System.out.println("[WS2812] write length:" + length); - } else { - } + return LuaValue.valueOf(true); + } else { + System.out.println("[WS2812] write no string given"); + return LuaValue.NIL; } - return LuaValue.valueOf(true); } } @@ -85,4 +94,175 @@ public class ESP8266Ws2812 extends TwoArgFunction { ESP8266Ws2812.layout = WS2812Layout.parse(file, nodemcuSimu); } } + + private class newBuffer extends VarArgFunction { + + public Varargs invoke(Varargs varargs) { + if (varargs.narg() == 2) { + final int leds = varargs.arg(1).toint(); + final int bytesPerLeds = varargs.arg(2).toint(); + final LuaTable rgbBuffer = new LuaTable(); + ArrayList ledList = new ArrayList(); + for(int i=0; i < leds; i++) { + ledList.add(new Color(0,0,0)); + } + rgbBuffer.set("fill", new bufferFill(ledList)); + rgbBuffer.set("set", new bufferWrite(ledList)); + rgbBuffer.set("get", new bufferRead(ledList)); + System.out.println("[WS2812] " + leds + "leds (" + bytesPerLeds + "bytes per led)"); + return rgbBuffer; + } else { + return LuaValue.NIL; + } + } + } + + private class bufferFill extends VarArgFunction { + + private ArrayList ledList = null; + + public bufferFill(ArrayList ledList) { + this.ledList = ledList; + } + + public Varargs invoke(Varargs varargs) { + if (varargs.narg() == 4) { + /* first argument is the object itself */ + final int red = varargs.arg(2).toint(); + final int green = varargs.arg(3).toint(); + final int blue = varargs.arg(4).toint(); + /* update local buffer */ + for(int i=0; i < ledList.size(); i++) { + ledList.set(i, new Color(red, green, blue)); + } + /* Update GUI */ + if (ESP8266Ws2812.layout != null) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + ESP8266Ws2812.layout.fillLEDs(red, green, blue); + } + }); + } + System.out.println("[WS2812] buffer fill with " + red + "," + green + "," + blue); + return LuaValue.valueOf(true); + } else if (varargs.isstring(2)) { + final LuaString color = varargs.arg(2).checkstring(); + + final int length = color.rawlen(); + if ((length == 3) && (ESP8266Ws2812.layout != null)) { + + final byte[] array = color.m_bytes; + final int r = array[0]+(Byte.MIN_VALUE*-1); + final int b = array[1]+(Byte.MIN_VALUE*-1); + final int g = array[2]+(Byte.MIN_VALUE*-1); + /* update local buffer */ + for(int i=0; i < ledList.size(); i++) { + ledList.set(i, new Color(r, g, b)); + } + /* Update GUI */ + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + ESP8266Ws2812.layout.fillLEDs(r, g, b); + } + }); + //System.out.println("[WS2812] buffer fill with " + r + "," + g + "," + b); + return LuaValue.valueOf(true); + } else { + System.err.println("[WS2812] buffer not initialized ("+varargs.narg() +"args) , length "+ length + ", raw:" + color.toString()); + return LuaValue.NIL; + } + } else { + System.err.println("[WS2812] fill with " + varargs.narg() + " arguments undefined."); + return LuaValue.NIL; + } + } + } + + private class bufferWrite extends VarArgFunction { + private ArrayList ledList = null; + + public bufferWrite(ArrayList ledList) { + this.ledList = ledList; + } + + public Varargs invoke(Varargs varargs) { + if (varargs.narg() == 3) { + final int index = varargs.arg(2).toint(); + final LuaString color = varargs.arg(3).checkstring(); + final int length = color.rawlen(); + if (length == 3) { + final byte[] array = color.m_bytes; + final int r = array[0]+(Byte.MIN_VALUE*-1); + final int b = array[1]+(Byte.MIN_VALUE*-1); + final int g = array[2]+(Byte.MIN_VALUE*-1); + // update buffer + ledList.set(index - 1, new Color(r, g, b)); + + // update GUI + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + ESP8266Ws2812.layout.updateLED(index - 1, r, g, b); + } + }); + return LuaValue.valueOf(true); + } else { + System.err.println("[WS2812] set with " + varargs.narg() + " arguments at index="+ index + " and "+ length + " charactes not matching"); + return LuaValue.NIL; + } + } else if (varargs.narg() == 5) { + final int index = varargs.arg(2).toint(); + final int green = varargs.arg(3).toint(); + final int red = varargs.arg(4).toint(); + final int blue = varargs.arg(5).toint(); + // update buffer + ledList.set(index - 1, new Color(red, green, blue)); + + // update GUI + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + ESP8266Ws2812.layout.updateLED(index - 1, red, green, blue); + } + }); + return LuaValue.valueOf(true); + } else { + for(int i=0; i <= varargs.narg(); i++) { + System.err.println("[WS2812] write ["+(i) + "] (" + varargs.arg(i).typename() + ") " + varargs.arg(i).toString() ); + } + System.err.println("[WS2812] set with " + varargs.narg() + " arguments undefined."); + return LuaValue.NIL; + } + } + } + + private class bufferRead extends VarArgFunction { + private ArrayList ledList = null; + + public bufferRead(ArrayList ledList) { + this.ledList = ledList; + } + + @Override + public Varargs invoke(Varargs varargs) { + final int offset = varargs.arg(2).toint(); + + if (ledList != null) { + // receiver from buffer + Color color = ledList.get(offset - 1); + final char[] array = new char[3]; + array[0] = (char) (color.getRed() ); + array[1] = (char) (color.getGreen() ); + array[2] = (char) (color.getBlue() ); + +// System.err.println("[WS2812] reading " + offset + ":" + ((int)array[0]) +"," + ((int) array[1]) + "," + ((int) array[2]) + " from " + color); + return LuaString.valueOf(array); + } + + System.err.println("[WS2812] reading " + offset + " impossible"); + return LuaValue.NIL; + } + } } diff --git a/simulation/src/de/c3ma/ollo/mockup/ui/WS2812Layout.java b/simulation/src/de/c3ma/ollo/mockup/ui/WS2812Layout.java index f59fa4b..d99e3b9 100644 --- a/simulation/src/de/c3ma/ollo/mockup/ui/WS2812Layout.java +++ b/simulation/src/de/c3ma/ollo/mockup/ui/WS2812Layout.java @@ -288,6 +288,10 @@ public class WS2812Layout extends JFrame { this.setForeground(new Color(red, green, blue)); this.repaint(); } + + public Color getColor() { + return this.getForeground(); + } @Override public String toString() { @@ -319,5 +323,40 @@ public class WS2812Layout extends JFrame { } } } + + public Element getLED(int index) { + if (mElements != null) { + int i = (index / mColumn); + int j = (index % mColumn); + if (i % 2 == 1) { + j = (mColumn-1) - j; + } + if (i < 0 || j < 0) { + System.err.println("LED index" + index + " results in " + i + "x" + j + " coordinate"); + return null; + } + + if ((i < mElements.length) && (j < mElements[i].length) && (mElements[i][j] != null)) { + return mElements[i][j]; + } else { + return null; + } + } else { + return null; + } + } + + public void fillLEDs(int r, int g, int b) { + if (mElements != null) { + for(int i=0;(i < mElements.length); i++) { + for (int j=0; (j < mElements[i].length); j++) { + if (mElements[i][j] != null) { + Element curlbl = mElements[i][j]; + curlbl.setColor(r, g, b); + } + } + } + } + } } diff --git a/telnet.lua b/telnet.lua new file mode 100644 index 0000000..8193728 --- /dev/null +++ b/telnet.lua @@ -0,0 +1,51 @@ +-- Telnet client +--[[ A telnet server T. Ellison, June 2019 + +This version of the telnet server demonstrates the use of the new stdin and stout +pipes, which is a C implementation of the Lua fifosock concept moved into the +Lua core. These two pipes are referenced in the Lua registry. + +]] +--luacheck: no unused args + +local telnetS = nil + +local function telnet_session(socket) + local node = node + local stdout + + local function output_CB(opipe) -- upval: socket + stdout = opipe + local rec = opipe:read(1400) + if rec and (#rec > 0) then socket:send(rec) end + return false -- don't repost as the on:sent will do this + end + + local function onsent_CB(skt) -- upval: stdout + local rec = stdout:read(1400) + if rec and #rec > 0 then skt:send(rec) end + end + + local function disconnect_CB(skt) -- upval: socket, stdout + node.output() + socket, stdout = nil, nil -- set upvals to nl to allow GC + end + + node.output(output_CB, 0) + socket:on("receive", function(_,rec) node.input(rec) end) + socket:on("sent", onsent_CB) + socket:on("disconnection", disconnect_CB) + print( ("Welcome to the Wordclock. (%d mem free, %s)"):format(node.heap(), wifi.sta.getip())) + print("- mydofile(\"commands\")") + print("- storeConfig()") + print("Visite https://github.com/nodemcu/nodemcu-firmware/wiki/nodemcu_api_en for further commands") + uart.write(0, "New client connected\r\n") +end + + +-- Telnet Server +function startTelnetServer() + telnetS=net.createServer(net.TCP, 180) + telnetS:listen(23, telnet_session) + print("Telnetserver started") +end diff --git a/timecore.lua b/timecore.lua index 4983a15..e7bb36c 100755 --- a/timecore.lua +++ b/timecore.lua @@ -1,3 +1,6 @@ +local M +do + --Summer winter time convertion --See: https://arduinodiy.wordpress.com/2015/10/13/the-arduino-and-daylight-saving-time/ -- @@ -27,7 +30,7 @@ -- @var dow Current day of week range (1 - 7) (1 is Monday, 7 is Sunday) -- @return true if we have currently summer time -- @return false if we have winter time -function isSummerTime(time) +local function isSummerTime(time) -- we are in 100% in the summer time if (time.month > 3 and time.month < 10) then return true @@ -103,7 +106,7 @@ local yearsize = function(year) end end -function getUTCtime(unixtimestmp) +local function getUTCtime(unixtimestmp) local year = EPOCH_YR local dayclock = math.floor(unixtimestmp % SECS_DAY) local dayno = math.floor(unixtimestmp / SECS_DAY) @@ -131,10 +134,16 @@ function getUTCtime(unixtimestmp) end -function getTime(unixtimestmp, timezoneoffset) +local getTime = function(unixtimestmp, timezoneoffset) local time = getUTCtime(unixtimestmp + (3600 * timezoneoffset)) if ( isSummerTime(time) ) then time = getUTCtime(unixtimestmp + (3600 * (timezoneoffset + 1)) ) end return time end +-- Pack everything into a module +M = { + getTime = getTime +} +end +tc = M diff --git a/tools/Readme.md b/tools/Readme.md index 2b6725d..82058d8 100644 --- a/tools/Readme.md +++ b/tools/Readme.md @@ -1,2 +1,13 @@ -# Source: +# luatool.py +Version 0.8.0 upgraded supported with python 3.x +## Source: https://github.com/4refr0nt/luatool/tree/master/luatool +# LuaSrcDiet +LuaSrcDiet reduces the size of Lua 5.1+ source files by aggressively removing all unnecessary whitespace and comments, optimizing constant tokens, and renaming local variables to shorter names. +* https://github.com/jirutka/luasrcdiet + +## Source: +https://raw.githubusercontent.com/jirutka/luasrcdiet/master/bin/luasrcdiet + +## Example: + bin/luasrcdiet ../webserver.lua -o ../webserver_diet.lua diff --git a/tools/bin/luasrcdiet b/tools/bin/luasrcdiet new file mode 100755 index 0000000..28e6289 --- /dev/null +++ b/tools/bin/luasrcdiet @@ -0,0 +1,653 @@ +#!/usr/bin/env lua +--------- +-- LuaSrcDiet +-- +-- Compresses Lua source code by removing unnecessary characters. +-- For Lua 5.1+ source code. +-- +-- **Notes:** +-- +-- * Remember to update version and date information below (MSG_TITLE). +-- * TODO: passing data tables around is a horrific mess. +-- * TODO: to implement pcall() to properly handle lexer etc. errors. +-- * TODO: need some automatic testing for a semblance of sanity. +-- * TODO: the plugin module is highly experimental and unstable. +---- +local equiv = require "luasrcdiet.equiv" +local fs = require "luasrcdiet.fs" +local llex = require "luasrcdiet.llex" +local lparser = require "luasrcdiet.lparser" +local luasrcdiet = require "luasrcdiet.init" +local optlex = require "luasrcdiet.optlex" +local optparser = require "luasrcdiet.optparser" + +local byte = string.byte +local concat = table.concat +local find = string.find +local fmt = string.format +local gmatch = string.gmatch +local match = string.match +local print = print +local rep = string.rep +local sub = string.sub + +local plugin + +local LUA_VERSION = match(_VERSION, " (5%.[123])$") or "5.1" + +-- Is --opt-binequiv available for this Lua version? +local BIN_EQUIV_AVAIL = LUA_VERSION == "5.1" and not package.loaded.jit + + +---------------------- Messages and textual data ---------------------- + +local MSG_TITLE = fmt([[ +LuaSrcDiet: Puts your Lua 5.1+ source code on a diet +Version %s <%s> +]], luasrcdiet._VERSION, luasrcdiet._HOMEPAGE) + +local MSG_USAGE = [[ +usage: luasrcdiet [options] [filenames] + +example: + >luasrcdiet myscript.lua -o myscript_.lua + +options: + -v, --version prints version information + -h, --help prints usage information + -o specify file name to write output + -s suffix for output files (default '_') + --keep keep block comment with inside + --plugin run in plugin/ directory + - stop handling arguments + + (optimization levels) + --none all optimizations off (normalizes EOLs only) + --basic lexer-based optimizations only + --maximum maximize reduction of source + + (informational) + --quiet process files quietly + --read-only read file and print token stats only + --dump-lexer dump raw tokens from lexer to stdout + --dump-parser dump variable tracking tables from parser + --details extra info (strings, numbers, locals) + +features (to disable, insert 'no' prefix like --noopt-comments): +%s +default settings: +%s]] + +-- Optimization options, for ease of switching on and off. +-- +-- * Positive to enable optimization, negative (no) to disable. +-- * These options should follow --opt-* and --noopt-* style for now. +local OPTION = [[ +--opt-comments,'remove comments and block comments' +--opt-whitespace,'remove whitespace excluding EOLs' +--opt-emptylines,'remove empty lines' +--opt-eols,'all above, plus remove unnecessary EOLs' +--opt-strings,'optimize strings and long strings' +--opt-numbers,'optimize numbers' +--opt-locals,'optimize local variable names' +--opt-entropy,'tries to reduce symbol entropy of locals' +--opt-srcequiv,'insist on source (lexer stream) equivalence' +--opt-binequiv,'insist on binary chunk equivalence (only for PUC Lua 5.1)' +--opt-experimental,'apply experimental optimizations' +]] + +-- Preset configuration. +local DEFAULT_CONFIG = [[ + --opt-comments --opt-whitespace --opt-emptylines + --opt-numbers --opt-locals + --opt-srcequiv --noopt-binequiv +]] +-- Override configurations: MUST explicitly enable/disable everything. +local BASIC_CONFIG = [[ + --opt-comments --opt-whitespace --opt-emptylines + --noopt-eols --noopt-strings --noopt-numbers + --noopt-locals --noopt-entropy + --opt-srcequiv --noopt-binequiv +]] +local MAXIMUM_CONFIG = [[ + --opt-comments --opt-whitespace --opt-emptylines + --opt-eols --opt-strings --opt-numbers + --opt-locals --opt-entropy + --opt-srcequiv +]] .. (BIN_EQUIV_AVAIL and ' --opt-binequiv' or ' --noopt-binequiv') + +local NONE_CONFIG = [[ + --noopt-comments --noopt-whitespace --noopt-emptylines + --noopt-eols --noopt-strings --noopt-numbers + --noopt-locals --noopt-entropy + --opt-srcequiv --noopt-binequiv +]] + +local DEFAULT_SUFFIX = "_" -- default suffix for file renaming +local PLUGIN_SUFFIX = "luasrcdiet.plugin." -- relative location of plugins + + +------------- Startup and initialize option list handling ------------- + +--- Simple error message handler; change to error if traceback wanted. +-- +-- @tparam string msg The message to print. +local function die(msg) + print("LuaSrcDiet (error): "..msg); os.exit(1) +end +--die = error--DEBUG + +-- Prepare text for list of optimizations, prepare lookup table. +local MSG_OPTIONS = "" +do + local WIDTH = 24 + local o = {} + for op, desc in gmatch(OPTION, "%s*([^,]+),'([^']+)'") do + local msg = " "..op + msg = msg..rep(" ", WIDTH - #msg)..desc.."\n" + MSG_OPTIONS = MSG_OPTIONS..msg + o[op] = true + o["--no"..sub(op, 3)] = true + end + OPTION = o -- replace OPTION with lookup table +end + +MSG_USAGE = fmt(MSG_USAGE, MSG_OPTIONS, DEFAULT_CONFIG) + + +--------- Global variable initialization, option set handling --------- + +local suffix = DEFAULT_SUFFIX -- file suffix +local option = {} -- program options +local stat_c, stat_l -- statistics tables + +--- Sets option lookup table based on a text list of options. +-- +-- Note: additional forced settings for --opt-eols is done in optlex.lua. +-- +-- @tparam string CONFIG +local function set_options(CONFIG) + for op in gmatch(CONFIG, "(%-%-%S+)") do + if sub(op, 3, 4) == "no" and -- handle negative options + OPTION["--"..sub(op, 5)] then + option[sub(op, 5)] = false + else + option[sub(op, 3)] = true + end + end +end + + +-------------------------- Support functions -------------------------- + +-- List of token types, parser-significant types are up to TTYPE_GRAMMAR +-- while the rest are not used by parsers; arranged for stats display. +local TTYPES = { + "TK_KEYWORD", "TK_NAME", "TK_NUMBER", -- grammar + "TK_STRING", "TK_LSTRING", "TK_OP", + "TK_EOS", + "TK_COMMENT", "TK_LCOMMENT", -- non-grammar + "TK_EOL", "TK_SPACE", +} +local TTYPE_GRAMMAR = 7 + +local EOLTYPES = { -- EOL names for token dump + ["\n"] = "LF", ["\r"] = "CR", + ["\n\r"] = "LFCR", ["\r\n"] = "CRLF", +} + +--- Reads source code from the file. +-- +-- @tparam string fname Path of the file to read. +-- @treturn string Content of the file. +local function load_file(fname) + local data, err = fs.read_file(fname, "rb") + if not data then die(err) end + return data +end + +--- Saves source code to the file. +-- +-- @tparam string fname Path of the destination file. +-- @tparam string dat The data to write into the file. +local function save_file(fname, dat) + local ok, err = fs.write_file(fname, dat, "wb") + if not ok then die(err) end +end + + +------------------ Functions to deal with statistics ------------------ + +--- Initializes the statistics table. +local function stat_init() + stat_c, stat_l = {}, {} + for i = 1, #TTYPES do + local ttype = TTYPES[i] + stat_c[ttype], stat_l[ttype] = 0, 0 + end +end + +--- Adds a token to the statistics table. +-- +-- @tparam string tok The token. +-- @param seminfo +local function stat_add(tok, seminfo) + stat_c[tok] = stat_c[tok] + 1 + stat_l[tok] = stat_l[tok] + #seminfo +end + +--- Computes totals for the statistics table, returns average table. +-- +-- @treturn table +local function stat_calc() + local function avg(c, l) -- safe average function + if c == 0 then return 0 end + return l / c + end + local stat_a = {} + local c, l = 0, 0 + for i = 1, TTYPE_GRAMMAR do -- total grammar tokens + local ttype = TTYPES[i] + c = c + stat_c[ttype]; l = l + stat_l[ttype] + end + stat_c.TOTAL_TOK, stat_l.TOTAL_TOK = c, l + stat_a.TOTAL_TOK = avg(c, l) + c, l = 0, 0 + for i = 1, #TTYPES do -- total all tokens + local ttype = TTYPES[i] + c = c + stat_c[ttype]; l = l + stat_l[ttype] + stat_a[ttype] = avg(stat_c[ttype], stat_l[ttype]) + end + stat_c.TOTAL_ALL, stat_l.TOTAL_ALL = c, l + stat_a.TOTAL_ALL = avg(c, l) + return stat_a +end + + +----------------------------- Main tasks ----------------------------- + +--- A simple token dumper, minimal translation of seminfo data. +-- +-- @tparam string srcfl Path of the source file. +local function dump_tokens(srcfl) + -- Load file and process source input into tokens. + local z = load_file(srcfl) + local toklist, seminfolist = llex.lex(z) + + -- Display output. + for i = 1, #toklist do + local tok, seminfo = toklist[i], seminfolist[i] + if tok == "TK_OP" and byte(seminfo) < 32 then + seminfo = "("..byte(seminfo)..")" + elseif tok == "TK_EOL" then + seminfo = EOLTYPES[seminfo] + else + seminfo = "'"..seminfo.."'" + end + print(tok.." "..seminfo) + end--for +end + +--- Dumps globalinfo and localinfo tables. +-- +-- @tparam string srcfl Path of the source file. +local function dump_parser(srcfl) + -- Load file and process source input into tokens, + local z = load_file(srcfl) + local toklist, seminfolist, toklnlist = llex.lex(z) + + -- Do parser optimization here. + local xinfo = lparser.parse(toklist, seminfolist, toklnlist) + local globalinfo, localinfo = xinfo.globalinfo, xinfo.localinfo + + -- Display output. + local hl = rep("-", 72) + print("*** Local/Global Variable Tracker Tables ***") + print(hl.."\n GLOBALS\n"..hl) + -- global tables have a list of xref numbers only + for i = 1, #globalinfo do + local obj = globalinfo[i] + local msg = "("..i..") '"..obj.name.."' -> " + local xref = obj.xref + for j = 1, #xref do msg = msg..xref[j].." " end + print(msg) + end + -- Local tables have xref numbers and a few other special + -- numbers that are specially named: decl (declaration xref), + -- act (activation xref), rem (removal xref). + print(hl.."\n LOCALS (decl=declared act=activated rem=removed)\n"..hl) + for i = 1, #localinfo do + local obj = localinfo[i] + local msg = "("..i..") '"..obj.name.."' decl:"..obj.decl.. + " act:"..obj.act.." rem:"..obj.rem + if obj.is_special then + msg = msg.." is_special" + end + msg = msg.." -> " + local xref = obj.xref + for j = 1, #xref do msg = msg..xref[j].." " end + print(msg) + end + print(hl.."\n") +end + +--- Reads source file(s) and reports some statistics. +-- +-- @tparam string srcfl Path of the source file. +local function read_only(srcfl) + -- Load file and process source input into tokens. + local z = load_file(srcfl) + local toklist, seminfolist = llex.lex(z) + print(MSG_TITLE) + print("Statistics for: "..srcfl.."\n") + + -- Collect statistics. + stat_init() + for i = 1, #toklist do + local tok, seminfo = toklist[i], seminfolist[i] + stat_add(tok, seminfo) + end--for + local stat_a = stat_calc() + + -- Display output. + local function figures(tt) + return stat_c[tt], stat_l[tt], stat_a[tt] + end + local tabf1, tabf2 = "%-16s%8s%8s%10s", "%-16s%8d%8d%10.2f" + local hl = rep("-", 42) + print(fmt(tabf1, "Lexical", "Input", "Input", "Input")) + print(fmt(tabf1, "Elements", "Count", "Bytes", "Average")) + print(hl) + for i = 1, #TTYPES do + local ttype = TTYPES[i] + print(fmt(tabf2, ttype, figures(ttype))) + if ttype == "TK_EOS" then print(hl) end + end + print(hl) + print(fmt(tabf2, "Total Elements", figures("TOTAL_ALL"))) + print(hl) + print(fmt(tabf2, "Total Tokens", figures("TOTAL_TOK"))) + print(hl.."\n") +end + +--- Processes source file(s), writes output and reports some statistics. +-- +-- @tparam string srcfl Path of the source file. +-- @tparam string destfl Path of the destination file where to write optimized source. +local function process_file(srcfl, destfl) + -- handle quiet option + local function print(...) --luacheck: ignore 431 + if option.QUIET then return end + _G.print(...) + end + if plugin and plugin.init then -- plugin init + option.EXIT = false + plugin.init(option, srcfl, destfl) + if option.EXIT then return end + end + print(MSG_TITLE) -- title message + + -- Load file and process source input into tokens. + local z = load_file(srcfl) + if plugin and plugin.post_load then -- plugin post-load + z = plugin.post_load(z) or z + if option.EXIT then return end + end + local toklist, seminfolist, toklnlist = llex.lex(z) + if plugin and plugin.post_lex then -- plugin post-lex + plugin.post_lex(toklist, seminfolist, toklnlist) + if option.EXIT then return end + end + + -- Collect 'before' statistics. + stat_init() + for i = 1, #toklist do + local tok, seminfo = toklist[i], seminfolist[i] + stat_add(tok, seminfo) + end--for + local stat1_a = stat_calc() + local stat1_c, stat1_l = stat_c, stat_l + + -- Do parser optimization here. + optparser.print = print -- hack + local xinfo = lparser.parse(toklist, seminfolist, toklnlist) + if plugin and plugin.post_parse then -- plugin post-parse + plugin.post_parse(xinfo.globalinfo, xinfo.localinfo) + if option.EXIT then return end + end + optparser.optimize(option, toklist, seminfolist, xinfo) + if plugin and plugin.post_optparse then -- plugin post-optparse + plugin.post_optparse() + if option.EXIT then return end + end + + -- Do lexer optimization here, save output file. + local warn = optlex.warn -- use this as a general warning lookup + optlex.print = print -- hack + toklist, seminfolist, toklnlist + = optlex.optimize(option, toklist, seminfolist, toklnlist) + if plugin and plugin.post_optlex then -- plugin post-optlex + plugin.post_optlex(toklist, seminfolist, toklnlist) + if option.EXIT then return end + end + local dat = concat(seminfolist) + -- Depending on options selected, embedded EOLs in long strings and + -- long comments may not have been translated to \n, tack a warning. + if find(dat, "\r\n", 1, 1) or + find(dat, "\n\r", 1, 1) then + warn.MIXEDEOL = true + end + + -- Test source and binary chunk equivalence. + equiv.init(option, llex, warn) + equiv.source(z, dat) + if BIN_EQUIV_AVAIL then + equiv.binary(z, dat) + end + local smsg = "before and after lexer streams are NOT equivalent!" + local bmsg = "before and after binary chunks are NOT equivalent!" + -- for reporting, die if option was selected, else just warn + if warn.SRC_EQUIV then + if option["opt-srcequiv"] then die(smsg) end + else + print("*** SRCEQUIV: token streams are sort of equivalent") + if option["opt-locals"] then + print("(but no identifier comparisons since --opt-locals enabled)") + end + print() + end + if warn.BIN_EQUIV then + if option["opt-binequiv"] then die(bmsg) end + elseif BIN_EQUIV_AVAIL then + print("*** BINEQUIV: binary chunks are sort of equivalent") + print() + end + + -- Save optimized source stream to output file. + save_file(destfl, dat) + + -- Collect 'after' statistics. + stat_init() + for i = 1, #toklist do + local tok, seminfo = toklist[i], seminfolist[i] + stat_add(tok, seminfo) + end--for + local stat_a = stat_calc() + + -- Display output. + print("Statistics for: "..srcfl.." -> "..destfl.."\n") + local function figures(tt) + return stat1_c[tt], stat1_l[tt], stat1_a[tt], + stat_c[tt], stat_l[tt], stat_a[tt] + end + local tabf1, tabf2 = "%-16s%8s%8s%10s%8s%8s%10s", + "%-16s%8d%8d%10.2f%8d%8d%10.2f" + local hl = rep("-", 68) + print("*** lexer-based optimizations summary ***\n"..hl) + print(fmt(tabf1, "Lexical", + "Input", "Input", "Input", + "Output", "Output", "Output")) + print(fmt(tabf1, "Elements", + "Count", "Bytes", "Average", + "Count", "Bytes", "Average")) + print(hl) + for i = 1, #TTYPES do + local ttype = TTYPES[i] + print(fmt(tabf2, ttype, figures(ttype))) + if ttype == "TK_EOS" then print(hl) end + end + print(hl) + print(fmt(tabf2, "Total Elements", figures("TOTAL_ALL"))) + print(hl) + print(fmt(tabf2, "Total Tokens", figures("TOTAL_TOK"))) + print(hl) + + -- Report warning flags from optimizing process. + if warn.LSTRING then + print("* WARNING: "..warn.LSTRING) + elseif warn.MIXEDEOL then + print("* WARNING: ".."output still contains some CRLF or LFCR line endings") + elseif warn.SRC_EQUIV then + print("* WARNING: "..smsg) + elseif warn.BIN_EQUIV then + print("* WARNING: "..bmsg) + end + print() +end + + +---------------------------- Main functions --------------------------- + +local arg = {...} -- program arguments +set_options(DEFAULT_CONFIG) -- set to default options at beginning + +--- Does per-file handling, ship off to tasks. +-- +-- @tparam {string,...} fspec List of source files. +local function do_files(fspec) + for i = 1, #fspec do + local srcfl = fspec[i] + local destfl + + -- Find and replace extension for filenames. + local extb, exte = find(srcfl, "%.[^%.%\\%/]*$") + local basename, extension = srcfl, "" + if extb and extb > 1 then + basename = sub(srcfl, 1, extb - 1) + extension = sub(srcfl, extb, exte) + end + destfl = basename..suffix..extension + if #fspec == 1 and option.OUTPUT_FILE then + destfl = option.OUTPUT_FILE + end + if srcfl == destfl then + die("output filename identical to input filename") + end + + -- Perform requested operations. + if option.DUMP_LEXER then + dump_tokens(srcfl) + elseif option.DUMP_PARSER then + dump_parser(srcfl) + elseif option.READ_ONLY then + read_only(srcfl) + else + process_file(srcfl, destfl) + end + end--for +end + +--- The main function. +local function main() + local fspec = {} + local argn, i = #arg, 1 + if argn == 0 then + option.HELP = true + end + + -- Handle arguments. + while i <= argn do + local o, p = arg[i], arg[i + 1] + local dash = match(o, "^%-%-?") + if dash == "-" then -- single-dash options + if o == "-h" then + option.HELP = true; break + elseif o == "-v" then + option.VERSION = true; break + elseif o == "-s" then + if not p then die("-s option needs suffix specification") end + suffix = p + i = i + 1 + elseif o == "-o" then + if not p then die("-o option needs a file name") end + option.OUTPUT_FILE = p + i = i + 1 + elseif o == "-" then + break -- ignore rest of args + else + die("unrecognized option "..o) + end + elseif dash == "--" then -- double-dash options + if o == "--help" then + option.HELP = true; break + elseif o == "--version" then + option.VERSION = true; break + elseif o == "--keep" then + if not p then die("--keep option needs a string to match for") end + option.KEEP = p + i = i + 1 + elseif o == "--plugin" then + if not p then die("--plugin option needs a module name") end + if option.PLUGIN then die("only one plugin can be specified") end + option.PLUGIN = p + plugin = require(PLUGIN_SUFFIX..p) + i = i + 1 + elseif o == "--quiet" then + option.QUIET = true + elseif o == "--read-only" then + option.READ_ONLY = true + elseif o == "--basic" then + set_options(BASIC_CONFIG) + elseif o == "--maximum" then + set_options(MAXIMUM_CONFIG) + elseif o == "--none" then + set_options(NONE_CONFIG) + elseif o == "--dump-lexer" then + option.DUMP_LEXER = true + elseif o == "--dump-parser" then + option.DUMP_PARSER = true + elseif o == "--details" then + option.DETAILS = true + elseif OPTION[o] then -- lookup optimization options + set_options(o) + else + die("unrecognized option "..o) + end + else + fspec[#fspec + 1] = o -- potential filename + end + i = i + 1 + end--while + if option.HELP then + print(MSG_TITLE..MSG_USAGE); return true + elseif option.VERSION then + print(MSG_TITLE); return true + end + if option["opt-binequiv"] and not BIN_EQUIV_AVAIL then + die("--opt-binequiv is available only for PUC Lua 5.1!") + end + if #fspec > 0 then + if #fspec > 1 and option.OUTPUT_FILE then + die("with -o, only one source file can be specified") + end + do_files(fspec) + return true + else + die("nothing to do!") + end +end + +-- entry point -> main() -> do_files() +if not main() then + die("Please run with option -h or --help for usage information") +end diff --git a/tools/initialDietFlash.sh b/tools/initialDietFlash.sh new file mode 100755 index 0000000..41fc7a5 --- /dev/null +++ b/tools/initialDietFlash.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +TOOLDIR=tools/ +LUATOOL=${TOOLDIR}luatool.py + +#DIET=bin/luasrcdiet --maximum +DIET=bin/luasrcdiet + +DEVICE=$1 +BAUD=115200 + +LUASCRIPT_STOP=${TOOLDIR}/stopController.lua + +# check environment +if [ ! -f $LUATOOL ]; then + echo "$LUATOOL not found" + echo "is the command prompt at the same level as the tools folder ?" + exit 1 +fi + +# check the serial connection + +if [ ! -c $DEVICE ]; then + echo "Serial target: $DEVICE does not exist" + exit 1 +fi + +if [ $# -eq 0 ]; then + echo "" + echo "e.g. usage $0 []" + exit 1 +fi + +if [ $# -eq 1 ]; then + FILES="displayword.lua main.lua timecore.lua webpage.html webserver.lua telnet.lua wordclock.lua init.lua" +else + FILES=$2 +fi + +# Convert files, if necessary +if [ "$FILES" != "config.lua" ]; then + echo "Generate DIET version of the files" + OUTFILES="" + ROOTDIR=$PWD + cd $TOOLDIR + for f in $FILES; do + if [[ "$f" == *.lua ]] && [[ "$f" != init.lua ]]; then + echo "Compress $f ..." + out=$(echo "$f" | sed 's/.lua/_diet.lua/g') + $DIET ../$f -o ../diet/$out + OUTFILES="$OUTFILES diet/$out" + else + OUTFILES="$OUTFILES $f" + fi + done + FILES=$OUTFILES + cd $ROOTDIR +fi +echo "Reboot ESP and stop init timer" +if [ ! -f $LUASCRIPT_STOP ]; then + echo "Cannot find $LUASCRIPT_STOP" + exit 1 +fi +python3 $LUATOOL -p $DEVICE -f $LUASCRIPT_STOP -b $BAUD --volatile --delay 2 +if [ $? -ne 0 ]; then + echo "Could not reboot" + exit 1 +fi + +if [ $# -eq 1 ]; then + # Format filesystem first + echo "Format the complete ESP" + python3 $LUATOOL -p $DEVICE -w -b $BAUD + if [ $? -ne 0 ]; then + echo "STOOOOP" + exit 1 + fi +fi + +echo "Start Flasing ..." +for f in $FILES; do + if [ ! -f $f ]; then + echo "Cannot find $f" + echo "place the terminal into the folder where the lua files are present" + exit 1 + fi + + espFile=$(echo "$f" | sed 's;diet/;;g') + echo "------------- $espFile ------------" + python3 $LUATOOL -p $DEVICE -f $f -b $BAUD -t $espFile + if [ $? -ne 0 ]; then + echo "STOOOOP" + exit 1 + fi +done + +if [ $# -eq 1 ]; then + echo "Reboot the ESP" + echo "node.restart()" >> $DEVICE +fi + +exit 0 diff --git a/tools/initialFlash.sh b/tools/initialFlash.sh deleted file mode 100755 index b54d309..0000000 --- a/tools/initialFlash.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -LUATOOL=./tools/luatool.py - -DEVICE=$1 - -# check the serial connection - -if [ ! -c $DEVICE ]; then - echo "$DEVICE does not exist" - exit 1 -fi - - -if [ $# -ne 1 ]; then - echo "" - echo "e.g. usage $0 " - exit 1 -fi - -FILES="displayword.lua main.lua timecore.lua webpage.html webserver.lua wordclock.lua init.lua" - -# Format filesystem first -echo "Format the complete ESP" -$LUATOOL -p $DEVICE -w -b 115200 -if [ $? -ne 0 ]; then - echo "STOOOOP" - exit 1 -fi - -echo -echo "Start Flasing ..." -for f in $FILES; do - if [ ! -f $f ]; then - echo "Cannot find $f" - echo "place the terminal into the folder where the lua files are present" - exit 1 - fi - echo "------------- $f ------------" - $LUATOOL -p $DEVICE -f $f -b 115200 -t $f - if [ $? -ne 0 ]; then - echo "STOOOOP" - exit 1 - fi -done - -echo "Reboot the ESP" -$LUATOOL -p $DEVICE -r -b 115200 - -exit 0 diff --git a/tools/luasrcdiet/equiv.lua b/tools/luasrcdiet/equiv.lua new file mode 100644 index 0000000..3efa4ef --- /dev/null +++ b/tools/luasrcdiet/equiv.lua @@ -0,0 +1,465 @@ +--------- +-- Source and binary equivalency comparisons +-- +-- **Notes:** +-- +-- * Intended as an extra safety check for mission-critical code, +-- should give affirmative results if everything works. +-- * Heavy on load() and string.dump(), which may be slowish, +-- and may cause problems for cross-compiled applications. +-- * Optional detailed information dump is mainly for debugging, +-- reason being, if the two are not equivalent when they should be, +-- then some form of optimization has failed. +-- * source: IMPORTANT: TK_NAME not compared if opt-locals enabled. +-- * binary: IMPORTANT: Some shortcuts are taken with int and size_t +-- value reading -- if the functions break, then the binary chunk +-- is very large indeed. +-- * binary: There is a lack of diagnostic information when a compare +-- fails; you can use ChunkSpy and compare using visual diff. +---- +local byte = string.byte +local dump = string.dump +local load = loadstring or load --luacheck: ignore 113 +local sub = string.sub + +local M = {} + +local is_realtoken = { -- significant (grammar) tokens + TK_KEYWORD = true, + TK_NAME = true, + TK_NUMBER = true, + TK_STRING = true, + TK_LSTRING = true, + TK_OP = true, + TK_EOS = true, +} + +local option, llex, warn + + +--- The initialization function. +-- +-- @tparam {[string]=bool,...} _option +-- @tparam luasrcdiet.llex _llex +-- @tparam table _warn +function M.init(_option, _llex, _warn) + option = _option + llex = _llex + warn = _warn +end + +--- Builds lists containing a 'normal' lexer stream. +-- +-- @tparam string s The source code. +-- @treturn table +-- @treturn table +local function build_stream(s) + local stok, sseminfo = llex.lex(s) -- source list (with whitespace elements) + local tok, seminfo -- processed list (real elements only) + = {}, {} + for i = 1, #stok do + local t = stok[i] + if is_realtoken[t] then + tok[#tok + 1] = t + seminfo[#seminfo + 1] = sseminfo[i] + end + end--for + return tok, seminfo +end + +-- Tests source (lexer stream) equivalence. +-- +-- @tparam string z +-- @tparam string dat +function M.source(z, dat) + + -- Returns a dumped string for seminfo compares. + local function dumpsem(s) + local sf = load("return "..s, "z") + if sf then + return dump(sf) + end + end + + -- Marks and optionally reports non-equivalence. + local function bork(msg) + if option.DETAILS then print("SRCEQUIV: "..msg) end + warn.SRC_EQUIV = true + end + + -- Get lexer streams for both source strings, compare. + local tok1, seminfo1 = build_stream(z) -- original + local tok2, seminfo2 = build_stream(dat) -- compressed + + -- Compare shbang lines ignoring EOL. + local sh1 = z:match("^(#[^\r\n]*)") + local sh2 = dat:match("^(#[^\r\n]*)") + if sh1 or sh2 then + if not sh1 or not sh2 or sh1 ~= sh2 then + bork("shbang lines different") + end + end + + -- Compare by simple count. + if #tok1 ~= #tok2 then + bork("count "..#tok1.." "..#tok2) + return + end + + -- Compare each element the best we can. + for i = 1, #tok1 do + local t1, t2 = tok1[i], tok2[i] + local s1, s2 = seminfo1[i], seminfo2[i] + if t1 ~= t2 then -- by type + bork("type ["..i.."] "..t1.." "..t2) + break + end + if t1 == "TK_KEYWORD" or t1 == "TK_NAME" or t1 == "TK_OP" then + if t1 == "TK_NAME" and option["opt-locals"] then + -- can't compare identifiers of locals that are optimized + elseif s1 ~= s2 then -- by semantic info (simple) + bork("seminfo ["..i.."] "..t1.." "..s1.." "..s2) + break + end + elseif t1 == "TK_EOS" then + -- no seminfo to compare + else-- "TK_NUMBER" or "TK_STRING" or "TK_LSTRING" + -- compare 'binary' form, so dump a function + local s1b,s2b = dumpsem(s1), dumpsem(s2) + if not s1b or not s2b or s1b ~= s2b then + bork("seminfo ["..i.."] "..t1.." "..s1.." "..s2) + break + end + end + end--for + + -- Successful comparison if end is reached with no borks. +end + +--- Tests binary chunk equivalence (only for PUC Lua 5.1). +-- +-- @tparam string z +-- @tparam string dat +function M.binary(z, dat) + local TNIL = 0 --luacheck: ignore 211 + local TBOOLEAN = 1 + local TNUMBER = 3 + local TSTRING = 4 + + -- sizes of data types + local endian + local sz_int + local sz_sizet + local sz_inst + local sz_number + local getint + local getsizet + + -- Marks and optionally reports non-equivalence. + local function bork(msg) + if option.DETAILS then print("BINEQUIV: "..msg) end + warn.BIN_EQUIV = true + end + + -- Checks if bytes exist. + local function ensure(c, sz) + if c.i + sz - 1 > c.len then return end + return true + end + + -- Skips some bytes. + local function skip(c, sz) + if not sz then sz = 1 end + c.i = c.i + sz + end + + -- Returns a byte value. + local function getbyte(c) + local i = c.i + if i > c.len then return end + local d = sub(c.dat, i, i) + c.i = i + 1 + return byte(d) + end + + -- Return an int value (little-endian). + local function getint_l(c) + local n, scale = 0, 1 + if not ensure(c, sz_int) then return end + for _ = 1, sz_int do + n = n + scale * getbyte(c) + scale = scale * 256 + end + return n + end + + -- Returns an int value (big-endian). + local function getint_b(c) + local n = 0 + if not ensure(c, sz_int) then return end + for _ = 1, sz_int do + n = n * 256 + getbyte(c) + end + return n + end + + -- Returns a size_t value (little-endian). + local function getsizet_l(c) + local n, scale = 0, 1 + if not ensure(c, sz_sizet) then return end + for _ = 1, sz_sizet do + n = n + scale * getbyte(c) + scale = scale * 256 + end + return n + end + + -- Returns a size_t value (big-endian). + local function getsizet_b(c) + local n = 0 + if not ensure(c, sz_sizet) then return end + for _ = 1, sz_sizet do + n = n * 256 + getbyte(c) + end + return n + end + + -- Returns a block (as a string). + local function getblock(c, sz) + local i = c.i + local j = i + sz - 1 + if j > c.len then return end + local d = sub(c.dat, i, j) + c.i = i + sz + return d + end + + -- Returns a string. + local function getstring(c) + local n = getsizet(c) + if not n then return end + if n == 0 then return "" end + return getblock(c, n) + end + + -- Compares byte value. + local function goodbyte(c1, c2) + local b1, b2 = getbyte(c1), getbyte(c2) + if not b1 or not b2 or b1 ~= b2 then + return + end + return b1 + end + + -- Compares byte value. + local function badbyte(c1, c2) + local b = goodbyte(c1, c2) + if not b then return true end + end + + -- Compares int value. + local function goodint(c1, c2) + local i1, i2 = getint(c1), getint(c2) + if not i1 or not i2 or i1 ~= i2 then + return + end + return i1 + end + + -- Recursively-called function to compare function prototypes. + local function getfunc(c1, c2) + -- source name (ignored) + if not getstring(c1) or not getstring(c2) then + bork("bad source name"); return + end + -- linedefined (ignored) + if not getint(c1) or not getint(c2) then + bork("bad linedefined"); return + end + -- lastlinedefined (ignored) + if not getint(c1) or not getint(c2) then + bork("bad lastlinedefined"); return + end + if not (ensure(c1, 4) and ensure(c2, 4)) then + bork("prototype header broken") + end + -- nups (compared) + if badbyte(c1, c2) then + bork("bad nups"); return + end + -- numparams (compared) + if badbyte(c1, c2) then + bork("bad numparams"); return + end + -- is_vararg (compared) + if badbyte(c1, c2) then + bork("bad is_vararg"); return + end + -- maxstacksize (compared) + if badbyte(c1, c2) then + bork("bad maxstacksize"); return + end + -- code (compared) + local ncode = goodint(c1, c2) + if not ncode then + bork("bad ncode"); return + end + local code1 = getblock(c1, ncode * sz_inst) + local code2 = getblock(c2, ncode * sz_inst) + if not code1 or not code2 or code1 ~= code2 then + bork("bad code block"); return + end + -- constants (compared) + local nconst = goodint(c1, c2) + if not nconst then + bork("bad nconst"); return + end + for _ = 1, nconst do + local ctype = goodbyte(c1, c2) + if not ctype then + bork("bad const type"); return + end + if ctype == TBOOLEAN then + if badbyte(c1, c2) then + bork("bad boolean value"); return + end + elseif ctype == TNUMBER then + local num1 = getblock(c1, sz_number) + local num2 = getblock(c2, sz_number) + if not num1 or not num2 or num1 ~= num2 then + bork("bad number value"); return + end + elseif ctype == TSTRING then + local str1 = getstring(c1) + local str2 = getstring(c2) + if not str1 or not str2 or str1 ~= str2 then + bork("bad string value"); return + end + end + end + -- prototypes (compared recursively) + local nproto = goodint(c1, c2) + if not nproto then + bork("bad nproto"); return + end + for _ = 1, nproto do + if not getfunc(c1, c2) then + bork("bad function prototype"); return + end + end + -- debug information (ignored) + -- lineinfo (ignored) + local sizelineinfo1 = getint(c1) + if not sizelineinfo1 then + bork("bad sizelineinfo1"); return + end + local sizelineinfo2 = getint(c2) + if not sizelineinfo2 then + bork("bad sizelineinfo2"); return + end + if not getblock(c1, sizelineinfo1 * sz_int) then + bork("bad lineinfo1"); return + end + if not getblock(c2, sizelineinfo2 * sz_int) then + bork("bad lineinfo2"); return + end + -- locvars (ignored) + local sizelocvars1 = getint(c1) + if not sizelocvars1 then + bork("bad sizelocvars1"); return + end + local sizelocvars2 = getint(c2) + if not sizelocvars2 then + bork("bad sizelocvars2"); return + end + for _ = 1, sizelocvars1 do + if not getstring(c1) or not getint(c1) or not getint(c1) then + bork("bad locvars1"); return + end + end + for _ = 1, sizelocvars2 do + if not getstring(c2) or not getint(c2) or not getint(c2) then + bork("bad locvars2"); return + end + end + -- upvalues (ignored) + local sizeupvalues1 = getint(c1) + if not sizeupvalues1 then + bork("bad sizeupvalues1"); return + end + local sizeupvalues2 = getint(c2) + if not sizeupvalues2 then + bork("bad sizeupvalues2"); return + end + for _ = 1, sizeupvalues1 do + if not getstring(c1) then bork("bad upvalues1"); return end + end + for _ = 1, sizeupvalues2 do + if not getstring(c2) then bork("bad upvalues2"); return end + end + return true + end + + -- Removes shbang line so that load runs. + local function zap_shbang(s) + local shbang = s:match("^(#[^\r\n]*\r?\n?)") + if shbang then -- cut out shbang + s = sub(s, #shbang + 1) + end + return s + end + + -- Attempt to compile, then dump to get binary chunk string. + local cz = load(zap_shbang(z), "z") + if not cz then + bork("failed to compile original sources for binary chunk comparison") + return + end + + local cdat = load(zap_shbang(dat), "z") + if not cdat then + bork("failed to compile compressed result for binary chunk comparison") + end + + -- if load() works, dump assuming string.dump() is error-free + local c1 = { i = 1, dat = dump(cz) } + c1.len = #c1.dat + + local c2 = { i = 1, dat = dump(cdat) } + c2.len = #c2.dat + + -- Parse binary chunks to verify equivalence. + -- * For headers, handle sizes to allow a degree of flexibility. + -- * Assume a valid binary chunk is generated, since it was not + -- generated via external means. + if not (ensure(c1, 12) and ensure(c2, 12)) then + bork("header broken") + end + skip(c1, 6) -- skip signature(4), version, format + endian = getbyte(c1) -- 1 = little endian + sz_int = getbyte(c1) -- get data type sizes + sz_sizet = getbyte(c1) + sz_inst = getbyte(c1) + sz_number = getbyte(c1) + skip(c1) -- skip integral flag + skip(c2, 12) -- skip other header (assume similar) + + if endian == 1 then -- set for endian sensitive data we need + getint = getint_l + getsizet = getsizet_l + else + getint = getint_b + getsizet = getsizet_b + end + getfunc(c1, c2) -- get prototype at root + + if c1.i ~= c1.len + 1 then + bork("inconsistent binary chunk1"); return + elseif c2.i ~= c2.len + 1 then + bork("inconsistent binary chunk2"); return + end + + -- Successful comparison if end is reached with no borks. +end + +return M diff --git a/tools/luasrcdiet/fs.lua b/tools/luasrcdiet/fs.lua new file mode 100644 index 0000000..00baa11 --- /dev/null +++ b/tools/luasrcdiet/fs.lua @@ -0,0 +1,74 @@ +--------- +-- Utility functions for operations on a file system. +-- +-- **Note: This module is not part of public API!** +---- +local fmt = string.format +local open = io.open + +local UTF8_BOM = '\239\187\191' + +local function normalize_io_error (name, err) + if err:sub(1, #name + 2) == name..': ' then + err = err:sub(#name + 3) + end + return err +end + +local M = {} + +--- Reads the specified file and returns its content as string. +-- +-- @tparam string filename Path of the file to read. +-- @tparam string mode The mode in which to open the file, see @{io.open} (default: "r"). +-- @treturn[1] string A content of the file. +-- @treturn[2] nil +-- @treturn[2] string An error message. +function M.read_file (filename, mode) + local handler, err = open(filename, mode or 'r') + if not handler then + return nil, fmt('Could not open %s for reading: %s', + filename, normalize_io_error(filename, err)) + end + + local content, err = handler:read('*a') --luacheck: ignore 411 + if not content then + return nil, fmt('Could not read %s: %s', filename, normalize_io_error(filename, err)) + end + + handler:close() + + if content:sub(1, #UTF8_BOM) == UTF8_BOM then + content = content:sub(#UTF8_BOM + 1) + end + + return content +end + +--- Writes the given data to the specified file. +-- +-- @tparam string filename Path of the file to write. +-- @tparam string data The data to write. +-- @tparam ?string mode The mode in which to open the file, see @{io.open} (default: "w"). +-- @treturn[1] true +-- @treturn[2] nil +-- @treturn[2] string An error message. +function M.write_file (filename, data, mode) + local handler, err = open(filename, mode or 'w') + if not handler then + return nil, fmt('Could not open %s for writing: %s', + filename, normalize_io_error(filename, err)) + end + + local _, err = handler:write(data) --luacheck: ignore 411 + if err then + return nil, fmt('Could not write %s: %s', filename, normalize_io_error(filename, err)) + end + + handler:flush() + handler:close() + + return true +end + +return M diff --git a/tools/luasrcdiet/init.lua b/tools/luasrcdiet/init.lua new file mode 100644 index 0000000..348a979 --- /dev/null +++ b/tools/luasrcdiet/init.lua @@ -0,0 +1,117 @@ +--------- +-- LuaSrcDiet API +---- +local equiv = require 'luasrcdiet.equiv' +local llex = require 'luasrcdiet.llex' +local lparser = require 'luasrcdiet.lparser' +local optlex = require 'luasrcdiet.optlex' +local optparser = require 'luasrcdiet.optparser' +local utils = require 'luasrcdiet.utils' + +local concat = table.concat +local merge = utils.merge + +local _ -- placeholder + + +local function noop () + return +end + +local function opts_to_legacy (opts) + local res = {} + for key, val in pairs(opts) do + res['opt-'..key] = val + end + return res +end + + +local M = {} + +--- The module's name. +M._NAME = 'luasrcdiet' + +--- The module's version number. +M._VERSION = '1.0.0' + +--- The module's homepage. +M._HOMEPAGE = 'https://github.com/jirutka/luasrcdiet' + +--- All optimizations disabled. +M.NONE_OPTS = { + binequiv = false, + comments = false, + emptylines = false, + entropy = false, + eols = false, + experimental = false, + locals = false, + numbers = false, + srcequiv = false, + strings = false, + whitespace = false, +} + +--- Basic optimizations enabled. +-- @table BASIC_OPTS +M.BASIC_OPTS = merge(M.NONE_OPTS, { + comments = true, + emptylines = true, + srcequiv = true, + whitespace = true, +}) + +--- Defaults. +-- @table DEFAULT_OPTS +M.DEFAULT_OPTS = merge(M.BASIC_OPTS, { + locals = true, + numbers = true, +}) + +--- Maximum optimizations enabled (all except experimental). +-- @table MAXIMUM_OPTS +M.MAXIMUM_OPTS = merge(M.DEFAULT_OPTS, { + entropy = true, + eols = true, + strings = true, +}) + +--- Optimizes the given Lua source code. +-- +-- @tparam ?{[string]=bool,...} opts Optimizations to do (default is @{DEFAULT_OPTS}). +-- @tparam string source The Lua source code to optimize. +-- @treturn string Optimized source. +-- @raise if the source is malformed, source equivalence test failed, or some +-- other error occured. +function M.optimize (opts, source) + assert(source and type(source) == 'string', + 'bad argument #2: expected string, got a '..type(source)) + + opts = opts and merge(M.NONE_OPTS, opts) or M.DEFAULT_OPTS + local legacy_opts = opts_to_legacy(opts) + + local toklist, seminfolist, toklnlist = llex.lex(source) + local xinfo = lparser.parse(toklist, seminfolist, toklnlist) + + optparser.print = noop + optparser.optimize(legacy_opts, toklist, seminfolist, xinfo) + + local warn = optlex.warn -- use this as a general warning lookup + optlex.print = noop + _, seminfolist = optlex.optimize(legacy_opts, toklist, seminfolist, toklnlist) + local optim_source = concat(seminfolist) + + if opts.srcequiv and not opts.experimental then + equiv.init(legacy_opts, llex, warn) + equiv.source(source, optim_source) + + if warn.SRC_EQUIV then + error('Source equivalence test failed!') + end + end + + return optim_source +end + +return M diff --git a/tools/luasrcdiet/llex.lua b/tools/luasrcdiet/llex.lua new file mode 100644 index 0000000..c9d5a0e --- /dev/null +++ b/tools/luasrcdiet/llex.lua @@ -0,0 +1,350 @@ +--------- +-- Lua 5.1+ lexical analyzer written in Lua. +-- +-- This file is part of LuaSrcDiet, based on Yueliang material. +-- +-- **Notes:** +-- +-- * This is a version of the native 5.1.x lexer from Yueliang 0.4.0, +-- with significant modifications to handle LuaSrcDiet's needs: +-- (1) llex.error is an optional error function handler, +-- (2) seminfo for strings include their delimiters and no +-- translation operations are performed on them. +-- * ADDED shbang handling has been added to support executable scripts. +-- * NO localized decimal point replacement magic. +-- * NO limit to number of lines. +-- * NO support for compatible long strings (LUA\_COMPAT_LSTR). +-- * Added goto keyword and double-colon operator (Lua 5.2+). +---- +local find = string.find +local fmt = string.format +local match = string.match +local sub = string.sub +local tonumber = tonumber + +local M = {} + +local kw = {} +for v in ([[ +and break do else elseif end false for function goto if in +local nil not or repeat return then true until while]]):gmatch("%S+") do + kw[v] = true +end + +local z, -- source stream + sourceid, -- name of source + I, -- position of lexer + buff, -- buffer for strings + ln, -- line number + tok, -- lexed token list + seminfo, -- lexed semantic information list + tokln -- line numbers for messages + + +--- Adds information to token listing. +-- +-- @tparam string token +-- @tparam string info +local function addtoken(token, info) + local i = #tok + 1 + tok[i] = token + seminfo[i] = info + tokln[i] = ln +end + +--- Handles line number incrementation and end-of-line characters. +-- +-- @tparam int i Position of lexer in the source stream. +-- @tparam bool is_tok +-- @treturn int +local function inclinenumber(i, is_tok) + local old = sub(z, i, i) + i = i + 1 -- skip '\n' or '\r' + local c = sub(z, i, i) + if (c == "\n" or c == "\r") and (c ~= old) then + i = i + 1 -- skip '\n\r' or '\r\n' + old = old..c + end + if is_tok then addtoken("TK_EOL", old) end + ln = ln + 1 + I = i + return i +end + +--- Returns a chunk name or id, no truncation for long names. +-- +-- @treturn string +local function chunkid() + if sourceid and match(sourceid, "^[=@]") then + return sub(sourceid, 2) -- remove first char + end + return "[string]" +end + +--- Formats error message and throws error. +-- +-- A simplified version, does not report what token was responsible. +-- +-- @tparam string s +-- @tparam int line The line number. +-- @raise +local function errorline(s, line) + local e = M.error or error + e(fmt("%s:%d: %s", chunkid(), line or ln, s)) +end + +--- Counts separators (`="` in a long string delimiter. +-- +-- @tparam int i Position of lexer in the source stream. +-- @treturn int +local function skip_sep(i) + local s = sub(z, i, i) + i = i + 1 + local count = #match(z, "=*", i) + i = i + count + I = i + return (sub(z, i, i) == s) and count or (-count) - 1 +end + +--- Reads a long string or long comment. +-- +-- @tparam bool is_str +-- @tparam string sep +-- @treturn string +-- @raise if unfinished long string or comment. +local function read_long_string(is_str, sep) + local i = I + 1 -- skip 2nd '[' + local c = sub(z, i, i) + if c == "\r" or c == "\n" then -- string starts with a newline? + i = inclinenumber(i) -- skip it + end + while true do + local p, _, r = find(z, "([\r\n%]])", i) -- (long range match) + if not p then + errorline(is_str and "unfinished long string" or + "unfinished long comment") + end + i = p + if r == "]" then -- delimiter test + if skip_sep(i) == sep then + buff = sub(z, buff, I) + I = I + 1 -- skip 2nd ']' + return buff + end + i = I + else -- newline + buff = buff.."\n" + i = inclinenumber(i) + end + end--while +end + +--- Reads a string. +-- +-- @tparam string del The delimiter. +-- @treturn string +-- @raise if unfinished string or too large escape sequence. +local function read_string(del) + local i = I + while true do + local p, _, r = find(z, "([\n\r\\\"\'])", i) -- (long range match) + if p then + if r == "\n" or r == "\r" then + errorline("unfinished string") + end + i = p + if r == "\\" then -- handle escapes + i = i + 1 + r = sub(z, i, i) + if r == "" then break end -- (EOZ error) + p = find("abfnrtv\n\r", r, 1, true) + + if p then -- special escapes + if p > 7 then + i = inclinenumber(i) + else + i = i + 1 + end + + elseif find(r, "%D") then -- other non-digits + i = i + 1 + + else -- \xxx sequence + local _, q, s = find(z, "^(%d%d?%d?)", i) + i = q + 1 + if s + 1 > 256 then -- UCHAR_MAX + errorline("escape sequence too large") + end + + end--if p + else + i = i + 1 + if r == del then -- ending delimiter + I = i + return sub(z, buff, i - 1) -- return string + end + end--if r + else + break -- (error) + end--if p + end--while + errorline("unfinished string") +end + + +--- Initializes lexer for given source _z and source name _sourceid. +-- +-- @tparam string _z The source code. +-- @tparam string _sourceid Name of the source. +local function init(_z, _sourceid) + z = _z -- source + sourceid = _sourceid -- name of source + I = 1 -- lexer's position in source + ln = 1 -- line number + tok = {} -- lexed token list* + seminfo = {} -- lexed semantic information list* + tokln = {} -- line numbers for messages* + + -- Initial processing (shbang handling). + local p, _, q, r = find(z, "^(#[^\r\n]*)(\r?\n?)") + if p then -- skip first line + I = I + #q + addtoken("TK_COMMENT", q) + if #r > 0 then inclinenumber(I, true) end + end +end + +--- Runs lexer on the given source code. +-- +-- @tparam string source The Lua source to scan. +-- @tparam ?string source_name Name of the source (optional). +-- @treturn {string,...} A list of lexed tokens. +-- @treturn {string,...} A list of semantic information (lexed strings). +-- @treturn {int,...} A list of line numbers. +function M.lex(source, source_name) + init(source, source_name) + + while true do--outer + local i = I + -- inner loop allows break to be used to nicely section tests + while true do --luacheck: ignore 512 + + local p, _, r = find(z, "^([_%a][_%w]*)", i) + if p then + I = i + #r + if kw[r] then + addtoken("TK_KEYWORD", r) -- reserved word (keyword) + else + addtoken("TK_NAME", r) -- identifier + end + break -- (continue) + end + + local p, _, r = find(z, "^(%.?)%d", i) + if p then -- numeral + if r == "." then i = i + 1 end + local _, q, r = find(z, "^%d*[%.%d]*([eE]?)", i) --luacheck: ignore 421 + i = q + 1 + if #r == 1 then -- optional exponent + if match(z, "^[%+%-]", i) then -- optional sign + i = i + 1 + end + end + local _, q = find(z, "^[_%w]*", i) + I = q + 1 + local v = sub(z, p, q) -- string equivalent + if not tonumber(v) then -- handles hex test also + errorline("malformed number") + end + addtoken("TK_NUMBER", v) + break -- (continue) + end + + local p, q, r, t = find(z, "^((%s)[ \t\v\f]*)", i) + if p then + if t == "\n" or t == "\r" then -- newline + inclinenumber(i, true) + else + I = q + 1 -- whitespace + addtoken("TK_SPACE", r) + end + break -- (continue) + end + + local _, q = find(z, "^::", i) + if q then + I = q + 1 + addtoken("TK_OP", "::") + break -- (continue) + end + + local r = match(z, "^%p", i) + if r then + buff = i + local p = find("-[\"\'.=<>~", r, 1, true) --luacheck: ignore 421 + if p then + + -- two-level if block for punctuation/symbols + if p <= 2 then + if p == 1 then -- minus + local c = match(z, "^%-%-(%[?)", i) + if c then + i = i + 2 + local sep = -1 + if c == "[" then + sep = skip_sep(i) + end + if sep >= 0 then -- long comment + addtoken("TK_LCOMMENT", read_long_string(false, sep)) + else -- short comment + I = find(z, "[\n\r]", i) or (#z + 1) + addtoken("TK_COMMENT", sub(z, buff, I - 1)) + end + break -- (continue) + end + -- (fall through for "-") + else -- [ or long string + local sep = skip_sep(i) + if sep >= 0 then + addtoken("TK_LSTRING", read_long_string(true, sep)) + elseif sep == -1 then + addtoken("TK_OP", "[") + else + errorline("invalid long string delimiter") + end + break -- (continue) + end + + elseif p <= 5 then + if p < 5 then -- strings + I = i + 1 + addtoken("TK_STRING", read_string(r)) + break -- (continue) + end + r = match(z, "^%.%.?%.?", i) -- .|..|... dots + -- (fall through) + + else -- relational + r = match(z, "^%p=?", i) + -- (fall through) + end + end + I = i + #r + addtoken("TK_OP", r) -- for other symbols, fall through + break -- (continue) + end + + local r = sub(z, i, i) + if r ~= "" then + I = i + 1 + addtoken("TK_OP", r) -- other single-char tokens + break + end + addtoken("TK_EOS", "") -- end of stream, + return tok, seminfo, tokln -- exit here + + end--while inner + end--while outer +end + +return M diff --git a/tools/luasrcdiet/lparser.lua b/tools/luasrcdiet/lparser.lua new file mode 100644 index 0000000..334243e --- /dev/null +++ b/tools/luasrcdiet/lparser.lua @@ -0,0 +1,1286 @@ +--------- +-- Lua 5.1+ parser written in Lua. +-- +-- This file is part of LuaSrcDiet, based on Yueliang material. +-- +-- **Notes:** +-- +-- * This is a version of the native 5.1.x parser from Yueliang 0.4.0, +-- with significant modifications to handle LuaSrcDiet's needs: +-- (1) needs pre-built token tables instead of a module.method, +-- (2) lparser.error is an optional error handler (from llex), +-- (3) not full parsing, currently fakes raw/unlexed constants, +-- (4) parser() returns globalinfo, localinfo tables. +-- * NO support for 'arg' vararg functions (LUA_COMPAT_VARARG). +-- * A lot of the parser is unused, but might later be useful for +-- full-on parsing and analysis. +-- * Relaxed parsing of statement to not require "break" to be the +-- last statement of block (Lua 5.2+). +-- * Added basic support for goto and label statements, i.e. parser +-- does not crash on them (Lua 5.2+). +---- +local fmt = string.format +local gmatch = string.gmatch +local pairs = pairs + +local M = {} + +--[[-------------------------------------------------------------------- +-- variable and data structure initialization +----------------------------------------------------------------------]] + +---------------------------------------------------------------------- +-- initialization: main variables +---------------------------------------------------------------------- + +local toklist, -- grammar-only token tables (token table, + seminfolist, -- semantic information table, line number + toklnlist, -- table, cross-reference table) + xreflist, + tpos, -- token position + + line, -- start line # for error messages + lastln, -- last line # for ambiguous syntax chk + tok, seminfo, ln, xref, -- token, semantic info, line + nameref, -- proper position of token + fs, -- current function state + top_fs, -- top-level function state + + globalinfo, -- global variable information table + globallookup, -- global variable name lookup table + localinfo, -- local variable information table + ilocalinfo, -- inactive locals (prior to activation) + ilocalrefs, -- corresponding references to activate + statinfo -- statements labeled by type + +-- forward references for local functions +local explist1, expr, block, exp1, body, chunk + +---------------------------------------------------------------------- +-- initialization: data structures +---------------------------------------------------------------------- + +local block_follow = {} -- lookahead check in chunk(), returnstat() +for v in gmatch("else elseif end until ", "%S+") do + block_follow[v] = true +end + +local binopr_left = {} -- binary operators, left priority +local binopr_right = {} -- binary operators, right priority +for op, lt, rt in gmatch([[ +{+ 6 6}{- 6 6}{* 7 7}{/ 7 7}{% 7 7} +{^ 10 9}{.. 5 4} +{~= 3 3}{== 3 3} +{< 3 3}{<= 3 3}{> 3 3}{>= 3 3} +{and 2 2}{or 1 1} +]], "{(%S+)%s(%d+)%s(%d+)}") do + binopr_left[op] = lt + 0 + binopr_right[op] = rt + 0 +end + +local unopr = { ["not"] = true, ["-"] = true, + ["#"] = true, } -- unary operators +local UNARY_PRIORITY = 8 -- priority for unary operators + +--[[-------------------------------------------------------------------- +-- support functions +----------------------------------------------------------------------]] + +---------------------------------------------------------------------- +-- formats error message and throws error (duplicated from llex) +-- * a simplified version, does not report what token was responsible +---------------------------------------------------------------------- + +local function errorline(s, line) + local e = M.error or error + e(fmt("(source):%d: %s", line or ln, s)) +end + +---------------------------------------------------------------------- +-- handles incoming token, semantic information pairs +-- * NOTE: 'nextt' is named 'next' originally +---------------------------------------------------------------------- + +-- reads in next token +local function nextt() + lastln = toklnlist[tpos] + tok, seminfo, ln, xref + = toklist[tpos], seminfolist[tpos], toklnlist[tpos], xreflist[tpos] + tpos = tpos + 1 +end + +-- peek at next token (single lookahead for table constructor) +local function lookahead() + return toklist[tpos] +end + +---------------------------------------------------------------------- +-- throws a syntax error, or if token expected is not there +---------------------------------------------------------------------- + +local function syntaxerror(msg) + if tok ~= "" and tok ~= "" then + if tok == "" then tok = seminfo end + tok = "'"..tok.."'" + end + errorline(msg.." near "..tok) +end + +local function error_expected(token) + syntaxerror("'"..token.."' expected") +end + +---------------------------------------------------------------------- +-- tests for a token, returns outcome +-- * return value changed to boolean +---------------------------------------------------------------------- + +local function testnext(c) + if tok == c then nextt(); return true end +end + +---------------------------------------------------------------------- +-- check for existence of a token, throws error if not found +---------------------------------------------------------------------- + +local function check(c) + if tok ~= c then error_expected(c) end +end + +---------------------------------------------------------------------- +-- verify existence of a token, then skip it +---------------------------------------------------------------------- + +local function checknext(c) + check(c); nextt() +end + +---------------------------------------------------------------------- +-- throws error if condition not matched +---------------------------------------------------------------------- + +local function check_condition(c, msg) + if not c then syntaxerror(msg) end +end + +---------------------------------------------------------------------- +-- verifies token conditions are met or else throw error +---------------------------------------------------------------------- + +local function check_match(what, who, where) + if not testnext(what) then + if where == ln then + error_expected(what) + else + syntaxerror("'"..what.."' expected (to close '"..who.."' at line "..where..")") + end + end +end + +---------------------------------------------------------------------- +-- expect that token is a name, consume it and return the name +---------------------------------------------------------------------- + +local function str_checkname() + check("") + local ts = seminfo + nameref = xref + nextt() + return ts +end + +--[[-------------------------------------------------------------------- +-- variable (global|local|upvalue) handling +-- * to track locals and globals, variable management code needed +-- * entry point is singlevar() for variable lookups +-- * lookup tables (bl.locallist) are maintained awkwardly in the basic +-- block data structures, PLUS the function data structure (this is +-- an inelegant hack, since bl is nil for the top level of a function) +----------------------------------------------------------------------]] + +---------------------------------------------------------------------- +-- register a local variable, create local variable object, set in +-- to-activate variable list +-- * used in new_localvarliteral(), parlist(), fornum(), forlist(), +-- localfunc(), localstat() +---------------------------------------------------------------------- + +local function new_localvar(name, special) + local bl = fs.bl + local locallist + -- locate locallist in current block object or function root object + if bl then + locallist = bl.locallist + else + locallist = fs.locallist + end + -- build local variable information object and set localinfo + local id = #localinfo + 1 + localinfo[id] = { -- new local variable object + name = name, -- local variable name + xref = { nameref }, -- xref, first value is declaration + decl = nameref, -- location of declaration, = xref[1] + } + if special or name == "_ENV" then -- "self" and "_ENV" must be not be changed + localinfo[id].is_special = true + end + -- this can override a local with the same name in the same scope + -- but first, keep it inactive until it gets activated + local i = #ilocalinfo + 1 + ilocalinfo[i] = id + ilocalrefs[i] = locallist +end + +---------------------------------------------------------------------- +-- actually activate the variables so that they are visible +-- * remember Lua semantics, e.g. RHS is evaluated first, then LHS +-- * used in parlist(), forbody(), localfunc(), localstat(), body() +---------------------------------------------------------------------- + +local function adjustlocalvars(nvars) + local sz = #ilocalinfo + -- i goes from left to right, in order of local allocation, because + -- of something like: local a,a,a = 1,2,3 which gives a = 3 + while nvars > 0 do + nvars = nvars - 1 + local i = sz - nvars + local id = ilocalinfo[i] -- local's id + local obj = localinfo[id] + local name = obj.name -- name of local + obj.act = xref -- set activation location + ilocalinfo[i] = nil + local locallist = ilocalrefs[i] -- ref to lookup table to update + ilocalrefs[i] = nil + local existing = locallist[name] -- if existing, remove old first! + if existing then -- do not overlap, set special + obj = localinfo[existing] -- form of rem, as -id + obj.rem = -id + end + locallist[name] = id -- activate, now visible to Lua + end +end + +---------------------------------------------------------------------- +-- remove (deactivate) variables in current scope (before scope exits) +-- * zap entire locallist tables since we are not allocating registers +-- * used in leaveblock(), close_func() +---------------------------------------------------------------------- + +local function removevars() + local bl = fs.bl + local locallist + -- locate locallist in current block object or function root object + if bl then + locallist = bl.locallist + else + locallist = fs.locallist + end + -- enumerate the local list at current scope and deactivate 'em + for _, id in pairs(locallist) do + local obj = localinfo[id] + obj.rem = xref -- set deactivation location + end +end + +---------------------------------------------------------------------- +-- creates a new local variable given a name +-- * skips internal locals (those starting with '('), so internal +-- locals never needs a corresponding adjustlocalvars() call +-- * special is true for "self" which must not be optimized +-- * used in fornum(), forlist(), parlist(), body() +---------------------------------------------------------------------- + +local function new_localvarliteral(name, special) + if name:sub(1, 1) == "(" then -- can skip internal locals + return + end + new_localvar(name, special) +end + +---------------------------------------------------------------------- +-- search the local variable namespace of the given fs for a match +-- * returns localinfo index +-- * used only in singlevaraux() +---------------------------------------------------------------------- + +local function searchvar(fs, n) + local bl = fs.bl + local locallist + if bl then + locallist = bl.locallist + while locallist do + if locallist[n] then return locallist[n] end -- found + bl = bl.prev + locallist = bl and bl.locallist + end + end + locallist = fs.locallist + return locallist[n] or -1 -- found or not found (-1) +end + +---------------------------------------------------------------------- +-- handle locals, globals and upvalues and related processing +-- * search mechanism is recursive, calls itself to search parents +-- * used only in singlevar() +---------------------------------------------------------------------- + +local function singlevaraux(fs, n, var) + if fs == nil then -- no more levels? + var.k = "VGLOBAL" -- default is global variable + return "VGLOBAL" + else + local v = searchvar(fs, n) -- look up at current level + if v >= 0 then + var.k = "VLOCAL" + var.id = v + -- codegen may need to deal with upvalue here + return "VLOCAL" + else -- not found at current level; try upper one + if singlevaraux(fs.prev, n, var) == "VGLOBAL" then + return "VGLOBAL" + end + -- else was LOCAL or UPVAL, handle here + var.k = "VUPVAL" -- upvalue in this level + return "VUPVAL" + end--if v + end--if fs +end + +---------------------------------------------------------------------- +-- consume a name token, creates a variable (global|local|upvalue) +-- * used in prefixexp(), funcname() +---------------------------------------------------------------------- + +local function singlevar(v) + local name = str_checkname() + singlevaraux(fs, name, v) + ------------------------------------------------------------------ + -- variable tracking + ------------------------------------------------------------------ + if v.k == "VGLOBAL" then + -- if global being accessed, keep track of it by creating an object + local id = globallookup[name] + if not id then + id = #globalinfo + 1 + globalinfo[id] = { -- new global variable object + name = name, -- global variable name + xref = { nameref }, -- xref, first value is declaration + } + globallookup[name] = id -- remember it + else + local obj = globalinfo[id].xref + obj[#obj + 1] = nameref -- add xref + end + else + -- local/upvalue is being accessed, keep track of it + local obj = localinfo[v.id].xref + obj[#obj + 1] = nameref -- add xref + end +end + +--[[-------------------------------------------------------------------- +-- state management functions with open/close pairs +----------------------------------------------------------------------]] + +---------------------------------------------------------------------- +-- enters a code unit, initializes elements +---------------------------------------------------------------------- + +local function enterblock(isbreakable) + local bl = {} -- per-block state + bl.isbreakable = isbreakable + bl.prev = fs.bl + bl.locallist = {} + fs.bl = bl +end + +---------------------------------------------------------------------- +-- leaves a code unit, close any upvalues +---------------------------------------------------------------------- + +local function leaveblock() + local bl = fs.bl + removevars() + fs.bl = bl.prev +end + +---------------------------------------------------------------------- +-- opening of a function +-- * top_fs is only for anchoring the top fs, so that parser() can +-- return it to the caller function along with useful output +-- * used in parser() and body() +---------------------------------------------------------------------- + +local function open_func() + local new_fs -- per-function state + if not fs then -- top_fs is created early + new_fs = top_fs + else + new_fs = {} + end + new_fs.prev = fs -- linked list of function states + new_fs.bl = nil + new_fs.locallist = {} + fs = new_fs +end + +---------------------------------------------------------------------- +-- closing of a function +-- * used in parser() and body() +---------------------------------------------------------------------- + +local function close_func() + removevars() + fs = fs.prev +end + +--[[-------------------------------------------------------------------- +-- other parsing functions +-- * for table constructor, parameter list, argument list +----------------------------------------------------------------------]] + +---------------------------------------------------------------------- +-- parse a function name suffix, for function call specifications +-- * used in primaryexp(), funcname() +---------------------------------------------------------------------- + +local function field(v) + -- field -> ['.' | ':'] NAME + nextt() -- skip the dot or colon + str_checkname() + v.k = "VINDEXED" +end + +---------------------------------------------------------------------- +-- parse a table indexing suffix, for constructors, expressions +-- * used in recfield(), primaryexp() +---------------------------------------------------------------------- + +local function yindex() + -- index -> '[' expr ']' + nextt() -- skip the '[' + expr({}) + checknext("]") +end + +---------------------------------------------------------------------- +-- parse a table record (hash) field +-- * used in constructor() +---------------------------------------------------------------------- + +local function recfield() + -- recfield -> (NAME | '['exp1']') = exp1 + if tok == "" then + str_checkname() + else-- tok == '[' + yindex() + end + checknext("=") + expr({}) +end + +---------------------------------------------------------------------- +-- parse a table list (array) field +-- * used in constructor() +---------------------------------------------------------------------- + +local function listfield(cc) + expr(cc.v) +end + +---------------------------------------------------------------------- +-- parse a table constructor +-- * used in funcargs(), simpleexp() +---------------------------------------------------------------------- + +local function constructor(t) + -- constructor -> '{' [ field { fieldsep field } [ fieldsep ] ] '}' + -- field -> recfield | listfield + -- fieldsep -> ',' | ';' + local line = ln + local cc = { + v = { k = "VVOID" }, + } + t.k = "VRELOCABLE" + checknext("{") + repeat + if tok == "}" then break end + -- closelistfield(cc) here + local c = tok + if c == "" then -- may be listfields or recfields + if lookahead() ~= "=" then -- look ahead: expression? + listfield(cc) + else + recfield() + end + elseif c == "[" then -- constructor_item -> recfield + recfield() + else -- constructor_part -> listfield + listfield(cc) + end + until not testnext(",") and not testnext(";") + check_match("}", "{", line) + -- lastlistfield(cc) here +end + +---------------------------------------------------------------------- +-- parse the arguments (parameters) of a function declaration +-- * used in body() +---------------------------------------------------------------------- + +local function parlist() + -- parlist -> [ param { ',' param } ] + local nparams = 0 + if tok ~= ")" then -- is 'parlist' not empty? + repeat + local c = tok + if c == "" then -- param -> NAME + new_localvar(str_checkname()) + nparams = nparams + 1 + elseif c == "..." then + nextt() + fs.is_vararg = true + else + syntaxerror(" or '...' expected") + end + until fs.is_vararg or not testnext(",") + end--if + adjustlocalvars(nparams) +end + +---------------------------------------------------------------------- +-- parse the parameters of a function call +-- * contrast with parlist(), used in function declarations +-- * used in primaryexp() +---------------------------------------------------------------------- + +local function funcargs(f) + local line = ln + local c = tok + if c == "(" then -- funcargs -> '(' [ explist1 ] ')' + if line ~= lastln then + syntaxerror("ambiguous syntax (function call x new statement)") + end + nextt() + if tok ~= ")" then -- arg list is not empty? + explist1() + end + check_match(")", "(", line) + elseif c == "{" then -- funcargs -> constructor + constructor({}) + elseif c == "" then -- funcargs -> STRING + nextt() -- must use 'seminfo' before 'next' + else + syntaxerror("function arguments expected") + return + end--if c + f.k = "VCALL" +end + +--[[-------------------------------------------------------------------- +-- mostly expression functions +----------------------------------------------------------------------]] + +---------------------------------------------------------------------- +-- parses an expression in parentheses or a single variable +-- * used in primaryexp() +---------------------------------------------------------------------- + +local function prefixexp(v) + -- prefixexp -> NAME | '(' expr ')' + local c = tok + if c == "(" then + local line = ln + nextt() + expr(v) + check_match(")", "(", line) + elseif c == "" then + singlevar(v) + else + syntaxerror("unexpected symbol") + end--if c +end + +---------------------------------------------------------------------- +-- parses a prefixexp (an expression in parentheses or a single +-- variable) or a function call specification +-- * used in simpleexp(), assignment(), expr_stat() +---------------------------------------------------------------------- + +local function primaryexp(v) + -- primaryexp -> + -- prefixexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } + prefixexp(v) + while true do + local c = tok + if c == "." then -- field + field(v) + elseif c == "[" then -- '[' exp1 ']' + yindex() + elseif c == ":" then -- ':' NAME funcargs + nextt() + str_checkname() + funcargs(v) + elseif c == "(" or c == "" or c == "{" then -- funcargs + funcargs(v) + else + return + end--if c + end--while +end + +---------------------------------------------------------------------- +-- parses general expression types, constants handled here +-- * used in subexpr() +---------------------------------------------------------------------- + +local function simpleexp(v) + -- simpleexp -> NUMBER | STRING | NIL | TRUE | FALSE | ... | + -- constructor | FUNCTION body | primaryexp + local c = tok + if c == "" then + v.k = "VKNUM" + elseif c == "" then + v.k = "VK" + elseif c == "nil" then + v.k = "VNIL" + elseif c == "true" then + v.k = "VTRUE" + elseif c == "false" then + v.k = "VFALSE" + elseif c == "..." then -- vararg + check_condition(fs.is_vararg == true, + "cannot use '...' outside a vararg function"); + v.k = "VVARARG" + elseif c == "{" then -- constructor + constructor(v) + return + elseif c == "function" then + nextt() + body(false, ln) + return + else + primaryexp(v) + return + end--if c + nextt() +end + +------------------------------------------------------------------------ +-- Parse subexpressions. Includes handling of unary operators and binary +-- operators. A subexpr is given the rhs priority level of the operator +-- immediately left of it, if any (limit is -1 if none,) and if a binop +-- is found, limit is compared with the lhs priority level of the binop +-- in order to determine which executes first. +-- * recursively called +-- * used in expr() +------------------------------------------------------------------------ + +local function subexpr(v, limit) + -- subexpr -> (simpleexp | unop subexpr) { binop subexpr } + -- * where 'binop' is any binary operator with a priority + -- higher than 'limit' + local op = tok + local uop = unopr[op] + if uop then + nextt() + subexpr(v, UNARY_PRIORITY) + else + simpleexp(v) + end + -- expand while operators have priorities higher than 'limit' + op = tok + local binop = binopr_left[op] + while binop and binop > limit do + nextt() + -- read sub-expression with higher priority + op = subexpr({}, binopr_right[op]) -- next operator + binop = binopr_left[op] + end + return op -- return first untreated operator +end + +---------------------------------------------------------------------- +-- Expression parsing starts here. Function subexpr is entered with the +-- left operator (which is non-existent) priority of -1, which is lower +-- than all actual operators. Expr information is returned in parm v. +-- * used in cond(), explist1(), index(), recfield(), listfield(), +-- prefixexp(), while_stat(), exp1() +---------------------------------------------------------------------- + +-- this is a forward-referenced local +function expr(v) + -- expr -> subexpr + subexpr(v, 0) +end + +--[[-------------------------------------------------------------------- +-- third level parsing functions +----------------------------------------------------------------------]] + +------------------------------------------------------------------------ +-- parse a variable assignment sequence +-- * recursively called +-- * used in expr_stat() +------------------------------------------------------------------------ + +local function assignment(v) + local c = v.v.k + check_condition(c == "VLOCAL" or c == "VUPVAL" or c == "VGLOBAL" + or c == "VINDEXED", "syntax error") + if testnext(",") then -- assignment -> ',' primaryexp assignment + local nv = {} -- expdesc + nv.v = {} + primaryexp(nv.v) + -- lparser.c deals with some register usage conflict here + assignment(nv) + else -- assignment -> '=' explist1 + checknext("=") + explist1() + return -- avoid default + end +end + +---------------------------------------------------------------------- +-- parse a for loop body for both versions of the for loop +-- * used in fornum(), forlist() +---------------------------------------------------------------------- + +local function forbody(nvars) + -- forbody -> DO block + checknext("do") + enterblock(false) -- scope for declared variables + adjustlocalvars(nvars) + block() + leaveblock() -- end of scope for declared variables +end + +---------------------------------------------------------------------- +-- parse a numerical for loop, calls forbody() +-- * used in for_stat() +---------------------------------------------------------------------- + +local function fornum(varname) + -- fornum -> NAME = exp1, exp1 [, exp1] DO body + new_localvarliteral("(for index)") + new_localvarliteral("(for limit)") + new_localvarliteral("(for step)") + new_localvar(varname) + checknext("=") + exp1() -- initial value + checknext(",") + exp1() -- limit + if testnext(",") then + exp1() -- optional step + else + -- default step = 1 + end + forbody(1) +end + +---------------------------------------------------------------------- +-- parse a generic for loop, calls forbody() +-- * used in for_stat() +---------------------------------------------------------------------- + +local function forlist(indexname) + -- forlist -> NAME {, NAME} IN explist1 DO body + -- create control variables + new_localvarliteral("(for generator)") + new_localvarliteral("(for state)") + new_localvarliteral("(for control)") + -- create declared variables + new_localvar(indexname) + local nvars = 1 + while testnext(",") do + new_localvar(str_checkname()) + nvars = nvars + 1 + end + checknext("in") + explist1() + forbody(nvars) +end + +---------------------------------------------------------------------- +-- parse a function name specification +-- * used in func_stat() +---------------------------------------------------------------------- + +local function funcname(v) + -- funcname -> NAME {field} [':' NAME] + local needself = false + singlevar(v) + while tok == "." do + field(v) + end + if tok == ":" then + needself = true + field(v) + end + return needself +end + +---------------------------------------------------------------------- +-- parse the single expressions needed in numerical for loops +-- * used in fornum() +---------------------------------------------------------------------- + +-- this is a forward-referenced local +function exp1() + -- exp1 -> expr + expr({}) +end + +---------------------------------------------------------------------- +-- parse condition in a repeat statement or an if control structure +-- * used in repeat_stat(), test_then_block() +---------------------------------------------------------------------- + +local function cond() + -- cond -> expr + expr({}) -- read condition +end + +---------------------------------------------------------------------- +-- parse part of an if control structure, including the condition +-- * used in if_stat() +---------------------------------------------------------------------- + +local function test_then_block() + -- test_then_block -> [IF | ELSEIF] cond THEN block + nextt() -- skip IF or ELSEIF + cond() + checknext("then") + block() -- 'then' part +end + +---------------------------------------------------------------------- +-- parse a local function statement +-- * used in local_stat() +---------------------------------------------------------------------- + +local function localfunc() + -- localfunc -> NAME body + new_localvar(str_checkname()) + adjustlocalvars(1) + body(false, ln) +end + +---------------------------------------------------------------------- +-- parse a local variable declaration statement +-- * used in local_stat() +---------------------------------------------------------------------- + +local function localstat() + -- localstat -> NAME {',' NAME} ['=' explist1] + local nvars = 0 + repeat + new_localvar(str_checkname()) + nvars = nvars + 1 + until not testnext(",") + if testnext("=") then + explist1() + else + -- VVOID + end + adjustlocalvars(nvars) +end + +---------------------------------------------------------------------- +-- parse a list of comma-separated expressions +-- * used in return_stat(), localstat(), funcargs(), assignment(), +-- forlist() +---------------------------------------------------------------------- + +-- this is a forward-referenced local +function explist1() + -- explist1 -> expr { ',' expr } + local e = {} + expr(e) + while testnext(",") do + expr(e) + end +end + +---------------------------------------------------------------------- +-- parse function declaration body +-- * used in simpleexp(), localfunc(), func_stat() +---------------------------------------------------------------------- + +-- this is a forward-referenced local +function body(needself, line) + -- body -> '(' parlist ')' chunk END + open_func() + checknext("(") + if needself then + new_localvarliteral("self", true) + adjustlocalvars(1) + end + parlist() + checknext(")") + chunk() + check_match("end", "function", line) + close_func() +end + +---------------------------------------------------------------------- +-- parse a code block or unit +-- * used in do_stat(), while_stat(), forbody(), test_then_block(), +-- if_stat() +---------------------------------------------------------------------- + +-- this is a forward-referenced local +function block() + -- block -> chunk + enterblock(false) + chunk() + leaveblock() +end + +--[[-------------------------------------------------------------------- +-- second level parsing functions, all with '_stat' suffix +-- * since they are called via a table lookup, they cannot be local +-- functions (a lookup table of local functions might be smaller...) +-- * stat() -> *_stat() +----------------------------------------------------------------------]] + +---------------------------------------------------------------------- +-- initial parsing for a for loop, calls fornum() or forlist() +-- * removed 'line' parameter (used to set debug information only) +-- * used in stat() +---------------------------------------------------------------------- + +local function for_stat() + -- stat -> for_stat -> FOR (fornum | forlist) END + local line = line + enterblock(true) -- scope for loop and control variables + nextt() -- skip 'for' + local varname = str_checkname() -- first variable name + local c = tok + if c == "=" then + fornum(varname) + elseif c == "," or c == "in" then + forlist(varname) + else + syntaxerror("'=' or 'in' expected") + end + check_match("end", "for", line) + leaveblock() -- loop scope (`break' jumps to this point) +end + +---------------------------------------------------------------------- +-- parse a while-do control structure, body processed by block() +-- * used in stat() +---------------------------------------------------------------------- + +local function while_stat() + -- stat -> while_stat -> WHILE cond DO block END + local line = line + nextt() -- skip WHILE + cond() -- parse condition + enterblock(true) + checknext("do") + block() + check_match("end", "while", line) + leaveblock() +end + +---------------------------------------------------------------------- +-- parse a repeat-until control structure, body parsed by chunk() +-- * originally, repeatstat() calls breakstat() too if there is an +-- upvalue in the scope block; nothing is actually lexed, it is +-- actually the common code in breakstat() for closing of upvalues +-- * used in stat() +---------------------------------------------------------------------- + +local function repeat_stat() + -- stat -> repeat_stat -> REPEAT block UNTIL cond + local line = line + enterblock(true) -- loop block + enterblock(false) -- scope block + nextt() -- skip REPEAT + chunk() + check_match("until", "repeat", line) + cond() + -- close upvalues at scope level below + leaveblock() -- finish scope + leaveblock() -- finish loop +end + +---------------------------------------------------------------------- +-- parse an if control structure +-- * used in stat() +---------------------------------------------------------------------- + +local function if_stat() + -- stat -> if_stat -> IF cond THEN block + -- {ELSEIF cond THEN block} [ELSE block] END + local line = line + test_then_block() -- IF cond THEN block + while tok == "elseif" do + test_then_block() -- ELSEIF cond THEN block + end + if tok == "else" then + nextt() -- skip ELSE + block() -- 'else' part + end + check_match("end", "if", line) +end + +---------------------------------------------------------------------- +-- parse a return statement +-- * used in stat() +---------------------------------------------------------------------- + +local function return_stat() + -- stat -> return_stat -> RETURN explist + nextt() -- skip RETURN + local c = tok + if block_follow[c] or c == ";" then + -- return no values + else + explist1() -- optional return values + end +end + +---------------------------------------------------------------------- +-- parse a break statement +-- * used in stat() +---------------------------------------------------------------------- + +local function break_stat() + -- stat -> break_stat -> BREAK + local bl = fs.bl + nextt() -- skip BREAK + while bl and not bl.isbreakable do -- find a breakable block + bl = bl.prev + end + if not bl then + syntaxerror("no loop to break") + end +end + +---------------------------------------------------------------------- +-- parse a label statement +-- * this function has been added later, it just parses label statement +-- without any validation! +-- * used in stat() +---------------------------------------------------------------------- + +local function label_stat() + -- stat -> label_stat -> '::' NAME '::' + nextt() -- skip '::' + str_checkname() + checknext("::") +end + +---------------------------------------------------------------------- +-- parse a goto statement +-- * this function has been added later, it just parses goto statement +-- without any validation! +-- * used in stat() +---------------------------------------------------------------------- + +local function goto_stat() + -- stat -> goto_stat -> GOTO NAME + nextt() -- skip GOTO + str_checkname() +end + +---------------------------------------------------------------------- +-- parse a function call with no returns or an assignment statement +-- * the struct with .prev is used for name searching in lparse.c, +-- so it is retained for now; present in assignment() also +-- * used in stat() +---------------------------------------------------------------------- + +local function expr_stat() + local id = tpos - 1 + -- stat -> expr_stat -> func | assignment + local v = { v = {} } + primaryexp(v.v) + if v.v.k == "VCALL" then -- stat -> func + -- call statement uses no results + statinfo[id] = "call" + else -- stat -> assignment + v.prev = nil + assignment(v) + statinfo[id] = "assign" + end +end + +---------------------------------------------------------------------- +-- parse a function statement +-- * used in stat() +---------------------------------------------------------------------- + +local function function_stat() + -- stat -> function_stat -> FUNCTION funcname body + local line = line + nextt() -- skip FUNCTION + local needself = funcname({}) + body(needself, line) +end + +---------------------------------------------------------------------- +-- parse a simple block enclosed by a DO..END pair +-- * used in stat() +---------------------------------------------------------------------- + +local function do_stat() + -- stat -> do_stat -> DO block END + local line = line + nextt() -- skip DO + block() + check_match("end", "do", line) +end + +---------------------------------------------------------------------- +-- parse a statement starting with LOCAL +-- * used in stat() +---------------------------------------------------------------------- + +local function local_stat() + -- stat -> local_stat -> LOCAL FUNCTION localfunc + -- -> LOCAL localstat + nextt() -- skip LOCAL + if testnext("function") then -- local function? + localfunc() + else + localstat() + end +end + +--[[-------------------------------------------------------------------- +-- main functions, top level parsing functions +-- * accessible functions are: init(lexer), parser() +-- * [entry] -> parser() -> chunk() -> stat() +----------------------------------------------------------------------]] + +---------------------------------------------------------------------- +-- initial parsing for statements, calls '_stat' suffixed functions +-- * used in chunk() +---------------------------------------------------------------------- + +local stat_call = { -- lookup for calls in stat() + ["if"] = if_stat, + ["while"] = while_stat, + ["do"] = do_stat, + ["for"] = for_stat, + ["repeat"] = repeat_stat, + ["function"] = function_stat, + ["local"] = local_stat, + ["return"] = return_stat, + ["break"] = break_stat, + ["goto"] = goto_stat, + ["::"] = label_stat, +} + +local function stat() + -- stat -> if_stat while_stat do_stat for_stat repeat_stat + -- function_stat local_stat return_stat break_stat + -- expr_stat + line = ln -- may be needed for error messages + local c = tok + local fn = stat_call[c] + -- handles: if while do for repeat function local return break + if fn then + statinfo[tpos - 1] = c + fn() + -- return must be last statement + if c == "return" then return true end + else + expr_stat() + end + return false +end + +---------------------------------------------------------------------- +-- parse a chunk, which consists of a bunch of statements +-- * used in parser(), body(), block(), repeat_stat() +---------------------------------------------------------------------- + +-- this is a forward-referenced local +function chunk() + -- chunk -> { stat [';'] } + local islast = false + while not islast and not block_follow[tok] do + islast = stat() + testnext(";") + end +end + +---------------------------------------------------------------------- +-- initialization function +---------------------------------------------------------------------- + +local function init(tokorig, seminfoorig, toklnorig) + tpos = 1 -- token position + top_fs = {} -- reset top level function state + ------------------------------------------------------------------ + -- set up grammar-only token tables; impedance-matching... + -- note that constants returned by the lexer is source-level, so + -- for now, fake(!) constant tokens (TK_NUMBER|TK_STRING|TK_LSTRING) + ------------------------------------------------------------------ + local j = 1 + toklist, seminfolist, toklnlist, xreflist = {}, {}, {}, {} + for i = 1, #tokorig do + local tok = tokorig[i] + local yep = true + if tok == "TK_KEYWORD" or tok == "TK_OP" then + tok = seminfoorig[i] + elseif tok == "TK_NAME" then + tok = "" + seminfolist[j] = seminfoorig[i] + elseif tok == "TK_NUMBER" then + tok = "" + seminfolist[j] = 0 -- fake! + elseif tok == "TK_STRING" or tok == "TK_LSTRING" then + tok = "" + seminfolist[j] = "" -- fake! + elseif tok == "TK_EOS" then + tok = "" + else + -- non-grammar tokens; ignore them + yep = false + end + if yep then -- set rest of the information + toklist[j] = tok + toklnlist[j] = toklnorig[i] + xreflist[j] = i + j = j + 1 + end + end--for + ------------------------------------------------------------------ + -- initialize data structures for variable tracking + ------------------------------------------------------------------ + globalinfo, globallookup, localinfo = {}, {}, {} + ilocalinfo, ilocalrefs = {}, {} + statinfo = {} -- experimental +end + +---------------------------------------------------------------------- +-- performs parsing, returns parsed data structure +---------------------------------------------------------------------- + +function M.parse(tokens, seminfo, tokens_ln) + init(tokens, seminfo, tokens_ln) + + open_func() + fs.is_vararg = true -- main func. is always vararg + nextt() -- read first token + chunk() + check("") + close_func() + return { -- return everything + globalinfo = globalinfo, + localinfo = localinfo, + statinfo = statinfo, + toklist = toklist, + seminfolist = seminfolist, + toklnlist = toklnlist, + xreflist = xreflist, + } +end + +return M diff --git a/tools/luasrcdiet/optlex.lua b/tools/luasrcdiet/optlex.lua new file mode 100644 index 0000000..3bbd8e2 --- /dev/null +++ b/tools/luasrcdiet/optlex.lua @@ -0,0 +1,852 @@ +--------- +-- This module does lexer-based optimizations. +-- +-- **Notes:** +-- +-- * TODO: General string delimiter conversion optimizer. +-- * TODO: (numbers) warn if overly significant digit. +---- +local char = string.char +local find = string.find +local match = string.match +local rep = string.rep +local sub = string.sub +local tonumber = tonumber +local tostring = tostring + +local print -- set in optimize() + +local M = {} + +-- error function, can override by setting own function into module +M.error = error + +M.warn = {} -- table for warning flags + +local stoks, sinfos, stoklns -- source lists + +local is_realtoken = { -- significant (grammar) tokens + TK_KEYWORD = true, + TK_NAME = true, + TK_NUMBER = true, + TK_STRING = true, + TK_LSTRING = true, + TK_OP = true, + TK_EOS = true, +} +local is_faketoken = { -- whitespace (non-grammar) tokens + TK_COMMENT = true, + TK_LCOMMENT = true, + TK_EOL = true, + TK_SPACE = true, +} + +local opt_details -- for extra information + +--- Returns true if current token is at the start of a line. +-- +-- It skips over deleted tokens via recursion. +-- +-- @tparam int i +-- @treturn bool +local function atlinestart(i) + local tok = stoks[i - 1] + if i <= 1 or tok == "TK_EOL" then + return true + elseif tok == "" then + return atlinestart(i - 1) + end + return false +end + +--- Returns true if current token is at the end of a line. +-- +-- It skips over deleted tokens via recursion. +-- +-- @tparam int i +-- @treturn bool +local function atlineend(i) + local tok = stoks[i + 1] + if i >= #stoks or tok == "TK_EOL" or tok == "TK_EOS" then + return true + elseif tok == "" then + return atlineend(i + 1) + end + return false +end + +--- Counts comment EOLs inside a long comment. +-- +-- In order to keep line numbering, EOLs need to be reinserted. +-- +-- @tparam string lcomment +-- @treturn int +local function commenteols(lcomment) + local sep = #match(lcomment, "^%-%-%[=*%[") + local z = sub(lcomment, sep + 1, -(sep - 1)) -- remove delims + local i, c = 1, 0 + while true do + local p, _, r, s = find(z, "([\r\n])([\r\n]?)", i) + if not p then break end -- if no matches, done + i = p + 1 + c = c + 1 + if #s > 0 and r ~= s then -- skip CRLF or LFCR + i = i + 1 + end + end + return c +end + +--- Compares two tokens (i, j) and returns the whitespace required. +-- +-- See documentation for a reference table of interactions. +-- +-- Only two grammar/real tokens are being considered: +-- +-- * if `""`, no separation is needed, +-- * if `" "`, then at least one whitespace (or EOL) is required. +-- +-- Note: This doesn't work at the start or the end or for EOS! +-- +-- @tparam int i +-- @tparam int j +-- @treturn string +local function checkpair(i, j) + local t1, t2 = stoks[i], stoks[j] + + if t1 == "TK_STRING" or t1 == "TK_LSTRING" or + t2 == "TK_STRING" or t2 == "TK_LSTRING" then + return "" + + elseif t1 == "TK_OP" or t2 == "TK_OP" then + if (t1 == "TK_OP" and (t2 == "TK_KEYWORD" or t2 == "TK_NAME")) or + (t2 == "TK_OP" and (t1 == "TK_KEYWORD" or t1 == "TK_NAME")) then + return "" + end + if t1 == "TK_OP" and t2 == "TK_OP" then + -- for TK_OP/TK_OP pairs, see notes in technotes.txt + local op, op2 = sinfos[i], sinfos[j] + if (match(op, "^%.%.?$") and match(op2, "^%.")) or + (match(op, "^[~=<>]$") and op2 == "=") or + (op == "[" and (op2 == "[" or op2 == "=")) then + return " " + end + return "" + end + -- "TK_OP" + "TK_NUMBER" case + local op = sinfos[i] + if t2 == "TK_OP" then op = sinfos[j] end + if match(op, "^%.%.?%.?$") then + return " " + end + return "" + + else-- "TK_KEYWORD" | "TK_NAME" | "TK_NUMBER" then + return " " + + end +end + +--- Repack tokens, removing deletions caused by optimization process. +local function repack_tokens() + local dtoks, dinfos, dtoklns = {}, {}, {} + local j = 1 + for i = 1, #stoks do + local tok = stoks[i] + if tok ~= "" then + dtoks[j], dinfos[j], dtoklns[j] = tok, sinfos[i], stoklns[i] + j = j + 1 + end + end + stoks, sinfos, stoklns = dtoks, dinfos, dtoklns +end + +--- Does number optimization. +-- +-- Optimization using string formatting functions is one way of doing this, +-- but here, we consider all cases and handle them separately (possibly an +-- idiotic approach...). +-- +-- Scientific notation being generated is not in canonical form, this may or +-- may not be a bad thing. +-- +-- Note: Intermediate portions need to fit into a normal number range. +-- +-- Optimizations can be divided based on number patterns: +-- +-- * hexadecimal: +-- (1) no need to remove leading zeros, just skip to (2) +-- (2) convert to integer if size equal or smaller +-- * change if equal size -> lose the 'x' to reduce entropy +-- (3) number is then processed as an integer +-- (4) note: does not make 0[xX] consistent +-- * integer: +-- (1) reduce useless fractional part, if present, e.g. 123.000 -> 123. +-- (2) remove leading zeros, e.g. 000123 +-- * float: +-- (1) split into digits dot digits +-- (2) if no integer portion, take as zero (can omit later) +-- (3) handle degenerate .000 case, after which the fractional part +-- must be non-zero (if zero, it's matched as float .0) +-- (4) remove trailing zeros for fractional portion +-- (5) p.q where p > 0 and q > 0 cannot be shortened any more +-- (6) otherwise p == 0 and the form is .q, e.g. .000123 +-- (7) if scientific shorter, convert, e.g. .000123 -> 123e-6 +-- * scientific: +-- (1) split into (digits dot digits) [eE] ([+-] digits) +-- (2) if significand is zero, just use .0 +-- (3) remove leading zeros for significand +-- (4) shift out trailing zeros for significand +-- (5) examine exponent and determine which format is best: +-- number with fraction, or scientific +-- +-- Note: Number with fraction and scientific number is never converted +-- to integer, because Lua 5.3 distinguishes between integers and floats. +-- +-- +-- @tparam int i +local function do_number(i) + local before = sinfos[i] -- 'before' + local z = before -- working representation + local y -- 'after', if better + -------------------------------------------------------------------- + if match(z, "^0[xX]") then -- hexadecimal number + local v = tostring(tonumber(z)) + if #v <= #z then + z = v -- change to integer, AND continue + else + return -- no change; stick to hex + end + end + + if match(z, "^%d+$") then -- integer + if tonumber(z) > 0 then + y = match(z, "^0*([1-9]%d*)$") -- remove leading zeros + else + y = "0" -- basic zero + end + + elseif not match(z, "[eE]") then -- float + local p, q = match(z, "^(%d*)%.(%d*)$") -- split + if p == "" then p = 0 end -- int part zero + if q == "" then q = "0" end -- fraction part zero + if tonumber(q) == 0 and p == 0 then + y = ".0" -- degenerate .000 to .0 + else + -- now, q > 0 holds and p is a number + local zeros_cnt = #match(q, "0*$") -- remove trailing zeros + if zeros_cnt > 0 then + q = sub(q, 1, #q - zeros_cnt) + end + -- if p > 0, nothing else we can do to simplify p.q case + if tonumber(p) > 0 then + y = p.."."..q + else + y = "."..q -- tentative, e.g. .000123 + local v = #match(q, "^0*") -- # leading spaces + local w = #q - v -- # significant digits + local nv = tostring(#q) + -- e.g. compare 123e-6 versus .000123 + if w + 2 + #nv < 1 + #q then + y = sub(q, -w).."e-"..nv + end + end + end + + else -- scientific number + local sig, ex = match(z, "^([^eE]+)[eE]([%+%-]?%d+)$") + ex = tonumber(ex) + -- if got ".", shift out fractional portion of significand + local p, q = match(sig, "^(%d*)%.(%d*)$") + if p then + ex = ex - #q + sig = p..q + end + if tonumber(sig) == 0 then + y = ".0" -- basic float zero + else + local v = #match(sig, "^0*") -- remove leading zeros + sig = sub(sig, v + 1) + v = #match(sig, "0*$") -- shift out trailing zeros + if v > 0 then + sig = sub(sig, 1, #sig - v) + ex = ex + v + end + -- examine exponent and determine which format is best + local nex = tostring(ex) + if ex >= 0 and (ex <= 1 + #nex) then -- a float + y = sig..rep("0", ex).."." + elseif ex < 0 and (ex >= -#sig) then -- fraction, e.g. .123 + v = #sig + ex + y = sub(sig, 1, v).."."..sub(sig, v + 1) + elseif ex < 0 and (#nex >= -ex - #sig) then + -- e.g. compare 1234e-5 versus .01234 + -- gives: #sig + 1 + #nex >= 1 + (-ex - #sig) + #sig + -- -> #nex >= -ex - #sig + v = -ex - #sig + y = "."..rep("0", v)..sig + else -- non-canonical scientific representation + y = sig.."e"..ex + end + end--if sig + end + + if y and y ~= sinfos[i] then + if opt_details then + print(" (line "..stoklns[i]..") "..sinfos[i].." -> "..y) + opt_details = opt_details + 1 + end + sinfos[i] = y + end +end + +--- Does string optimization. +-- +-- Note: It works on well-formed strings only! +-- +-- Optimizations on characters can be summarized as follows: +-- +-- \a\b\f\n\r\t\v -- no change +-- \\ -- no change +-- \"\' -- depends on delim, other can remove \ +-- \[\] -- remove \ +-- \ -- general escape, remove \ (Lua 5.1 only) +-- \ -- normalize the EOL only +-- \ddd -- if \a\b\f\n\r\t\v, change to latter +-- if other < ascii 32, keep ddd but zap leading zeros +-- but cannot have following digits +-- if >= ascii 32, translate it into the literal, then also +-- do escapes for \\,\",\' cases +-- -- no change +-- +-- Switch delimiters if string becomes shorter. +-- +-- @tparam int I +local function do_string(I) + local info = sinfos[I] + local delim = sub(info, 1, 1) -- delimiter used + local ndelim = (delim == "'") and '"' or "'" -- opposite " <-> ' + local z = sub(info, 2, -2) -- actual string + local i = 1 + local c_delim, c_ndelim = 0, 0 -- "/' counts + + while i <= #z do + local c = sub(z, i, i) + + if c == "\\" then -- escaped stuff + local j = i + 1 + local d = sub(z, j, j) + local p = find("abfnrtv\\\n\r\"\'0123456789", d, 1, true) + + if not p then -- \ -- remove \ (Lua 5.1 only) + z = sub(z, 1, i - 1)..sub(z, j) + i = i + 1 + + elseif p <= 8 then -- \a\b\f\n\r\t\v\\ + i = i + 2 -- no change + + elseif p <= 10 then -- \ -- normalize EOL + local eol = sub(z, j, j + 1) + if eol == "\r\n" or eol == "\n\r" then + z = sub(z, 1, i).."\n"..sub(z, j + 2) + elseif p == 10 then -- \r case + z = sub(z, 1, i).."\n"..sub(z, j + 1) + end + i = i + 2 + + elseif p <= 12 then -- \"\' -- remove \ for ndelim + if d == delim then + c_delim = c_delim + 1 + i = i + 2 + else + c_ndelim = c_ndelim + 1 + z = sub(z, 1, i - 1)..sub(z, j) + i = i + 1 + end + + else -- \ddd -- various steps + local s = match(z, "^(%d%d?%d?)", j) + j = i + 1 + #s -- skip to location + local cv = tonumber(s) + local cc = char(cv) + p = find("\a\b\f\n\r\t\v", cc, 1, true) + if p then -- special escapes + s = "\\"..sub("abfnrtv", p, p) + elseif cv < 32 then -- normalized \ddd + if match(sub(z, j, j), "%d") then + -- if a digit follows, \ddd cannot be shortened + s = "\\"..s + else + s = "\\"..cv + end + elseif cc == delim then -- \ + s = "\\"..cc + c_delim = c_delim + 1 + elseif cc == "\\" then -- \\ + s = "\\\\" + else -- literal character + s = cc + if cc == ndelim then + c_ndelim = c_ndelim + 1 + end + end + z = sub(z, 1, i - 1)..s..sub(z, j) + i = i + #s + + end--if p + + else-- c ~= "\\" -- -- no change + i = i + 1 + if c == ndelim then -- count ndelim, for switching delimiters + c_ndelim = c_ndelim + 1 + end + + end--if c + end--while + + -- Switching delimiters, a long-winded derivation: + -- (1) delim takes 2+2*c_delim bytes, ndelim takes c_ndelim bytes + -- (2) delim becomes c_delim bytes, ndelim becomes 2+2*c_ndelim bytes + -- simplifying the condition (1)>(2) --> c_delim > c_ndelim + if c_delim > c_ndelim then + i = 1 + while i <= #z do + local p, _, r = find(z, "([\'\"])", i) + if not p then break end + if r == delim then -- \ -> + z = sub(z, 1, p - 2)..sub(z, p) + i = p + else-- r == ndelim -- -> \ + z = sub(z, 1, p - 1).."\\"..sub(z, p) + i = p + 2 + end + end--while + delim = ndelim -- actually change delimiters + end + + z = delim..z..delim + if z ~= sinfos[I] then + if opt_details then + print(" (line "..stoklns[I]..") "..sinfos[I].." -> "..z) + opt_details = opt_details + 1 + end + sinfos[I] = z + end +end + +--- Does long string optimization. +-- +-- * remove first optional newline +-- * normalize embedded newlines +-- * reduce '=' separators in delimiters if possible +-- +-- Note: warning flagged if trailing whitespace found, not trimmed. +-- +-- @tparam int I +local function do_lstring(I) + local info = sinfos[I] + local delim1 = match(info, "^%[=*%[") -- cut out delimiters + local sep = #delim1 + local delim2 = sub(info, -sep, -1) + local z = sub(info, sep + 1, -(sep + 1)) -- lstring without delims + local y = "" + local i = 1 + + while true do + local p, _, r, s = find(z, "([\r\n])([\r\n]?)", i) + -- deal with a single line + local ln + if not p then + ln = sub(z, i) + elseif p >= i then + ln = sub(z, i, p - 1) + end + if ln ~= "" then + -- flag a warning if there are trailing spaces, won't optimize! + if match(ln, "%s+$") then + M.warn.LSTRING = "trailing whitespace in long string near line "..stoklns[I] + end + y = y..ln + end + if not p then -- done if no more EOLs + break + end + -- deal with line endings, normalize them + i = p + 1 + if p then + if #s > 0 and r ~= s then -- skip CRLF or LFCR + i = i + 1 + end + -- skip first newline, which can be safely deleted + if not(i == 1 and i == p) then + y = y.."\n" + end + end + end--while + + -- handle possible deletion of one or more '=' separators + if sep >= 3 then + local chk, okay = sep - 1 + -- loop to test ending delimiter with less of '=' down to zero + while chk >= 2 do + local delim = "%]"..rep("=", chk - 2).."%]" + if not match(y.."]", delim) then okay = chk end + chk = chk - 1 + end + if okay then -- change delimiters + sep = rep("=", okay - 2) + delim1, delim2 = "["..sep.."[", "]"..sep.."]" + end + end + + sinfos[I] = delim1..y..delim2 +end + +--- Does long comment optimization. +-- +-- * trim trailing whitespace +-- * normalize embedded newlines +-- * reduce '=' separators in delimiters if possible +-- +-- Note: It does not remove first optional newline. +-- +-- @tparam int I +local function do_lcomment(I) + local info = sinfos[I] + local delim1 = match(info, "^%-%-%[=*%[") -- cut out delimiters + local sep = #delim1 + local delim2 = sub(info, -(sep - 2), -1) + local z = sub(info, sep + 1, -(sep - 1)) -- comment without delims + local y = "" + local i = 1 + + while true do + local p, _, r, s = find(z, "([\r\n])([\r\n]?)", i) + -- deal with a single line, extract and check trailing whitespace + local ln + if not p then + ln = sub(z, i) + elseif p >= i then + ln = sub(z, i, p - 1) + end + if ln ~= "" then + -- trim trailing whitespace if non-empty line + local ws = match(ln, "%s*$") + if #ws > 0 then ln = sub(ln, 1, -(ws + 1)) end + y = y..ln + end + if not p then -- done if no more EOLs + break + end + -- deal with line endings, normalize them + i = p + 1 + if p then + if #s > 0 and r ~= s then -- skip CRLF or LFCR + i = i + 1 + end + y = y.."\n" + end + end--while + + -- handle possible deletion of one or more '=' separators + sep = sep - 2 + if sep >= 3 then + local chk, okay = sep - 1 + -- loop to test ending delimiter with less of '=' down to zero + while chk >= 2 do + local delim = "%]"..rep("=", chk - 2).."%]" + if not match(y, delim) then okay = chk end + chk = chk - 1 + end + if okay then -- change delimiters + sep = rep("=", okay - 2) + delim1, delim2 = "--["..sep.."[", "]"..sep.."]" + end + end + + sinfos[I] = delim1..y..delim2 +end + +--- Does short comment optimization. +-- +-- * trim trailing whitespace +-- +-- @tparam int i +local function do_comment(i) + local info = sinfos[i] + local ws = match(info, "%s*$") -- just look from end of string + if #ws > 0 then + info = sub(info, 1, -(ws + 1)) -- trim trailing whitespace + end + sinfos[i] = info +end + +--- Returns true if string found in long comment. +-- +-- This is a feature to keep copyright or license texts. +-- +-- @tparam bool opt_keep +-- @tparam string info +-- @treturn bool +local function keep_lcomment(opt_keep, info) + if not opt_keep then return false end -- option not set + local delim1 = match(info, "^%-%-%[=*%[") -- cut out delimiters + local sep = #delim1 + local z = sub(info, sep + 1, -(sep - 1)) -- comment without delims + if find(z, opt_keep, 1, true) then -- try to match + return true + end +end + +--- The main entry point. +-- +-- * currently, lexer processing has 2 passes +-- * processing is done on a line-oriented basis, which is easier to +-- grok due to the next point... +-- * since there are various options that can be enabled or disabled, +-- processing is a little messy or convoluted +-- +-- @tparam {[string]=bool,...} option +-- @tparam {string,...} toklist +-- @tparam {string,...} semlist +-- @tparam {int,...} toklnlist +-- @treturn {string,...} toklist +-- @treturn {string,...} semlist +-- @treturn {int,...} toklnlist +function M.optimize(option, toklist, semlist, toklnlist) + -- Set option flags. + local opt_comments = option["opt-comments"] + local opt_whitespace = option["opt-whitespace"] + local opt_emptylines = option["opt-emptylines"] + local opt_eols = option["opt-eols"] + local opt_strings = option["opt-strings"] + local opt_numbers = option["opt-numbers"] + local opt_x = option["opt-experimental"] + local opt_keep = option.KEEP + opt_details = option.DETAILS and 0 -- upvalues for details display + print = M.print or _G.print + if opt_eols then -- forced settings, otherwise won't work properly + opt_comments = true + opt_whitespace = true + opt_emptylines = true + elseif opt_x then + opt_whitespace = true + end + + -- Variable initialization. + stoks, sinfos, stoklns -- set source lists + = toklist, semlist, toklnlist + local i = 1 -- token position + local tok, info -- current token + local prev -- position of last grammar token + -- on same line (for TK_SPACE stuff) + + -- Changes a token, info pair. + local function settoken(tok, info, I) --luacheck: ignore 431 + I = I or i + stoks[I] = tok or "" + sinfos[I] = info or "" + end + + -- Experimental optimization for ';' operator. + if opt_x then + while true do + tok, info = stoks[i], sinfos[i] + if tok == "TK_EOS" then -- end of stream/pass + break + elseif tok == "TK_OP" and info == ";" then + -- ';' operator found, since it is entirely optional, set it + -- as a space to let whitespace optimization do the rest + settoken("TK_SPACE", " ") + end + i = i + 1 + end + repack_tokens() + end + + -- Processing loop (PASS 1) + i = 1 + while true do + tok, info = stoks[i], sinfos[i] + + local atstart = atlinestart(i) -- set line begin flag + if atstart then prev = nil end + + if tok == "TK_EOS" then -- end of stream/pass + break + + elseif tok == "TK_KEYWORD" or -- keywords, identifiers, + tok == "TK_NAME" or -- operators + tok == "TK_OP" then + -- TK_KEYWORD and TK_OP can't be optimized without a big + -- optimization framework; it would be more of an optimizing + -- compiler, not a source code compressor + -- TK_NAME that are locals needs parser to analyze/optimize + prev = i + + elseif tok == "TK_NUMBER" then -- numbers + if opt_numbers then + do_number(i) -- optimize + end + prev = i + + elseif tok == "TK_STRING" or -- strings, long strings + tok == "TK_LSTRING" then + if opt_strings then + if tok == "TK_STRING" then + do_string(i) -- optimize + else + do_lstring(i) -- optimize + end + end + prev = i + + elseif tok == "TK_COMMENT" then -- short comments + if opt_comments then + if i == 1 and sub(info, 1, 1) == "#" then + -- keep shbang comment, trim whitespace + do_comment(i) + else + -- safe to delete, as a TK_EOL (or TK_EOS) always follows + settoken() -- remove entirely + end + elseif opt_whitespace then -- trim whitespace only + do_comment(i) + end + + elseif tok == "TK_LCOMMENT" then -- long comments + if keep_lcomment(opt_keep, info) then + -- if --keep, we keep a long comment if is found; + -- this is a feature to keep copyright or license texts + if opt_whitespace then -- trim whitespace only + do_lcomment(i) + end + prev = i + elseif opt_comments then + local eols = commenteols(info) + + -- prepare opt_emptylines case first, if a disposable token + -- follows, current one is safe to dump, else keep a space; + -- it is implied that the operation is safe for '-', because + -- current is a TK_LCOMMENT, and must be separate from a '-' + if is_faketoken[stoks[i + 1]] then + settoken() -- remove entirely + tok = "" + else + settoken("TK_SPACE", " ") + end + + -- if there are embedded EOLs to keep and opt_emptylines is + -- disabled, then switch the token into one or more EOLs + if not opt_emptylines and eols > 0 then + settoken("TK_EOL", rep("\n", eols)) + end + + -- if optimizing whitespaces, force reinterpretation of the + -- token to give a chance for the space to be optimized away + if opt_whitespace and tok ~= "" then + i = i - 1 -- to reinterpret + end + else -- disabled case + if opt_whitespace then -- trim whitespace only + do_lcomment(i) + end + prev = i + end + + elseif tok == "TK_EOL" then -- line endings + if atstart and opt_emptylines then + settoken() -- remove entirely + elseif info == "\r\n" or info == "\n\r" then + -- normalize the rest of the EOLs for CRLF/LFCR only + -- (note that TK_LCOMMENT can change into several EOLs) + settoken("TK_EOL", "\n") + end + + elseif tok == "TK_SPACE" then -- whitespace + if opt_whitespace then + if atstart or atlineend(i) then + -- delete leading and trailing whitespace + settoken() -- remove entirely + else + + -- at this point, since leading whitespace have been removed, + -- there should be a either a real token or a TK_LCOMMENT + -- prior to hitting this whitespace; the TK_LCOMMENT case + -- only happens if opt_comments is disabled; so prev ~= nil + local ptok = stoks[prev] + if ptok == "TK_LCOMMENT" then + -- previous TK_LCOMMENT can abut with anything + settoken() -- remove entirely + else + -- prev must be a grammar token; consecutive TK_SPACE + -- tokens is impossible when optimizing whitespace + local ntok = stoks[i + 1] + if is_faketoken[ntok] then + -- handle special case where a '-' cannot abut with + -- either a short comment or a long comment + if (ntok == "TK_COMMENT" or ntok == "TK_LCOMMENT") and + ptok == "TK_OP" and sinfos[prev] == "-" then + -- keep token + else + settoken() -- remove entirely + end + else--is_realtoken + -- check a pair of grammar tokens, if can abut, then + -- delete space token entirely, otherwise keep one space + local s = checkpair(prev, i + 1) + if s == "" then + settoken() -- remove entirely + else + settoken("TK_SPACE", " ") + end + end + end + + end + end + + else + error("unidentified token encountered") + end + + i = i + 1 + end--while + repack_tokens() + + -- Processing loop (PASS 2) + if opt_eols then + i = 1 + -- Aggressive EOL removal only works with most non-grammar tokens + -- optimized away because it is a rather simple scheme -- basically + -- it just checks 'real' token pairs around EOLs. + if stoks[1] == "TK_COMMENT" then + -- first comment still existing must be shbang, skip whole line + i = 3 + end + while true do + tok = stoks[i] + + if tok == "TK_EOS" then -- end of stream/pass + break + + elseif tok == "TK_EOL" then -- consider each TK_EOL + local t1, t2 = stoks[i - 1], stoks[i + 1] + if is_realtoken[t1] and is_realtoken[t2] then -- sanity check + local s = checkpair(i - 1, i + 1) + if s == "" or t2 == "TK_EOS" then + settoken() -- remove entirely + end + end + end--if tok + + i = i + 1 + end--while + repack_tokens() + end + + if opt_details and opt_details > 0 then print() end -- spacing + return stoks, sinfos, stoklns +end + +return M diff --git a/tools/luasrcdiet/optparser.lua b/tools/luasrcdiet/optparser.lua new file mode 100644 index 0000000..162b881 --- /dev/null +++ b/tools/luasrcdiet/optparser.lua @@ -0,0 +1,644 @@ +--------- +-- This module does parser-based optimizations. +-- +-- **Notes:** +-- +-- * The processing load is quite significant, but since this is an +-- off-line text processor, I believe we can wait a few seconds. +-- * TODO: Might process "local a,a,a" wrongly... need tests! +-- * TODO: Remove position handling if overlapped locals (rem < 0) +-- needs more study, to check behaviour. +-- * TODO: There are probably better ways to do allocation, e.g. by +-- choosing better methods to sort and pick locals... +-- * TODO: We don't need 53*63 two-letter identifiers; we can make +-- do with significantly less depending on how many that are really +-- needed and improve entropy; e.g. 13 needed -> choose 4*4 instead. +---- +local byte = string.byte +local char = string.char +local concat = table.concat +local fmt = string.format +local pairs = pairs +local rep = string.rep +local sort = table.sort +local sub = string.sub + + +local M = {} + +-- Letter frequencies for reducing symbol entropy (fixed version) +-- * Might help a wee bit when the output file is compressed +-- * See Wikipedia: http://en.wikipedia.org/wiki/Letter_frequencies +-- * We use letter frequencies according to a Linotype keyboard, plus +-- the underscore, and both lower case and upper case letters. +-- * The arrangement below (LC, underscore, %d, UC) is arbitrary. +-- * This is certainly not optimal, but is quick-and-dirty and the +-- process has no significant overhead +local LETTERS = "etaoinshrdlucmfwypvbgkqjxz_ETAOINSHRDLUCMFWYPVBGKQJXZ" +local ALPHANUM = "etaoinshrdlucmfwypvbgkqjxz_0123456789ETAOINSHRDLUCMFWYPVBGKQJXZ" + +-- Names or identifiers that must be skipped. +-- (The first two lines are for keywords.) +local SKIP_NAME = {} +for v in ([[ +and break do else elseif end false for function if in +local nil not or repeat return then true until while +self _ENV]]):gmatch("%S+") do + SKIP_NAME[v] = true +end + + +local toklist, seminfolist, -- token lists (lexer output) + tokpar, seminfopar, xrefpar, -- token lists (parser output) + globalinfo, localinfo, -- variable information tables + statinfo, -- statment type table + globaluniq, localuniq, -- unique name tables + var_new, -- index of new variable names + varlist -- list of output variables + +--- Preprocesses information table to get lists of unique names. +-- +-- @tparam {table,...} infotable +-- @treturn table +local function preprocess(infotable) + local uniqtable = {} + for i = 1, #infotable do -- enumerate info table + local obj = infotable[i] + local name = obj.name + + if not uniqtable[name] then -- not found, start an entry + uniqtable[name] = { + decl = 0, token = 0, size = 0, + } + end + + local uniq = uniqtable[name] -- count declarations, tokens, size + uniq.decl = uniq.decl + 1 + local xref = obj.xref + local xcount = #xref + uniq.token = uniq.token + xcount + uniq.size = uniq.size + xcount * #name + + if obj.decl then -- if local table, create first,last pairs + obj.id = i + obj.xcount = xcount + if xcount > 1 then -- if ==1, means local never accessed + obj.first = xref[2] + obj.last = xref[xcount] + end + + else -- if global table, add a back ref + uniq.id = i + end + + end--for + return uniqtable +end + +--- Calculates actual symbol frequencies, in order to reduce entropy. +-- +-- * This may help further reduce the size of compressed sources. +-- * Note that since parsing optimizations is put before lexing +-- optimizations, the frequency table is not exact! +-- * Yes, this will miss --keep block comments too... +-- +-- @tparam table option +local function recalc_for_entropy(option) + -- table of token classes to accept in calculating symbol frequency + local ACCEPT = { + TK_KEYWORD = true, TK_NAME = true, TK_NUMBER = true, + TK_STRING = true, TK_LSTRING = true, + } + if not option["opt-comments"] then + ACCEPT.TK_COMMENT = true + ACCEPT.TK_LCOMMENT = true + end + + -- Create a new table and remove any original locals by filtering. + local filtered = {} + for i = 1, #toklist do + filtered[i] = seminfolist[i] + end + for i = 1, #localinfo do -- enumerate local info table + local obj = localinfo[i] + local xref = obj.xref + for j = 1, obj.xcount do + local p = xref[j] + filtered[p] = "" -- remove locals + end + end + + local freq = {} -- reset symbol frequency table + for i = 0, 255 do freq[i] = 0 end + for i = 1, #toklist do -- gather symbol frequency + local tok, info = toklist[i], filtered[i] + if ACCEPT[tok] then + for j = 1, #info do + local c = byte(info, j) + freq[c] = freq[c] + 1 + end + end--if + end--for + + -- Re-sorts symbols according to actual frequencies. + -- + -- @tparam string symbols + -- @treturn string + local function resort(symbols) + local symlist = {} + for i = 1, #symbols do -- prepare table to sort + local c = byte(symbols, i) + symlist[i] = { c = c, freq = freq[c], } + end + sort(symlist, function(v1, v2) -- sort selected symbols + return v1.freq > v2.freq + end) + local charlist = {} -- reconstitute the string + for i = 1, #symlist do + charlist[i] = char(symlist[i].c) + end + return concat(charlist) + end + + LETTERS = resort(LETTERS) -- change letter arrangement + ALPHANUM = resort(ALPHANUM) +end + +--- Returns a string containing a new local variable name to use, and +-- a flag indicating whether it collides with a global variable. +-- +-- Trapping keywords and other names like 'self' is done elsewhere. +-- +-- @treturn string A new local variable name. +-- @treturn bool Whether the name collides with a global variable. +local function new_var_name() + local var + local cletters, calphanum = #LETTERS, #ALPHANUM + local v = var_new + if v < cletters then -- single char + v = v + 1 + var = sub(LETTERS, v, v) + else -- longer names + local range, sz = cletters, 1 -- calculate # chars fit + repeat + v = v - range + range = range * calphanum + sz = sz + 1 + until range > v + local n = v % cletters -- left side cycles faster + v = (v - n) / cletters -- do first char first + n = n + 1 + var = sub(LETTERS, n, n) + while sz > 1 do + local m = v % calphanum + v = (v - m) / calphanum + m = m + 1 + var = var..sub(ALPHANUM, m, m) + sz = sz - 1 + end + end + var_new = var_new + 1 + return var, globaluniq[var] ~= nil +end + +--- Calculates and prints some statistics. +-- +-- Note: probably better in main source, put here for now. +-- +-- @tparam table globaluniq +-- @tparam table localuniq +-- @tparam table afteruniq +-- @tparam table option +local function stats_summary(globaluniq, localuniq, afteruniq, option) --luacheck: ignore 431 + local print = M.print or print + local opt_details = option.DETAILS + if option.QUIET then return end + + local uniq_g , uniq_li, uniq_lo = 0, 0, 0 + local decl_g, decl_li, decl_lo = 0, 0, 0 + local token_g, token_li, token_lo = 0, 0, 0 + local size_g, size_li, size_lo = 0, 0, 0 + + local function avg(c, l) -- safe average function + if c == 0 then return 0 end + return l / c + end + + -- Collect statistics (Note: globals do not have declarations!) + for _, uniq in pairs(globaluniq) do + uniq_g = uniq_g + 1 + token_g = token_g + uniq.token + size_g = size_g + uniq.size + end + for _, uniq in pairs(localuniq) do + uniq_li = uniq_li + 1 + decl_li = decl_li + uniq.decl + token_li = token_li + uniq.token + size_li = size_li + uniq.size + end + for _, uniq in pairs(afteruniq) do + uniq_lo = uniq_lo + 1 + decl_lo = decl_lo + uniq.decl + token_lo = token_lo + uniq.token + size_lo = size_lo + uniq.size + end + local uniq_ti = uniq_g + uniq_li + local decl_ti = decl_g + decl_li + local token_ti = token_g + token_li + local size_ti = size_g + size_li + local uniq_to = uniq_g + uniq_lo + local decl_to = decl_g + decl_lo + local token_to = token_g + token_lo + local size_to = size_g + size_lo + + -- Detailed stats: global list + if opt_details then + local sorted = {} -- sort table of unique global names by size + for name, uniq in pairs(globaluniq) do + uniq.name = name + sorted[#sorted + 1] = uniq + end + sort(sorted, function(v1, v2) + return v1.size > v2.size + end) + + do + local tabf1, tabf2 = "%8s%8s%10s %s", "%8d%8d%10.2f %s" + local hl = rep("-", 44) + print("*** global variable list (sorted by size) ***\n"..hl) + print(fmt(tabf1, "Token", "Input", "Input", "Global")) + print(fmt(tabf1, "Count", "Bytes", "Average", "Name")) + print(hl) + for i = 1, #sorted do + local uniq = sorted[i] + print(fmt(tabf2, uniq.token, uniq.size, avg(uniq.token, uniq.size), uniq.name)) + end + print(hl) + print(fmt(tabf2, token_g, size_g, avg(token_g, size_g), "TOTAL")) + print(hl.."\n") + end + + -- Detailed stats: local list + do + local tabf1, tabf2 = "%8s%8s%8s%10s%8s%10s %s", "%8d%8d%8d%10.2f%8d%10.2f %s" + local hl = rep("-", 70) + print("*** local variable list (sorted by allocation order) ***\n"..hl) + print(fmt(tabf1, "Decl.", "Token", "Input", "Input", "Output", "Output", "Global")) + print(fmt(tabf1, "Count", "Count", "Bytes", "Average", "Bytes", "Average", "Name")) + print(hl) + for i = 1, #varlist do -- iterate according to order assigned + local name = varlist[i] + local uniq = afteruniq[name] + local old_t, old_s = 0, 0 + for j = 1, #localinfo do -- find corresponding old names and calculate + local obj = localinfo[j] + if obj.name == name then + old_t = old_t + obj.xcount + old_s = old_s + obj.xcount * #obj.oldname + end + end + print(fmt(tabf2, uniq.decl, uniq.token, old_s, avg(old_t, old_s), + uniq.size, avg(uniq.token, uniq.size), name)) + end + print(hl) + print(fmt(tabf2, decl_lo, token_lo, size_li, avg(token_li, size_li), + size_lo, avg(token_lo, size_lo), "TOTAL")) + print(hl.."\n") + end + end--if opt_details + + -- Display output + do + local tabf1, tabf2 = "%-16s%8s%8s%8s%8s%10s", "%-16s%8d%8d%8d%8d%10.2f" + local hl = rep("-", 58) + print("*** local variable optimization summary ***\n"..hl) + print(fmt(tabf1, "Variable", "Unique", "Decl.", "Token", "Size", "Average")) + print(fmt(tabf1, "Types", "Names", "Count", "Count", "Bytes", "Bytes")) + print(hl) + print(fmt(tabf2, "Global", uniq_g, decl_g, token_g, size_g, avg(token_g, size_g))) + print(hl) + print(fmt(tabf2, "Local (in)", uniq_li, decl_li, token_li, size_li, avg(token_li, size_li))) + print(fmt(tabf2, "TOTAL (in)", uniq_ti, decl_ti, token_ti, size_ti, avg(token_ti, size_ti))) + print(hl) + print(fmt(tabf2, "Local (out)", uniq_lo, decl_lo, token_lo, size_lo, avg(token_lo, size_lo))) + print(fmt(tabf2, "TOTAL (out)", uniq_to, decl_to, token_to, size_to, avg(token_to, size_to))) + print(hl.."\n") + end +end + +--- Does experimental optimization for f("string") statements. +-- +-- It's safe to delete parentheses without adding whitespace, as both +-- kinds of strings can abut with anything else. +local function optimize_func1() + + local function is_strcall(j) -- find f("string") pattern + local t1 = tokpar[j + 1] or "" + local t2 = tokpar[j + 2] or "" + local t3 = tokpar[j + 3] or "" + if t1 == "(" and t2 == "" and t3 == ")" then + return true + end + end + + local del_list = {} -- scan for function pattern, + local i = 1 -- tokens to be deleted are marked + while i <= #tokpar do + local id = statinfo[i] + if id == "call" and is_strcall(i) then -- found & mark () + del_list[i + 1] = true -- '(' + del_list[i + 3] = true -- ')' + i = i + 3 + end + i = i + 1 + end + + -- Delete a token and adjust all relevant tables. + -- * Currently invalidates globalinfo and localinfo (not updated), + -- so any other optimization is done after processing locals + -- (of course, we can also lex the source data again...). + -- * Faster one-pass token deletion. + local del_list2 = {} + do + local i, dst, idend = 1, 1, #tokpar + while dst <= idend do -- process parser tables + if del_list[i] then -- found a token to delete? + del_list2[xrefpar[i]] = true + i = i + 1 + end + if i > dst then + if i <= idend then -- shift table items lower + tokpar[dst] = tokpar[i] + seminfopar[dst] = seminfopar[i] + xrefpar[dst] = xrefpar[i] - (i - dst) + statinfo[dst] = statinfo[i] + else -- nil out excess entries + tokpar[dst] = nil + seminfopar[dst] = nil + xrefpar[dst] = nil + statinfo[dst] = nil + end + end + i = i + 1 + dst = dst + 1 + end + end + + do + local i, dst, idend = 1, 1, #toklist + while dst <= idend do -- process lexer tables + if del_list2[i] then -- found a token to delete? + i = i + 1 + end + if i > dst then + if i <= idend then -- shift table items lower + toklist[dst] = toklist[i] + seminfolist[dst] = seminfolist[i] + else -- nil out excess entries + toklist[dst] = nil + seminfolist[dst] = nil + end + end + i = i + 1 + dst = dst + 1 + end + end +end + +--- Does local variable optimization. +-- +-- @tparam {[string]=bool,...} option +local function optimize_locals(option) + var_new = 0 -- reset variable name allocator + varlist = {} + + -- Preprocess global/local tables, handle entropy reduction. + globaluniq = preprocess(globalinfo) + localuniq = preprocess(localinfo) + if option["opt-entropy"] then -- for entropy improvement + recalc_for_entropy(option) + end + + -- Build initial declared object table, then sort according to + -- token count, this might help assign more tokens to more common + -- variable names such as 'e' thus possibly reducing entropy. + -- * An object knows its localinfo index via its 'id' field. + -- * Special handling for "self" and "_ENV" special local (parameter) here. + local object = {} + for i = 1, #localinfo do + object[i] = localinfo[i] + end + sort(object, function(v1, v2) -- sort largest first + return v1.xcount > v2.xcount + end) + + -- The special "self" and "_ENV" function parameters must be preserved. + -- * The allocator below will never use "self", so it is safe to + -- keep those implicit declarations as-is. + local temp, j, used_specials = {}, 1, {} + for i = 1, #object do + local obj = object[i] + if not obj.is_special then + temp[j] = obj + j = j + 1 + else + used_specials[#used_specials + 1] = obj.name + end + end + object = temp + + -- A simple first-come first-served heuristic name allocator, + -- note that this is in no way optimal... + -- * Each object is a local variable declaration plus existence. + -- * The aim is to assign short names to as many tokens as possible, + -- so the following tries to maximize name reuse. + -- * Note that we preserve sort order. + local nobject = #object + while nobject > 0 do + local varname, gcollide + repeat + varname, gcollide = new_var_name() -- collect a variable name + until not SKIP_NAME[varname] -- skip all special names + varlist[#varlist + 1] = varname -- keep a list + local oleft = nobject + + -- If variable name collides with an existing global, the name + -- cannot be used by a local when the name is accessed as a global + -- during which the local is alive (between 'act' to 'rem'), so + -- we drop objects that collides with the corresponding global. + if gcollide then + -- find the xref table of the global + local gref = globalinfo[globaluniq[varname].id].xref + local ngref = #gref + -- enumerate for all current objects; all are valid at this point + for i = 1, nobject do + local obj = object[i] + local act, rem = obj.act, obj.rem -- 'live' range of local + -- if rem < 0, it is a -id to a local that had the same name + -- so follow rem to extend it; does this make sense? + while rem < 0 do + rem = localinfo[-rem].rem + end + local drop + for j = 1, ngref do + local p = gref[j] + if p >= act and p <= rem then drop = true end -- in range? + end + if drop then + obj.skip = true + oleft = oleft - 1 + end + end--for + end--if gcollide + + -- Now the first unassigned local (since it's sorted) will be the + -- one with the most tokens to rename, so we set this one and then + -- eliminate all others that collides, then any locals that left + -- can then reuse the same variable name; this is repeated until + -- all local declaration that can use this name is assigned. + -- + -- The criteria for local-local reuse/collision is: + -- A is the local with a name already assigned + -- B is the unassigned local under consideration + -- => anytime A is accessed, it cannot be when B is 'live' + -- => to speed up things, we have first/last accesses noted + while oleft > 0 do + local i = 1 + while object[i].skip do -- scan for first object + i = i + 1 + end + + -- First object is free for assignment of the variable name + -- [first,last] gives the access range for collision checking. + oleft = oleft - 1 + local obja = object[i] + i = i + 1 + obja.newname = varname + obja.skip = true + obja.done = true + local first, last = obja.first, obja.last + local xref = obja.xref + + -- Then, scan all the rest and drop those colliding. + -- If A was never accessed then it'll never collide with anything + -- otherwise trivial skip if: + -- * B was activated after A's last access (last < act), + -- * B was removed before A's first access (first > rem), + -- if not, see detailed skip below... + if first and oleft > 0 then -- must have at least 1 access + local scanleft = oleft + while scanleft > 0 do + while object[i].skip do -- next valid object + i = i + 1 + end + scanleft = scanleft - 1 + local objb = object[i] + i = i + 1 + local act, rem = objb.act, objb.rem -- live range of B + -- if rem < 0, extend range of rem thru' following local + while rem < 0 do + rem = localinfo[-rem].rem + end + + if not(last < act or first > rem) then -- possible collision + + -- B is activated later than A or at the same statement, + -- this means for no collision, A cannot be accessed when B + -- is alive, since B overrides A (or is a peer). + if act >= obja.act then + for j = 1, obja.xcount do -- ... then check every access + local p = xref[j] + if p >= act and p <= rem then -- A accessed when B live! + oleft = oleft - 1 + objb.skip = true + break + end + end--for + + -- A is activated later than B, this means for no collision, + -- A's access is okay since it overrides B, but B's last + -- access need to be earlier than A's activation time. + else + if objb.last and objb.last >= obja.act then + oleft = oleft - 1 + objb.skip = true + end + end + end + + if oleft == 0 then break end + end + end--if first + + end--while + + -- After assigning all possible locals to one variable name, the + -- unassigned locals/objects have the skip field reset and the table + -- is compacted, to hopefully reduce iteration time. + local temp, j = {}, 1 + for i = 1, nobject do + local obj = object[i] + if not obj.done then + obj.skip = false + temp[j] = obj + j = j + 1 + end + end + object = temp -- new compacted object table + nobject = #object -- objects left to process + + end--while + + -- After assigning all locals with new variable names, we can + -- patch in the new names, and reprocess to get 'after' stats. + for i = 1, #localinfo do -- enumerate all locals + local obj = localinfo[i] + local xref = obj.xref + if obj.newname then -- if got new name, patch it in + for j = 1, obj.xcount do + local p = xref[j] -- xrefs indexes the token list + seminfolist[p] = obj.newname + end + obj.name, obj.oldname -- adjust names + = obj.newname, obj.name + else + obj.oldname = obj.name -- for cases like 'self' + end + end + + -- Deal with statistics output. + for _, name in ipairs(used_specials) do + varlist[#varlist + 1] = name + end + local afteruniq = preprocess(localinfo) + stats_summary(globaluniq, localuniq, afteruniq, option) +end + +--- The main entry point. +-- +-- @tparam table option +-- @tparam {string,...} _toklist +-- @tparam {string,...} _seminfolist +-- @tparam table xinfo +function M.optimize(option, _toklist, _seminfolist, xinfo) + -- set tables + toklist, seminfolist -- from lexer + = _toklist, _seminfolist + tokpar, seminfopar, xrefpar -- from parser + = xinfo.toklist, xinfo.seminfolist, xinfo.xreflist + globalinfo, localinfo, statinfo -- from parser + = xinfo.globalinfo, xinfo.localinfo, xinfo.statinfo + + -- Optimize locals. + if option["opt-locals"] then + optimize_locals(option) + end + + -- Other optimizations. + if option["opt-experimental"] then -- experimental + optimize_func1() + -- WARNING globalinfo and localinfo now invalidated! + end +end + +return M diff --git a/tools/luasrcdiet/plugin/example.lua b/tools/luasrcdiet/plugin/example.lua new file mode 100644 index 0000000..6a04e33 --- /dev/null +++ b/tools/luasrcdiet/plugin/example.lua @@ -0,0 +1,90 @@ +--------- +-- Example of a plugin for LuaSrcDiet. +-- +-- WARNING: highly experimental! interface liable to change +-- +-- **Notes:** +-- +-- * Any function can be omitted and LuaSrcDiet won't call it. +-- * The functions are: +-- (1) init(_option, _srcfl, _destfl) +-- (2) post_load(z) can return z +-- (3) post_lex(toklist, seminfolist, toklnlist) +-- (4) post_parse(globalinfo, localinfo) +-- (5) post_optparse() +-- (6) post_optlex(toklist, seminfolist, toklnlist) +-- * Older tables can be copied and kept in the plugin and used later. +-- * If you modify 'option', remember that LuaSrcDiet might be +-- processing more than one file. +-- * Arrangement of the functions is not final! +-- * TODO: can't process additional options from command line yet +---- + +local M = {} + +local option -- local reference to list of options +local srcfl, destfl -- filenames +local old_quiet + +local function print(...) -- handle quiet option + if option.QUIET then return end + _G.print(...) +end + +--- Initialization. +-- +-- @tparam {[string]=bool,...} _option +-- @tparam string _srcfl Path of the source file. +-- @tparam string _destfl Path of the destination file. +function M.init(_option, _srcfl, _destfl) + option = _option + srcfl, destfl = _srcfl, _destfl + -- plugin can impose its own option starting from here +end + +--- Message display, post-load processing, can return z. +function M.post_load(z) + -- this message will print after the LuaSrcDiet title message + print([[ +Example plugin module for LuaSrcDiet +]]) + print("Example: source file name is '"..srcfl.."'") + print("Example: destination file name is '"..destfl.."'") + print("Example: the size of the source file is "..#z.." bytes") + -- returning z is optional; this allows optional replacement of + -- the source data prior to lexing + return z +end + +--- Post-lexing processing, can work on lexer table output. +function M.post_lex(toklist, seminfolist, toklnlist) --luacheck: ignore + print("Example: the number of lexed elements is "..#toklist) +end + +--- Post-parsing processing, gives globalinfo, localinfo. +function M.post_parse(globalinfo, localinfo) + print("Example: size of globalinfo is "..#globalinfo) + print("Example: size of localinfo is "..#localinfo) + old_quiet = option.QUIET + option.QUIET = true +end + +--- Post-parser optimization processing, can get tables from elsewhere. +function M.post_optparse() + option.QUIET = old_quiet + print("Example: pretend to do post-optparse") +end + +--- Post-lexer optimization processing, can get tables from elsewhere. +function M.post_optlex(toklist, seminfolist, toklnlist) --luacheck: ignore + print("Example: pretend to do post-optlex") + -- restore old settings, other file might need original settings + option.QUIET = old_quiet + -- option.EXIT can be set at the end of any post_* function to stop + -- further processing and exit for the current file being worked on + -- in this case, final stats printout is disabled and the output will + -- not be written to the destination file + option.EXIT = true +end + +return M diff --git a/tools/luasrcdiet/plugin/html.lua b/tools/luasrcdiet/plugin/html.lua new file mode 100644 index 0000000..2305ee7 --- /dev/null +++ b/tools/luasrcdiet/plugin/html.lua @@ -0,0 +1,177 @@ +--------- +-- Turns Lua 5.1 source code into HTML files. +-- +-- WARNING: highly experimental! interface liable to change +-- +-- **Notes:** +-- +-- * This HTML highlighter marks globals brightly so that their usage +-- can be manually optimized. +-- * Either uses a .html extension for output files or it follows the +-- -o option. +-- * The HTML style tries to follow that of the Lua wiki. +---- +local fs = require "luasrcdiet.fs" + +local concat = table.concat +local find = string.find +local fmt = string.format +local sub = string.sub + +local M = {} + +local HTML_EXT = ".html" +local ENTITIES = { + ["&"] = "&", ["<"] = "<", [">"] = ">", + ["'"] = "'", ["\""] = """, +} + +-- simple headers and footers +local HEADER = [[ + + + +%s + + + + +
+]]
+local FOOTER = [[
+
+ + +]] +-- for more, please see wikimain.css from the Lua wiki site +local STYLESHEET = [[ +BODY { + background: white; + color: navy; +} +pre.code { color: black; } +span.comment { color: #00a000; } +span.string { color: #009090; } +span.keyword { color: black; font-weight: bold; } +span.number { color: #993399; } +span.operator { } +span.name { } +span.global { color: #ff0000; font-weight: bold; } +span.local { color: #0000ff; font-weight: bold; } +]] + +local option -- local reference to list of options +local srcfl, destfl -- filenames +local toklist, seminfolist -- token data + +local function print(...) -- handle quiet option + if option.QUIET then return end + _G.print(...) +end + +--- Initialization. +function M.init(_option, _srcfl) + option = _option + srcfl = _srcfl + local extb, _ = find(srcfl, "%.[^%.%\\%/]*$") + local basename = srcfl + if extb and extb > 1 then + basename = sub(srcfl, 1, extb - 1) + end + destfl = basename..HTML_EXT + if option.OUTPUT_FILE then + destfl = option.OUTPUT_FILE + end + if srcfl == destfl then + error("output filename identical to input filename") + end +end + +--- Message display, post-load processing. +function M.post_load() + print([[ +HTML plugin module for LuaSrcDiet +]]) + print("Exporting: "..srcfl.." -> "..destfl.."\n") +end + +--- Post-lexing processing, can work on lexer table output. +function M.post_lex(_toklist, _seminfolist) + toklist, seminfolist = _toklist, _seminfolist +end + +--- Escapes the usual suspects for HTML/XML. +local function do_entities(z) + local i = 1 + while i <= #z do + local c = sub(z, i, i) + local d = ENTITIES[c] + if d then + c = d + z = sub(z, 1, i - 1)..c..sub(z, i + 1) + end + i = i + #c + end--while + return z +end + +--- Post-parsing processing, gives globalinfo, localinfo. +function M.post_parse(globalinfo, localinfo) + local html = {} + local function add(s) -- html helpers + html[#html + 1] = s + end + local function span(class, s) + add(''..s..'') + end + + for i = 1, #globalinfo do -- mark global identifiers as TK_GLOBAL + local obj = globalinfo[i] + local xref = obj.xref + for j = 1, #xref do + local p = xref[j] + toklist[p] = "TK_GLOBAL" + end + end--for + + for i = 1, #localinfo do -- mark local identifiers as TK_LOCAL + local obj = localinfo[i] + local xref = obj.xref + for j = 1, #xref do + local p = xref[j] + toklist[p] = "TK_LOCAL" + end + end--for + + add(fmt(HEADER, -- header and leading stuff + do_entities(srcfl), + STYLESHEET)) + for i = 1, #toklist do -- enumerate token list + local tok, info = toklist[i], seminfolist[i] + if tok == "TK_KEYWORD" then + span("keyword", info) + elseif tok == "TK_STRING" or tok == "TK_LSTRING" then + span("string", do_entities(info)) + elseif tok == "TK_COMMENT" or tok == "TK_LCOMMENT" then + span("comment", do_entities(info)) + elseif tok == "TK_GLOBAL" then + span("global", info) + elseif tok == "TK_LOCAL" then + span("local", info) + elseif tok == "TK_NAME" then + span("name", info) + elseif tok == "TK_NUMBER" then + span("number", info) + elseif tok == "TK_OP" then + span("operator", do_entities(info)) + elseif tok ~= "TK_EOS" then -- TK_EOL, TK_SPACE + add(info) + end + end--for + add(FOOTER) + assert(fs.write_file(destfl, concat(html), "wb")) + option.EXIT = true +end + +return M diff --git a/tools/luasrcdiet/plugin/sloc.lua b/tools/luasrcdiet/plugin/sloc.lua new file mode 100644 index 0000000..50b811f --- /dev/null +++ b/tools/luasrcdiet/plugin/sloc.lua @@ -0,0 +1,89 @@ +--------- +-- Calculates SLOC for Lua 5.1 scripts +-- +-- WARNING: highly experimental! interface liable to change +-- +-- **Notes:** +-- +-- * SLOC's behaviour is based on David Wheeler's SLOCCount. +-- * Empty lines and comment don't count as significant. +-- * Empty lines in long strings are also insignificant. This is +-- debatable. In SLOCCount, this allows counting of invalid multi- +-- line strings for C. But an empty line is still an empty line. +-- * Ignores the --quiet option, print own result line. +---- + +local M = {} + +local option -- local reference to list of options +local srcfl -- source file name + +function M.init(_option, _srcfl) + option = _option + option.QUIET = true + srcfl = _srcfl +end + +--- Splits a block into a table of lines (minus EOLs). +-- +-- @tparam string blk +-- @treturn {string,...} lines +local function split(blk) + local lines = {} + local i, nblk = 1, #blk + while i <= nblk do + local p, q, r, s = blk:find("([\r\n])([\r\n]?)", i) + if not p then + p = nblk + 1 + end + lines[#lines + 1] = blk:sub(i, p - 1) + i = p + 1 + if p < nblk and q > p and r ~= s then -- handle Lua-style CRLF, LFCR + i = i + 1 + end + end + return lines +end + +--- Post-lexing processing, can work on lexer table output. +function M.post_lex(toklist, seminfolist, toklnlist) + local lnow, sloc = 0, 0 + local function chk(ln) -- if a new line, count it as an SLOC + if ln > lnow then -- new line # must be > old line # + sloc = sloc + 1; lnow = ln + end + end + for i = 1, #toklist do -- enumerate over all tokens + local tok, info, ln + = toklist[i], seminfolist[i], toklnlist[i] + + if tok == "TK_KEYWORD" or tok == "TK_NAME" or -- significant + tok == "TK_NUMBER" or tok == "TK_OP" then + chk(ln) + + -- Both TK_STRING and TK_LSTRING may be multi-line, hence, a loop + -- is needed in order to mark off lines one-by-one. Since llex.lua + -- currently returns the line number of the last part of the string, + -- we must subtract in order to get the starting line number. + elseif tok == "TK_STRING" then -- possible multi-line + local t = split(info) + ln = ln - #t + 1 + for _ = 1, #t do + chk(ln); ln = ln + 1 + end + + elseif tok == "TK_LSTRING" then -- possible multi-line + local t = split(info) + ln = ln - #t + 1 + for j = 1, #t do + if t[j] ~= "" then chk(ln) end + ln = ln + 1 + end + -- Other tokens are comments or whitespace and are ignored. + end + end--for + print(srcfl..": "..sloc) -- display result + option.EXIT = true +end + +return M diff --git a/tools/luasrcdiet/utils.lua b/tools/luasrcdiet/utils.lua new file mode 100644 index 0000000..611e9de --- /dev/null +++ b/tools/luasrcdiet/utils.lua @@ -0,0 +1,30 @@ +--------- +-- General utility functions. +-- +-- **Note: This module is not part of public API!** +---- +local ipairs = ipairs +local pairs = pairs + +local M = {} + +--- Returns a new table containing the contents of all the given tables. +-- Tables are iterated using @{pairs}, so this function is intended for tables +-- that represent *associative arrays*. Entries with duplicate keys are +-- overwritten with the values from a later table. +-- +-- @tparam {table,...} ... The tables to merge. +-- @treturn table A new table. +function M.merge (...) + local result = {} + + for _, tab in ipairs{...} do + for key, val in pairs(tab) do + result[key] = val + end + end + + return result +end + +return M diff --git a/tools/luatool.py b/tools/luatool.py index 6358855..4f87995 100755 --- a/tools/luatool.py +++ b/tools/luatool.py @@ -16,6 +16,9 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin # Street, Fifth Floor, Boston, MA 02110-1301 USA. +# version 0.6.4 based version +# Version 0.8.0 upgraded to python3 + import sys import serial @@ -25,7 +28,7 @@ import argparse from os.path import basename -version = "0.6.4" +version = "0.8.0" class TransportError(Exception): @@ -51,15 +54,19 @@ class AbstractTransport: def writer(self, data): self.writeln("file.writeline([==[" + data + "]==])\r") + def execute(self, data): + self.writeln(data + "\r") + def performcheck(self, expected): line = '' char = '' i = -1 - while char != chr(62): # '>' + while (len(char) == 0) or (ord(char) != 62): # '>' char = self.read(1) + #print(ord(char)) if char == '': raise Exception('No proper answer from MCU') - if char == chr(13) or char == chr(10): # LF or CR + if ord(char) == 13 or ord(char) == 10: # LF or CR if line != '': line = line.strip() if line+'\r' == expected: @@ -78,8 +85,8 @@ class AbstractTransport: raise Exception('Error sending data to MCU\r\n\r\n') line = '' else: - line += char - if char == chr(62) and expected[i] == char: + line += char.decode("utf-8") + if ord(char) == 62 and expected[i] == char: char = '' i += 1 @@ -105,7 +112,7 @@ class SerialTransport(AbstractTransport): if len(data) > 0: sys.stdout.write("\r\n->") sys.stdout.write(data.split("\r")[0]) - self.serial.write(data) + self.serial.write(str.encode(data)) sleep(self.delay) if check > 0: self.performcheck(data) @@ -142,7 +149,7 @@ class TcpSocketTransport(AbstractTransport): if len(data) > 0: sys.stdout.write("\r\n->") sys.stdout.write(data.split("\r")[0]) - self.socket.sendall(data) + self.socket.sendall(str.encode(data)) if check > 0: self.performcheck(data) else: @@ -186,6 +193,7 @@ if __name__ == '__main__': parser.add_argument('-e', '--echo', action='store_true', help='Echo output of MCU until script is terminated.') parser.add_argument('--delay', default=0.3, help='Delay in seconds between each write.', type=float) parser.add_argument('--delete', default=None, help='Delete a lua/lc file from device.') + parser.add_argument('--volatile', action='store_true', help='Volatile (nothing stored on ESP)') parser.add_argument('--ip', default=None, help='Connect to a telnet server on the device (--ip IP[:port])') args = parser.parse_args() @@ -197,7 +205,7 @@ if __name__ == '__main__': char = transport.read(1) if char == '' or char == chr(62): break - sys.stdout.write(char) + sys.stdout.write(str.encode(char)) sys.exit(0) if args.id: @@ -208,7 +216,7 @@ if __name__ == '__main__': if char == '' or char == chr(62): break if char.isdigit(): - id += char + id += char.decode("utf-8") print("\n"+id) sys.exit(0) @@ -218,10 +226,10 @@ if __name__ == '__main__': fn = "" while True: char = transport.read(1) - if char == '' or char == chr(62): + if char == '' or len(char) == 0 or ord(char) == 62: break if char not in ['\r', '\n']: - fn += char + fn += str(char) else: if fn: file_list.append(fn.strip()) @@ -266,7 +274,7 @@ if __name__ == '__main__': sys.stderr.write("Upload starting\r\n") # remove existing file on device - if args.append==False: + if args.append==False and not args.volatile: if args.verbose: sys.stderr.write("Stage 1. Deleting old file from flash memory") transport.writeln("file.open(\"" + args.dest + "\", \"w\")\r") @@ -280,23 +288,31 @@ if __name__ == '__main__': # read source file line by line and write to device if args.verbose: sys.stderr.write("\r\nStage 2. Creating file in flash memory and write first line") - if args.append: - transport.writeln("file.open(\"" + args.dest + "\", \"a+\")\r") + if not args.volatile: + if args.append: + transport.writeln("file.open(\"" + args.dest + "\", \"a+\")\r") + else: + transport.writeln("file.open(\"" + args.dest + "\", \"w+\")\r") else: - transport.writeln("file.open(\"" + args.dest + "\", \"w+\")\r") + if args.verbose: + sys.stderr.write("\r\nStage 2. Directly execute the script...") line = f.readline() if args.verbose: sys.stderr.write("\r\nStage 3. Start writing data to flash memory...") while line != '': - transport.writer(line.strip()) + if args.volatile: + transport.execute(line.strip()) + else: + transport.writer(line.strip()) line = f.readline() # close both files f.close() if args.verbose: sys.stderr.write("\r\nStage 4. Flush data and closing file") - transport.writeln("file.flush()\r") - transport.writeln("file.close()\r") + if not args.volatile: + transport.writeln("file.flush()\r") + transport.writeln("file.close()\r") # compile? if args.compile: @@ -315,7 +331,8 @@ if __name__ == '__main__': if args.verbose: sys.stderr.write("\r\nEchoing MCU output, press Ctrl-C to exit") while True: - sys.stdout.write(transport.read(1)) + data = transport.read(1) + sys.stdout.write( data.decode("ascii") ) # close serial port transport.close() diff --git a/tools/remoteFlash.sh b/tools/remoteFlash.sh index c8d3446..a46fd35 100755 --- a/tools/remoteFlash.sh +++ b/tools/remoteFlash.sh @@ -1,49 +1,96 @@ #!/bin/bash -IP=$1 +MQTTSERVER=$1 +MQTTPREFIX=$2 +CUSTOMFILE=$3 FLASHTOOL=./tools/tcpFlash.py +TOOLDIR=tools/ +DIET=bin/luasrcdiet + +UPGRADEPREP=/tmp/upgradeCMD4clock.txt if [ ! -f $FLASHTOOL ]; then echo "Execute the script in root folder of the project" exit 2 fi -if [ "$IP" == "" ]; then - echo "IP address of ESP required" +if [[ "$MQTTPREFIX" == "" ]] || [[ "$MQTTSERVER" == "" ]]; then + echo "MQTTSERVER: ip address to mqtt server" + echo "MQTTPREFIX: configured prefex in MQTT of ESP required" echo "usage:" - echo "$0 " - echo "$0 192.168.0.2" + echo "$0 " + echo "$0 192.168.0.2 basetopic" exit 1 fi +# Prepare all files on host +if [[ "$CUSTOMFILE" == "" ]]; then + FILES="displayword.lua main.lua timecore.lua webpage.html webserver.lua wordclock.lua init.lua" + echo "Start Flasing ..." +else + FILES=$CUSTOMFILE + echo "Start Flasing $FILES ..." +fi + + +# Convert files, if necessary +if [ "$FILES" != "config.lua" ]; then + echo "Generate DIET version of the files" + OUTFILES="" + ROOTDIR=$PWD + cd $TOOLDIR + for f in $FILES; do + if [[ "$f" == *.lua ]] && [[ "$f" != init.lua ]]; then + echo "Compress $f ..." + out=$(echo "$f" | sed 's/.lua/_diet.lua/g') + $DIET ../$f -o ../diet/$out >> /dev/null + OUTFILES="$OUTFILES diet/$out" + else + OUTFILES="$OUTFILES $f" + fi + done + FILES=$OUTFILES + cd $ROOTDIR +fi + + # check the connection -echo "Searching $IP ..." -ping $IP -c 2 >> /dev/null +echo "Searching $MQTTPREFIX ..." +mosquitto_sub -h $MQTTSERVER -t "$MQTTPREFIX/#" -C 1 -v if [ $? -ne 0 ]; then - echo "Entered IP address: $IP is NOT online" + echo "Entered Wordclock address: $MQTTPREFIX on $MQTTSERVER is NOT online" exit 2 fi -echo "Upgrading $IP" +echo "Activate Telnet server ..." +TELNETIP="empty" +while [ "$TELNETIP" = "empty" ]; do + date + mosquitto_pub -h $MQTTSERVER -t "$MQTTPREFIX/cmd/telnet" -m "a" + TELNETIP=$(mosquitto_sub -h $MQTTSERVER -t "$MQTTPREFIX/telnet" -C 1 -W 30) +done -echo "stopWordclock()" > /tmp/wordClockCMD.txt -echo "uart.write(0, tostring(node.heap())" >> /tmp/wordClockCMD.txt -echo "c = string.char(0,0,128)" >> /tmp/wordClockCMD.txt -echo "w = string.char(0,0,0)" >> /tmp/wordClockCMD.txt -echo "ws2812.write(w:rep(4) .. c .. w:rep(15) .. c .. w:rep(9) .. c .. w:rep(30) .. c .. w:rep(41) .. c )" >> /tmp/wordClockCMD.txt -$FLASHTOOL -f /tmp/wordClockCMD.txt -t $IP -v +echo "Upgrading $MQTTPREFIX via telenet on $TELNETIP ..." +sleep 1 +echo "if (mlt ~= nil) then mlt:unregister() end" > $UPGRADEPREP +echo "uart.write(0, tostring(node.heap())" >> $UPGRADEPREP +echo "collectgarbage()" >> $UPGRADEPREP +echo "" >> $UPGRADEPREP +echo "download = string.char(0,0,64)" >> $UPGRADEPREP +echo "w = string.char(0,0,0)" >> $UPGRADEPREP +echo "ws2812.write(w:rep(4) .. download .. w:rep(15) .. download .. w:rep(9) .. download .. w:rep(30) .. download .. w:rep(41) .. download )" >> $UPGRADEPREP +echo "collectgarbage()" >> $UPGRADEPREP +$FLASHTOOL -f $UPGRADEPREP -t $TELNETIP -v -FILES="displayword.lua main.lua timecore.lua webpage.html webserver.lua wordclock.lua init.lua" - -echo "Start Flasing ..." for f in $FILES; do if [ ! -f $f ]; then echo "Cannot find $f" echo "place the terminal into the folder where the lua files are present" exit 1 fi - echo "------------- $f ------------" - $FLASHTOOL -t $IP -f $f + espFile=$(echo "$f" | sed 's;diet/;;g') + echo "------------- $espFile ------------" + $FLASHTOOL -t $TELNETIP -f $f -o $espFile if [ $? -ne 0 ]; then echo "STOOOOP" exit 1 @@ -51,6 +98,6 @@ for f in $FILES; do done echo "TODO: Reboot the ESP" -#echo "node.restart()" | nc $IP 80 +#echo "node.restart()" | nc $TELNETIP 23 exit 0 diff --git a/tools/stopController.lua b/tools/stopController.lua new file mode 100644 index 0000000..be33f8e --- /dev/null +++ b/tools/stopController.lua @@ -0,0 +1,2 @@ +node.restart() +if (initTimer ~= nil) then initTimer:unregister() end diff --git a/tools/tcpFlash.py b/tools/tcpFlash.py index 2c32002..f66ab57 100755 --- a/tools/tcpFlash.py +++ b/tools/tcpFlash.py @@ -43,13 +43,13 @@ def sendCmd(s, message, cleaningEnter=False): print "ERROR, received : " + reply return False -def main(nodeip, luafile, volatile=None): +def main(nodeip, luafile, volatile=None, outfile=None): if ( not os.path.isfile(luafile) ): print "The file " + luafile + " is not available" else: try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect((nodeip, 80)) + s.connect((nodeip, 23)) time.sleep(0.050) s.sendall("\n") # Receive the hello Message of answer of the ESP @@ -71,14 +71,15 @@ def main(nodeip, luafile, volatile=None): print "NOT communicating with an ESP8266 running LUA (nodemcu) firmware" s.close() sys.exit(3) - - sendCmd(s, "for i=0,5 do tmr.stop(i) end") - sendCmd(s, "collectgarbage()") + if (volatile is None): - print "Flashing " + luafile - sendCmd(s, "file.remove(\"" + luafile+"\");", True) + if (outfile is None): + print "Flashing " + luafile + outfile=luafile + else: + print "Flashing " + luafile + " as " + outfile sendCmd(s, "w= file.writeline", True) - sendCmd(s, "file.open(\"" + luafile + "\",\"w+\");", True) + sendCmd(s, "file.open(\"temp.lua\",\"w+\");", True) else: print "Executing " + luafile + " on nodemcu" @@ -94,7 +95,7 @@ def main(nodeip, luafile, volatile=None): print "add a space at the end" if (volatile is None): - if (not sendCmd(s, "w([[" + l + "]]);")): + if (not sendCmd(s, "w([==[" + l + "]==]);")): print "Cannot write line " + str(i) s.close() sys.exit(4) @@ -112,13 +113,18 @@ def main(nodeip, luafile, volatile=None): if (not sendCmd(s, "file.close();")): print "Cannot close the file" sys.exit(4) + + sendCmd(s, "file.remove(\"" + outfile +"\");", True) + if (not sendCmd(s, "file.rename(\"temp.lua\",\""+ outfile + "\")")): + print "Cannot move temporary file to " + outfile + # Check if the file exists: - if (not sendRecv(s, "=file.exists(\"" + luafile + "\")", "true")): - print("Cannot send " + luafile + " to the ESP") + if (not sendRecv(s, "=file.exists(\"" + outfile + "\")", "true")): + print("Cannot send " + outfile + " to the ESP") sys.exit(4) else: - print("Updated " + luafile + " successfully") + print("Updated " + outfile + " successfully") else: print("Send " + luafile + " successfully") @@ -133,12 +139,16 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-t', '--target', help='IP address or dns of the ESP to flash') parser.add_argument('-f', '--file', help='LUA file, that should be updated') + parser.add_argument('-o', '--outfile', help='LUA file name on the microcontroller (default: same name as on host)') parser.add_argument('-v', '--volatile', help='File is executed at the commandline', action='store_const', const=1) args = parser.parse_args() - - if (args.target and args.file and args.volatile): - main(args.target, args.file, args.volatile) + if (args.target and args.file and args.volatile and args.outfile): + main(args.target, args.file, args.volatile, args.outfile) + elif (args.target and args.file and args.outfile): + main(args.target, args.file, None, args.outfile) + elif (args.target and args.file and args.volatile): + main(args.target, args.file, args.volatile) elif (args.target and args.file): main(args.target, args.file) else: diff --git a/webpage.html b/webpage.html index 25d1c80..7266fa7 100644 --- a/webpage.html +++ b/webpage.html @@ -48,15 +48,6 @@ Please note that all settings are mandatory

SNTP ServerServer to sync the time with. Only one ntp server is allowed. Offset to UTC timeDefine the offset to UTC time in hours. For example +1 hour for Germany Foreground ColorLED Color for all minutes, divisible by five -Background ColorBackground LED Color -1. Minute ColorFirst minute after -2. Minute ColorSecond minute after -3. Minute ColorThird minute after -4. Minute ColorFourth minute after -Three quaterDreiviertel Joa/nei -Invert lines 4-6invert -Adjust brightnesAdjust brightness of LEDs -
diff --git a/webserver.lua b/webserver.lua index 5a7d178..77b87fd 100644 --- a/webserver.lua +++ b/webserver.lua @@ -1,8 +1,7 @@ ---TODO: -configFile="config.lua" - -httpSending=false -sentBytes=0 +-- Webserver +local configFile="config.lua" +local httpSending=false +local sentBytes=0 function sendPage(conn, nameOfFile, replaceMap) collectgarbage() print("Sending " .. nameOfFile .. " " .. sentBytes .. "B already; " .. node.heap() .. "B in heap") @@ -65,43 +64,26 @@ function sendPage(conn, nameOfFile, replaceMap) --reset amount of sent bytes, as we reached the end sentBytes=0 -- send the rest - conn:send(buf) - print("Sent rest") + if (string.len(buf) > 0) then + conn:send(buf) + print("Sent rest") + end end end function fillDynamicMap() replaceMap = {} ssid, _ = wifi.sta.getconfig() - - if (ssid == nil) then - ssid="Not set" - end - if (sntpserverhostname == nil) then - sntpserverhostname="ptbtime1.ptb.de" - end - if (timezoneoffset == nil) then - timezoneoffset=1 - end + if (ssid == nil) then return replaceMap end + if (sntpserverhostname == nil) then sntpserverhostname="ptbtime1.ptb.de" end + if (timezoneoffset == nil) then timezoneoffset=1 end -- Set the default color, if nothing is set - if (color == nil) then - color=string.char(0,0,250) - end - if (color1 == nil) then - color1=color - end - if (color2 == nil) then - color2=color - end - if (color3 == nil) then - color3=color - end - if (color4 == nil) then - color4=color - end - if (colorBg == nil) then - colorBg=string.char(0,0,0) -- black is the default background color - end + if (color == nil) then color=string.char(0,0,250) end + if (color1 == nil) then color1=color end + if (color2 == nil) then color2=color end + if (color3 == nil) then color3=color end + if (color4 == nil) then color4=color end + if (colorBg == nil) then colorBg=string.char(0,0,0) end local hexColor = "#" .. string.format("%02x",string.byte(color,2)) .. string.format("%02x",string.byte(color,1)) .. string.format("%02x",string.byte(color,3)) local hexColor1 = "#" .. string.format("%02x",string.byte(color1,2)) .. string.format("%02x",string.byte(color1,1)) .. string.format("%02x",string.byte(color1,3)) local hexColor2 = "#" .. string.format("%02x",string.byte(color2,2)) .. string.format("%02x",string.byte(color2,1)) .. string.format("%02x",string.byte(color2,3)) @@ -125,23 +107,6 @@ function fillDynamicMap() return replaceMap end -function stopWordclock() - print("Stop all Wordclock") - -- Stop all - for i=0,5 do tmr.stop(i) end - -- unload all other functions - -- grep function *.lua | grep -v webserver | grep -v init.lua | grep -v main.lua | cut -f 2 -d ':' | grep "^function" | sed "s/function //g" | grep -o "^[a-zA-Z0-9\_]*" - updateColor = nil - drawLEDs = nil - round = nil - generateLEDs = nil - isSummerTime = nil - getUTCtime = nil - getTime = nil - display_timestat = nil - collectgarbage() -end - function startWebServer() srv=net.createServer(net.TCP) srv:listen(80,function(conn) @@ -152,20 +117,10 @@ function startWebServer() end if (payload:find("GET /") ~= nil) then httpSending=true - stopWordclock() + if (color == nil) then + color=string.char(0,128,0) + end ws2812.write(string.char(0,0,0):rep(56) .. color:rep(2) .. string.char(0,0,0):rep(4) .. color:rep(2) .. string.char(0,0,0):rep(48)) - -- Start Time after 3 minute - tmr.alarm(5, 180000, 0 ,function() - dependModules = { "timecore" , "wordclock", "displayword" } - for _,mod in pairs(dependModules) do - print("Loading " .. mod) - mydofile(mod) - end - -- Start the time Thread again - tmr.alarm(1, 20000, 1 ,function() - displayTime() - end) - end) if (sendPage ~= nil) then print("Sending webpage.html (" .. tostring(node.heap()) .. "B free) ...") -- Load the sendPagewebcontent @@ -280,18 +235,24 @@ function startWebServer() print("Rename config") if (file.rename(configFile .. ".new", configFile)) then print("Successfully") - tmr.alarm(3, 20, 0 ,function() + local mytimer = tmr.create() + mytimer:register(50, tmr.ALARM_SINGLE, function (t) replaceMap=fillDynamicMap() replaceMap["$ADDITIONAL_LINE"]="

New configuration saved

" print("Send success to client") sendPage(conn, "webpage.html", replaceMap) + t:unregister() end) + mytimer:start() else - tmr.alarm(3, 20, 0 ,function() + local mytimer = tmr.create() + mytimer:register(50, tmr.ALARM_SINGLE, function (t) replaceMap=fillDynamicMap() replaceMap["$ADDITIONAL_LINE"]="

ERROR

" sendPage(conn, "webpage.html", replaceMap) + t:unregister() end) + mytimer:start() end else replaceMap=fillDynamicMap() @@ -329,4 +290,26 @@ function startWebServer() end) end ---FileView done. + + +-- start the webserver module +collectgarbage() +wifi.setmode(wifi.SOFTAP) +cfg={} +cfg.ssid="wordclock" +cfg.pwd="wordclock" +wifi.ap.config(cfg) + +-- Write the buffer to the LEDs +local color=string.char(0,128,0) +local white=string.char(0,0,0) +local ledBuf= white:rep(6) .. color .. white:rep(7) .. color:rep(3) .. white:rep(44) .. color:rep(3) .. white:rep(50) +ws2812.write(ledBuf) +color=nil +white=nil +ledBuf=nil + +print("Waiting in access point >wordclock< for Clients") +print("Please visit 192.168.4.1") +startWebServer() +collectgarbage() \ No newline at end of file diff --git a/wordclock.lua b/wordclock.lua index ef3136e..93567c7 100755 --- a/wordclock.lua +++ b/wordclock.lua @@ -1,21 +1,25 @@ -- Revese engeeniered code of display_wc_ger.c by Vlad Tepesch --- See https://www.mikrocontroller.net/articles/Word_Clock_Variante_1#Download +-- See https://www.mikrocontroller.net/articles/Word_cl_Variante_1#Download +local M +do --- @fn display_timestat +-- @fn wc_timestat -- Return the leds to use the granuality is 5 minutes -- @param hours the current hours (0-23) -- @param minutes the current minute (0-59) -- @param longmode (optional parameter) 0: no long mode, 1: long mode (itis will be set) -function display_timestat(hours, minutes, longmode) +local timestat=function (hours, minutes, longmode) if (longmode == nil) then longmode=0 end -- generate an empty return type - local ret = { it=0, is=0, fiveMin=0, tenMin=0, after=0, before=0, threeHour=0, quater=0, threequater=0, half=0, s=0, - one=0, oneLong=0, two=0, three=0, four=0, five=0, six=0, seven=0, eight=0, nine=0, ten=0, eleven=0, twelve=0, - twenty=0, - clock=0, sr_nc=0, min1=0, min2=0, min3=0, min4=0 } + -- Values: it, is, 5 minutes, 10 minutes, afer, before, three hour, quarter, dreiviertel, half, s + -- hours: one, one Long, two, three, four, five, six, seven, eight, nine, ten, eleven, twelve + -- Special ones: twenty, clock, minute 1 flag, minute 2 flag, minute 3 flag, minute 4 flag + local ret = { it=0, is=0, m5=0, m10=0, ha=0, hb=0, h3=0, hq=0, h3q=0, half=0, s=0, + h1=0, h1l=0, h2=0, h3=0, h4=0, h5=0, h6=0, h7=0, h8=0, h9=0, h10=0, h11=0, h12=0, + m20=0, cl=0, m1=0, m2=0, m3=0, m4=0 } -- return black screen if there is no real time given if (hours == nil or minutes == nil) then @@ -38,61 +42,61 @@ function display_timestat(hours, minutes, longmode) -- Handle minutes if (minutes > 0) then if (minutes==1) then - ret.fiveMin=1 - ret.after=1 + ret.m5=1 + ret.ha=1 elseif (minutes==2) then - ret.tenMin=1 - ret.after=1 + ret.m10=1 + ret.ha=1 elseif (minutes==3) then - ret.quater=1 - ret.after=1 + ret.hq=1 + ret.ha=1 elseif (minutes==4) then - ret.twenty=1 - ret.after=1 + ret.m20=1 + ret.ha=1 elseif (minutes==5) then - ret.fiveMin=1 + ret.m5=1 ret.half=1 - ret.before=1 + ret.hb=1 elseif (minutes==6) then ret.half=1 elseif (minutes==7) then - ret.fiveMin=1 + ret.m5=1 ret.half=1 - ret.after=1 + ret.ha=1 elseif (minutes==8) then - ret.twenty=1 - ret.before=1 + ret.m20=1 + ret.hb=1 elseif (minutes==9) then -- Hande if three quater or quater before is displayed - if (threequater ~= nil) then - ret.threequater=1 + if ((threequater ~= nil) and (threequater==true or threequater=="on")) then + ret.h3q=1 else - ret.quater = 1 - ret.before = 1 + ret.hq = 1 + ret.hb = 1 end elseif (minutes==10) then - ret.tenMin=1 - ret.before=1 + ret.m10=1 + ret.hb=1 elseif (minutes==11) then - ret.fiveMin=1 - ret.before=1 + ret.m5=1 + ret.hb=1 end if (minutes > 4) then hours=hours+1 end else - ret.clock=1 + ret.cl=1 end - -- Display the minutes as as extra gimmic on min1 to min 4 to display the cut number + -- Display the minutes as as extra gimmic on m1 to min 4 to display the cut number if (minutesLeds==1) then - ret.min1=1 + ret.m1=1 elseif (minutesLeds==2) then - ret.min2=1 + ret.m2=1 elseif (minutesLeds==3) then - ret.min3=1 + ret.m3=1 elseif (minutesLeds==4) then - ret.min4=1 + ret.m4=1 end -- handle hours @@ -106,33 +110,90 @@ function display_timestat(hours, minutes, longmode) if (hours == 1) then if ((ret.it == 1) and (ret.half == 0) ) then - ret.one=1 + ret.h1=1 else - ret.oneLong=1 + ret.h1l=1 end elseif (hours == 2) then - ret.two=1 + ret.h2=1 elseif (hours == 3) then - ret.three=1 + ret.h3=1 elseif (hours == 4) then - ret.four=1 + ret.h4=1 elseif (hours == 5) then - ret.five=1 + ret.h5=1 elseif (hours == 6) then - ret.six=1 + ret.h6=1 elseif (hours == 7) then - ret.seven=1 + ret.h7=1 elseif (hours == 8) then - ret.eight=1 + ret.h8=1 elseif (hours == 9) then - ret.nine=1 + ret.h9=1 elseif (hours == 10) then - ret.ten=1 + ret.h10=1 elseif (hours == 11) then - ret.eleven=1 + ret.h11=1 elseif (hours == 12) then - ret.twelve=1 + ret.h12=1 end collectgarbage() return ret end + + +-- Logic to display Mqtt +function temp(dw, rgbBuffer, invertRows, dispTemp) +if (dispTemp ~= nil) then + -- Values: it, is, 5 minutes, 10 minutes, afer, before, three hour, quarter, dreiviertel, half, s + -- hours: one, one Long, two, three, four, five, six, seven, eight, nine, ten, eleven, twelve + -- Special ones: twenty, clock, minute 1 flag, minute 2 flag, minute 3 flag, minute 4 flag + local ret = { it=0, is=0, m5=0, m10=0, ha=0, hb=0, h3=0, hq=0, h3q=0, half=0, s=0, + h1=0, h1l=0, h2=0, h3=0, h4=0, h5=0, h6=0, h7=0, h8=0, h9=0, h10=0, h11=0, h12=0, + m20=0, cl=0, m1=0, m2=0, m3=0, m4=0 } + + print("Mqtt Display of temperature: " .. tostring(dispTemp) ) + if (dispTemp == 1) or (dispTemp == -1) then + ret.h1=1 + elseif (dispTemp == 2) or (dispTemp == -2) then + ret.h2=1 + elseif (dispTemp == 3) or (dispTemp == -3) then + ret.h3=1 + elseif (dispTemp == 4) or (dispTemp == -4) then + ret.h4=1 + elseif (dispTemp == 5) or (dispTemp == -5) then + ret.h5=1 + elseif (dispTemp == 6) or (dispTemp == -6) then + ret.h6=1 + elseif (dispTemp == 7) or (dispTemp == -7) then + ret.h7=1 + elseif (dispTemp == 8) or (dispTemp == -8) then + ret.h8=1 + elseif (dispTemp == 9) or (dispTemp == -9) then + ret.h9=1 + elseif (dispTemp == 10) or (dispTemp == -10) then + ret.h10=1 + elseif (dispTemp == 11) or (dispTemp == -11) then + ret.h11=1 + elseif (dispTemp == 12) or (dispTemp == -12) then + ret.h12=1 + else + -- over or under temperature + end + local col=string.char(128,0,0) -- red; positive degrees + if (dispTemp < 0) then + col=string.char(0,0,128) -- blue; negative degrees + end + return ret, col +else + return nil, nil +end + +end +-- Pack everything into a module +M = { + timestat = timestat, + temp = temp +} +end +wc = M