diff --git a/.gitignore b/.gitignore
index 0ac4a73..40db714 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
wlancfg.lua
+config.lua
*.swp
unit/testTimesMarchOctoberWithAllSeconds.lua
diff --git a/Readme.md b/Readme.md
index 751ac1d..4b0038d 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,6 +1,7 @@
# ESP Wordclock
## Setup
+### Initial Setup
Install the firmware on the ESP:
The ESP must be set into the bootloader mode, like [this](https://www.ccc-mannheim.de/wiki/ESP8266#Boot_Modi)
@@ -10,11 +11,10 @@ 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:
@@ -22,5 +22,60 @@ Then disconnect the serial terminal and copy the required files to the microcont
./tools/initialFlash.sh /dev/ttyUSB0
-## Internal Setup
+### Upgrade
+
+Determine the IP address of your clock and execute the following script:
+
+./tools/remoteFlash.sh IP-Address
+
+
+## Hardware Setup
+Mandatory:
* GPIO2 LEDs
+* GPIO0 Bootloader (at start)
+* GPIO0 factory reset (long during operation)
+Optinal:
+* ADC VT93N2, 48k light resistor
+* GPIO4 DS18B20 Temperatur sensor
+
+## MQTT Interface
+### Status
+* **basetopic**/brightness **Current brightness in percent**
+* **basetopic**/background **Current background color**
+* **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/telnet
+ * ignored **Stop MQTT server 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/displayword.lua b/displayword.lua
index 3d12716..b860ce4 100644
--- a/displayword.lua
+++ b/displayword.lua
@@ -1,36 +1,31 @@
-- Module filling a buffer, sent to the LEDs
-
-function updateColor(data)
- if (data.usedCharacters <= data.charsPerMinute) then
- if (data.words.min1 == 1 or data.words.min2 == 1 or data.words.min3 == 1 or data.words.min4 == 1) then
- return data.colorMin1
- else
- return data.colorFg
- end
- elseif (data.usedCharacters <= data.charsPerMinute*2) then
- if (data.words.min2 == 1 or data.words.min3 == 1 or data.words.min4 == 1) then
- return data.colorMin2
- else
- return data.colorFg
- end
- elseif (data.usedCharacters <= data.charsPerMinute*3) then
- if (data.words.min3 == 1 or data.words.min4 == 1) then
- return data.colorMin3
- else
- return data.colorFg
- end
- elseif (data.usedCharacters > data.charsPerMinute*3) then
- if (data.words.min4 == 1) then
- return data.colorMin4
- else
- return data.colorFg
- end
+local M
+do
+local updateColor = function (data)
+ if (data.amountOfChars > 0) then
+ local div = tonumber(data.drawnCharacters/data.amountOfChars)
+ if (div < 1) then
+ return data.colorFg
+ elseif (div < 2) then
+ return data.colorMin1
+ elseif (div < 3) then
+ return data.colorMin2
+ elseif (div < 4) then
+ return data.colorMin3
+ elseif (div < 5) then
+ return data.colorMin4
+ else
+ return data.colorFg
+ end
else
- return data.colorFg
+ return data.colorFg
end
end
-function drawLEDs(data, numberNewChars)
+local drawLEDs = function(data, numberNewChars)
+ if (numberNewChars == nil) then
+ numberNewChars=0
+ end
local tmpBuf=nil
for i=1,numberNewChars do
if (tmpBuf == nil) then
@@ -38,165 +33,311 @@ function drawLEDs(data, numberNewChars)
else
tmpBuf=tmpBuf .. updateColor(data)
end
- data.usedCharacters=data.usedCharacters+1
-
+ data.drawnCharacters=data.drawnCharacters+1
end
return tmpBuf
end
+-- Utility function for round
+local round = function(num)
+ under = math.floor(num)
+ upper = math.floor(num) + 1
+ underV = -(under - num)
+ upperV = upper - num
+ if (upperV > underV) then
+ return under
+ else
+ return upper
+ end
+end
+
+local data={}
+
-- Module displaying of the words
-function generateLEDs(words, colorFg, colorMin1, colorMin2, colorMin3, colorMin4, characters)
+local generateLEDs = function(words, colorBg, colorFg, colorMin1, colorMin2, colorMin3, colorMin4, invertRows, amountOfChars)
-- Set the local variables needed for the colored progress bar
- data={}
- data.charsPerMinute=math.floor(characters/3) -- devide by three (Minute 1 to Minute 3, Minute 4 takes the last chars)
- data.words=words
- data.colorFg=colorFg
- data.colorMin1=colorMin1
- data.colorMin2=colorMin2
- data.colorMin3=colorMin3
- data.colorMin4=colorMin4
- data.usedCharacters=0
+ if (words == nil) then
+ return nil
+ end
+ if (invertRows == nil) then
+ invertRows=false
+ end
+
+ local minutes=1
+ if (words.min1 == 1) then
+ minutes = minutes + 1
+ elseif (words.min2 == 1) then
+ minutes = minutes + 2
+ elseif (words.min3 == 1) then
+ minutes = minutes + 3
+ elseif (words.min4 == 1) then
+ minutes = minutes + 4
+ end
+ -- always set a foreground value
+ if (colorFg == nil) then
+ colorFg = string.char(255,255,255)
+ end
+
+ if (amountOfChars ~= nil) then
+ data.amountOfChars = amountOfChars/minutes
+ else
+ data.amountOfChars = 0
+ end
+
+ if ( (adc ~= nil) and (words.briPercent ~= 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(colorFg,1) * briPercent / 100, string.byte(colorFg,2) * briPercent / 100, string.byte(colorFg,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)
+ else
+ -- devide by five (Minute 0, Minute 1 to Minute 4 takes the last chars)
+ data.colorFg=colorFg
+ data.colorMin1=colorMin1
+ data.colorMin2=colorMin2
+ data.colorMin3=colorMin3
+ data.colorMin4=colorMin4
+ end
+ data.drawnCharacters=0
+ local charsPerLine=11
+
+ -- Space / background has no color by default
local space=string.char(0,0,0)
- -- update the background color, if set
+
+ -- Background color must always be set
if (colorBg ~= nil) then
- space = colorBg
+ space = colorBg
+ else
+ colorBg = space
end
-- Set the foreground color as the default color
- local buf=colorFg
-
+ local buf=data.colorFg
+ local line=space
-- line 1----------------------------------------------
- if (words.itis == 1) then
- buf=drawLEDs(data,2) -- ES
- print(tostring(buf))
- -- K fill character
- buf=buf .. space:rep(1)
- buf=buf .. drawLEDs(data,3) -- IST
- -- L fill character
- buf=buf .. space:rep(1)
- else
- buf=space:rep(7)
+ if (rowbgColor[1] ~= nil) then
+ space = rowbgColor[1]
end
- if (words.fiveMin== 1) then
+ if (words.it==1) then
+ buf=drawLEDs(data,2) -- ES
+ else
+ buf=space:rep(2)
+ end
+-- K fill character
+buf=buf .. space:rep(1)
+ if (words.is == 1) then
+ buf=buf .. drawLEDs(data,3) -- IST
+ else
+ buf=buf .. space:rep(3)
+ 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)
end
-- line 2-- even row (so inverted) --------------------
- if (words.twenty == 1) then
- buf= buf .. drawLEDs(data,7) -- ZWANZIG
+ if (rowbgColor[2] ~= nil) then
+ space = rowbgColor[2]
else
- buf= buf .. space:rep(7)
- end
+ space = colorBg
+ end
if (words.tenMin == 1) then
- buf= buf .. drawLEDs(data,4) -- ZEHN
+ line= drawLEDs(data,4) -- ZEHN
else
- buf= buf .. space:rep(4)
+ line= space:rep(4)
end
+ if (words.twenty == 1) then
+ line= line .. drawLEDs(data,7) -- ZWANZIG
+ else
+ line= line .. space:rep(7)
+ end
+ -- fill, the buffer
+ for i = 0,10 do
+ buf = buf .. line:sub((11-i)*3-2,(11-i)*3)
+ end
+
-- line3----------------------------------------------
+ if (rowbgColor[3] ~= nil) then
+ space = rowbgColor[3]
+ else
+ space = colorBg
+ end
if (words.threequater == 1) then
- buf= buf .. drawLEDs(data,11) -- Dreiviertel
+ line= drawLEDs(data,11) -- Dreiviertel
elseif (words.quater == 1) then
- buf= buf .. space:rep(4)
- buf= buf .. drawLEDs(data,7) -- VIERTEL
+ line= space:rep(4)
+ line= line .. drawLEDs(data,7) -- VIERTEL
else
- buf= buf .. space:rep(11)
+ line= space:rep(11)
end
+ -- fill, the buffer
+ buf = buf .. line
--line 4-------- even row (so inverted) -------------
- if (words.before == 1) then
- buf=buf .. space:rep(2)
- buf= buf .. drawLEDs(data,3) -- VOR
+ if (rowbgColor[4] ~= nil) then
+ space = rowbgColor[4]
else
- buf= buf .. space:rep(5)
- end
+ space = colorBg
+ end
if (words.after == 1) then
- buf= buf .. drawLEDs(data,4) -- NACH
- buf= buf .. space:rep(2) -- TG
+ line= space:rep(2) -- TG
+ line= line .. drawLEDs(data,4) -- NACH
else
- buf= buf .. space:rep(6)
+ line= space:rep(6)
+ end
+ if (words.before == 1) then
+ line= line .. drawLEDs(data,3) -- VOR
+ line= line .. space:rep(2)
+ else
+ line= line .. space:rep(5)
+ end
+ if (invertRows == true) then
+ buf = buf .. line
+ else
+ for i = 0,10 do
+ buf = buf .. line:sub((11-i)*3-2,(11-i)*3)
+ end
end
------------------------------------------------
- if (words.half == 1) then
- buf= buf .. drawLEDs(data,4) -- HALB
- buf= buf .. space:rep(1) -- X
+ if (rowbgColor[5] ~= nil) then
+ space = rowbgColor[5]
else
- buf= buf .. space:rep(5)
+ space = colorBg
+ end
+ if (words.half == 1) then
+ line= drawLEDs(data,4) -- HALB
+ line= line .. space:rep(1) -- X
+ else
+ line= space:rep(5)
end
if (words.twelve == 1) then
- buf= buf .. drawLEDs(data,5) -- ZWOELF
- buf= buf .. space:rep(1) -- P
+ line= line .. drawLEDs(data,5) -- ZWOELF
+ line= line .. space:rep(1) -- P
else
- buf= buf .. space:rep(6)
+ line= line .. space:rep(6)
+ end
+ if (invertRows == true) then
+ for i = 0,10 do
+ buf = buf .. line:sub((11-i)*3-2,(11-i)*3)
+ end
+ else
+ buf=buf .. line
end
------------even row (so inverted) ---------------------
+ if (rowbgColor[6] ~= nil) then
+ space = rowbgColor[6]
+ else
+ space = colorBg
+ end
if (words.seven == 1) then
- buf= buf .. drawLEDs(data,6) -- SIEBEN
- buf= buf .. space:rep(5)
+ line= space:rep(5)
+ line= line .. drawLEDs(data,6) -- SIEBEN
elseif (words.oneLong == 1) then
- buf= buf .. space:rep(5)
- buf= buf .. drawLEDs(data,4) -- EINS
- buf= buf .. space:rep(2)
+ line= space:rep(2)
+ line= line .. drawLEDs(data,4) -- EINS
+ line= line .. space:rep(5)
elseif (words.one == 1) then
- buf= buf .. space:rep(6)
- buf= buf .. drawLEDs(data,3) -- EIN
- buf= buf .. space:rep(2)
+ line= space:rep(2)
+ line= line .. drawLEDs(data,3) -- EIN
+ line= line .. space:rep(6)
elseif (words.two == 1) then
- buf= buf .. space:rep(7)
- buf= buf .. drawLEDs(data,4) -- ZWEI
+ line= drawLEDs(data,4) -- ZWEI
+ line= line .. space:rep(7)
else
- buf= buf .. space:rep(11)
+ line= space:rep(11)
+ end
+ if (invertRows == true) then
+ buf = buf .. line
+ else
+ for i = 0,10 do
+ buf = buf .. line:sub((11-i)*3-2,(11-i)*3)
+ end
end
------------------------------------------------
+ if (rowbgColor[7] ~= nil) then
+ space = rowbgColor[7]
+ else
+ space = colorBg
+ 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
+ line= space:rep(1)
+ line= line .. drawLEDs(data,4) -- DREI
+ line= line .. space:rep(6)
+ elseif (words.five == 1) then
+ line= space:rep(7)
+ line= line .. drawLEDs(data,4) -- FUENF
else
- buf= buf .. space:rep(11)
+ line= space:rep(11)
end
+ buf = buf .. line
------------even row (so inverted) ---------------------
+ if (rowbgColor[8] ~= nil) then
+ space = rowbgColor[8]
+ else
+ space = colorBg
+ end
if (words.four == 1) then
- buf= buf .. drawLEDs(data,4) -- VIER
- buf= buf .. space:rep(7)
+ line= space:rep(7)
+ line= line .. drawLEDs(data,4) -- VIER
elseif (words.nine == 1) then
- buf= buf .. space:rep(4)
- buf= buf .. drawLEDs(data,4) -- NEUN
- buf= buf .. space:rep(3)
+ line= space:rep(3)
+ line= line .. drawLEDs(data,4) -- NEUN
+ line= line .. space:rep(4)
elseif (words.eleven == 1) then
- buf= buf .. space:rep(8)
- buf= buf .. drawLEDs(data,3) -- ELEVEN
+ line= drawLEDs(data,3) -- ELEVEN
+ line= line .. space:rep(8)
else
- buf= buf .. space:rep(11)
+ line= space:rep(11)
+ end
+
+ for i = 0,10 do
+ buf = buf .. line:sub((11-i)*3-2,(11-i)*3)
end
------------------------------------------------
+ if (rowbgColor[9] ~= nil) then
+ space = rowbgColor[9]
+ else
+ space = colorBg
+ end
if (words.eight == 1) then
- buf= buf .. space:rep(1)
- buf= buf .. drawLEDs(data,4) -- ACHT
- buf= buf .. space:rep(6)
+ line= space:rep(1)
+ line= line .. drawLEDs(data,4) -- ACHT
+ line= line .. space:rep(6)
elseif (words.ten == 1) then
- buf= buf .. space:rep(5)
- buf= buf .. drawLEDs(data,4) -- ZEHN
- buf= buf .. space:rep(2)
+ line= space:rep(5)
+ line= line .. drawLEDs(data,4) -- ZEHN
+ line= line .. space:rep(2)
else
- buf= buf .. space:rep(11)
+ line= space:rep(11)
end
+ buf = buf .. line
------------even row (so inverted) ---------------------
- if (words.clock == 1) then
- buf= buf .. drawLEDs(data,3) -- UHR
+ if (rowbgColor[10] ~= nil) then
+ space = rowbgColor[10]
else
- buf= buf .. space:rep(3)
- end
+ space = colorBg
+ end
if (words.six == 1) then
- buf= buf .. space:rep(2)
- buf= buf .. drawLEDs(data,5) -- SECHS
- buf= buf .. space:rep(1)
+ line= space:rep(1)
+ line= line .. drawLEDs(data,5) -- SECHS
+ line= line .. space:rep(2)
else
- buf= buf .. space:rep(8)
+ line= space:rep(8)
end
-
+ if (words.clock == 1) then
+ line= line .. drawLEDs(data,3) -- UHR
+ else
+ line= line .. space:rep(3)
+ end
+
+ for i = 0,10 do
+ buf = buf .. line:sub((11-i)*3-2,(11-i)*3)
+ end
+------ Minutes -----------
if (words.min1 == 1) then
buf= buf .. colorFg
else
@@ -220,3 +361,77 @@ function generateLEDs(words, colorFg, colorMin1, colorMin2, colorMin3, colorMin4
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 == "fiveMin") then
+ characters = characters + 4
+ elseif (key == "tenMin") then
+ characters = characters + 4
+ elseif (key == "after") then
+ characters = characters + 4
+ elseif (key == "before") then
+ characters = characters + 3
+ elseif (key == "threeHour") then
+ characters = characters + 4
+ elseif (key == "quater") then
+ characters = characters + 7
+ elseif (key == "threequater") then
+ characters = characters + 11
+ elseif (key == "half") then
+ characters = characters + 4
+ elseif (key == "one") then
+ characters = characters + 3
+ elseif (key == "oneLong") then
+ characters = characters + 4
+ elseif (key == "two") then
+ characters = characters + 4
+ elseif (key == "three") then
+ characters = characters + 4
+ elseif (key == "four") then
+ characters = characters + 4
+ elseif (key == "five") then
+ characters = characters + 4
+ elseif (key == "six") then
+ characters = characters + 4
+ elseif (key == "seven") then
+ characters = characters + 6
+ elseif (key == "eight") then
+ characters = characters + 4
+ elseif (key == "nine") then
+ characters = characters + 4
+ elseif (key == "ten") then
+ characters = characters + 4
+ elseif (key == "eleven") then
+ characters = characters + 3
+ elseif (key == "twelve") then
+ characters = characters + 5
+ elseif (key == "twenty") then
+ characters = characters + 7
+ elseif (key == "clock") then
+ characters = characters + 3
+ elseif (key == "sr_nc") 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
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 a6caac6..0000000
--- a/index.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-WordClock Setup Page
-
-Welcome to the WordClock
-
-$ADDITIONAL_LINE
-
\ No newline at end of file
diff --git a/init.lua b/init.lua
index 74d28a2..0d0f1f8 100644
--- a/init.lua
+++ b/init.lua
@@ -7,11 +7,13 @@ counter1=0
ws2812.write(string.char(0,0,0):rep(114))
tmr.alarm(2, 85, 1, function()
counter1=counter1+1
- ws2812.write(string.char(128,0,0):rep(counter1) .. string.char(0,0,0):rep(MAXLEDS - (counter1*2)) .. string.char(0,0,64):rep(counter1))
+ 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"
+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
@@ -34,15 +36,28 @@ end
function mydofile(mod)
if (file.open(mod .. ".lua")) then
dofile( mod .. ".lua")
- else
+ elseif (file.open(mod .. ".lc")) then
dofile(mod .. ".lc")
+ else
+ print("Error: " .. mod)
end
end
tmr.alarm(1, 5000, 0, function()
tmr.stop(2)
- if (file.open("main.lua")) then
+ if (
+ (file.open("main.lua")) or
+ (file.open("timecore.lua")) or
+ (file.open("wordclock.lua")) or
+ (file.open("displayword.lua")) or
+ (file.open("mqtt.lua")) or
+ (file.open("ds18b20.lua")) or
+ (file.open("telnet.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
@@ -54,3 +69,4 @@ tmr.alarm(1, 5000, 0, function()
print("No Main file found")
end
end)
+print("Init file end reached")
diff --git a/main.lua b/main.lua
index 1eb98a9..c624ff0 100644
--- a/main.lua
+++ b/main.lua
@@ -1,5 +1,7 @@
-- Main Module
+displayword = {}
+
function startSetupMode()
tmr.stop(0)
tmr.stop(1)
@@ -29,36 +31,62 @@ end
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!')
+ syncRunning=nil
end
)
+ end
end
+briPercent = 50
function displayTime()
- sec, usec = rtctime.get()
+ local sec, usec = rtctime.get()
-- Handle lazy programmer:
if (timezoneoffset == nil) then
timezoneoffset=0
end
- time = getTime(sec, timezoneoffset)
- words = display_timestat(time.hour, time.minute)
-
- local charactersOfTime = display_countwords_de(words)
- ledBuf = generateLEDs(words, color, color1, color2, color3, color4,
- charactersOfTime)
-
- print("Local time : " .. time.year .. "-" .. time.month .. "-" .. time.day .. " " .. time.hour .. ":" .. time.minute .. ":" .. time.second .. " in " .. charactersOfTime .. " chars")
-
- -- Write the buffer to the LEDs
- ws2812.write(ledBuf)
-
+ local time = getTime(sec, timezoneoffset)
+ local words = display_timestat(time.hour, time.minute)
+ if ((dim ~= nil) and (dim == "on")) then
+ words.briPercent=briPercent
+ if (words.briPercent ~= nil and words.briPercent < 3) then
+ words.briPercent=3
+ end
+ else
+ words.briPercent=nil
+ end
+ dofile("displayword.lc")
+ if (displayword ~= 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 characters = displayword.countChars(words)
+ ledBuf = displayword.generateLEDs(words, colorBg, color, color1, color2, color3, color4, invertRows, characters)
+ end
+ displayword = nil
+ if (ledBuf ~= nil) then
+ ws2812.write(ledBuf)
+ else
+ if ((colorBg ~= nil) and (color ~= nil)) then
+ ws2812.write(colorBg:rep(107) .. color:rep(3))
+ else
+ local space=string.char(0,0,0)
+ -- set FG to fix value:
+ colorFg = string.char(255,0,0)
+ ws2812.write(space:rep(107) .. colorFg:rep(3))
+ end
+ end
-- Used for debugging
if (clockdebug ~= nil) then
for key,value in pairs(words) do
@@ -68,7 +96,7 @@ function displayTime()
end
end
-- cleanup
- ledBuf=nil
+ briPercent=words.briPercent
words=nil
time=nil
collectgarbage()
@@ -80,18 +108,41 @@ function normalOperation()
-- Color is defined as GREEN, RED, BLUE
color=string.char(0,0,250)
end
+ print("Fg Color: " .. tostring(string.byte(color,1)) .. "x" .. tostring(string.byte(color,2)) .. "x" .. tostring(string.byte(color,3)) )
connect_counter=0
-- Wait to be connect to the WiFi access point.
- tmr.alarm(0, 1000, 1, function()
+ tmr.alarm(0, 500, 1, function()
connect_counter=connect_counter+1
if wifi.sta.status() ~= 5 then
print(connect_counter .. "/60 Connecting to AP...")
- if (connect_counter % 2 == 0) then
+ 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) .. wlanColor .. space:rep(4)
- buf= buf .. space:rep(3) .. wlanColor:rep(3)
+ local buf=space:rep(6)
+ if ((connect_counter % 5) >= 1) then
+ buf = buf .. wlanColor
+ else
+ buf = buf .. space
+ end
+ buf = buf .. space:rep(4)
+ buf= buf .. space:rep(3)
+ if ((connect_counter % 5) >= 3) then
+ buf = buf .. wlanColor
+ else
+ buf = buf .. space
+ end
+ if ((connect_counter % 5) >= 2) then
+ buf = buf .. wlanColor
+ else
+ buf = buf .. space
+ end
+ if ((connect_counter % 5) >= 0) then
+ buf = buf .. wlanColor
+ else
+ buf = buf .. space
+ end
+ buf = buf .. space:rep(100)
ws2812.write(buf)
else
ws2812.write(string.char(0,0,0):rep(114))
@@ -101,26 +152,43 @@ function normalOperation()
print('IP: ',wifi.sta.getip())
-- Here the WLAN is found, and something is done
print("Solving dependencies")
- local dependModules = { "timecore" , "wordclock", "displayword" }
+ local dependModules = { "timecore" , "wordclock", "mqtt" }
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, 20000, 1 ,function()
- displayTime()
- end)
+ setupCounter=5
+ tmr.alarm(1, 5000, 1 ,function()
+ if (setupCounter > 4) then
+ syncTimeFromInternet()
+ setupCounter=setupCounter-1
+ elseif (setupCounter > 3) then
+ if (startMqttClient ~= nil) then
+ 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
+ else
+ displayTime()
+ end
+ collectgarbage()
+ end)
+
+ -- sync the time every 5 minutes
+ tmr.alarm(2, 300003, 1 ,function()
+ syncTimeFromInternet()
+ displayTime()
+ end)
end
-- when no wifi available, open an accesspoint and ask the user
@@ -132,15 +200,37 @@ function normalOperation()
end
+function stopWordclock()
+ for i=0,5,1 do tmr.stop(i) end
+end
+
-------------------main program -----------------------------
ws2812.init() -- WS2812 LEDs initialized on GPIO2
if ( file.open("config.lua") ) then
--- Normal operation
wifi.setmode(wifi.STATION)
- dofile("config.lua")
+ mydofile("config")
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()
+ if (gpio.read(3) == 0) then
+ tmr.stop(1) -- stop the LED thread
+ 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)
+ if (btnCounter >= 110) then
+ file.remove("config.lua")
+ file.remove("config.lc")
+ node.restart()
+ end
+ end
+end)
diff --git a/mqtt.lua b/mqtt.lua
new file mode 100644
index 0000000..6247e08
--- /dev/null
+++ b/mqtt.lua
@@ -0,0 +1,169 @@
+-- Global variable
+m=nil
+mqttConnected = false
+-- Temp:
+t=nil
+rowbgColor= {}
+
+function handleSingleCommand(client, topic, data)
+ if (data == "ON") then
+ briPercent=100
+ m:publish(mqttPrefix .. "/clock", "ON", 0, 0)
+ elseif (data == "OFF") then
+ briPercent=0
+ m: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 * briPercent / 100, red * briPercent / 100, blue * briPercent / 100)
+ print("Updated BG: " .. tostring(red) .. "," .. tostring(green) .. "," .. tostring(blue) )
+ m:publish(mqttPrefix .. "/background", tostring(red) .. "," .. tostring(green) .. "," .. tostring(blue), 0, 0)
+ if (displayTime~= nil) then
+ displayTime()
+ end
+ else
+ if (tonumber(data) >= 0 and tonumber(data) <= 100) then
+ briPercent=tonumber(data)
+ m: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
+function parseBgColor(data, row)
+ 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
+ m:publish(mqttPrefix .. "/"..row, tostring(red) .. "," .. tostring(green) .. "," .. tostring(blue), 0, 0)
+ return string.char(green * briPercent / 100, red * briPercent / 100, blue * briPercent / 100)
+ else
+ return nil
+ end
+end
+
+function readTemp()
+ if (t ~= nil) then
+ addrs=t.addrs()
+ -- Total DS18B20 numbers
+ sensors=table.getn(addrs)
+ local temp1=0
+ if (sensors >= 1) then
+ temp1=t.read(addrs[0])
+ end
+ return temp1
+ else
+ return nil
+ end
+end
+
+-- MQTT extension
+function registerMqtt()
+ m = mqtt.Client("wordclock", 120)
+ -- on publish message receive event
+ m:on("message", function(client, topic, data)
+ print(topic .. ":" )
+ if data ~= nil then
+ print(data)
+ if (topic == (mqttPrefix .. "/cmd/single")) then
+ handleSingleCommand(client, topic, data)
+ 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
+ mqttConnected = false
+ for i=0,6,1 do tmr.stop(i) end
+ collectgarbage()
+ mydofile("telnet")
+ if (startTelnetServer ~= nil) then
+ startTelnetServer()
+ end
+ else
+ for i=1,10,1 do
+ if (string.match(topic, "row".. tostring(i) .."$")) then
+ rowbgColor[i] = parseBgColor(data, "row" .. tostring(i))
+ print("Updated row" .. tostring(i) )
+ return
+ end
+ end
+ end
+ end
+ end
+ end)
+
+ m:connect(mqttServer, 1883, 0, function(client)
+ print("MQTT is connected")
+ mqttConnected = true
+ -- subscribe topic with qos = 0
+ client:subscribe(mqttPrefix .. "/cmd/#", 0)
+ tmr.alarm(3, 1000, 0, function()
+ -- publish a message with data = hello, QoS = 0, retain = 0
+ client: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)
+ client:publish(mqttPrefix .. "/background", tostring(red) .. "," .. tostring(green) .. "," .. tostring(blue), 0, 0)
+ end)
+ end,
+ function(client, reason)
+ print("failed reason: " .. reason)
+ mqttConnected = false
+ end)
+end
+
+function startMqttClient()
+ if (mqttServer ~= nil and mqttPrefix ~= nil) then
+ registerMqtt()
+ print "Started MQTT client"
+ if (file.open("ds18b20.lc")) then
+ t=require("ds18b20")
+ t.setup(2) -- GPIO4
+ readTemp() -- read once, to setup chip
+ print "Setup temperature"
+ end
+ oldBrightness=0
+ oldTemp=0
+ tmr.alarm(5, 5001, 1 ,function()
+ if (mqttConnected) then
+ local temp = nil
+ if (t ~= nil) then
+ temp=readTemp()
+ print(tostring(temp) .. "°C")
+ end
+ if (oldBrightness ~= briPercent) then
+ m:publish(mqttPrefix .. "/brightness", tostring(briPercent), 0, 0)
+ elseif (temp ~= nil and temp ~= oldTemp) then
+ oldTemp = temp
+ m:publish(mqttPrefix .. "/temp", tostring(temp/100).."."..tostring(temp%100), 0, 0)
+ else
+ m:publish(mqttPrefix .. "/heap", tostring(node.heap()), 0, 0)
+ end
+ oldBrightness = briPercent
+ end
+ end)
+ end
+end
diff --git a/os/0x00000.bin b/os/0x00000.bin
index 19567c0..47bcfd4 100644
Binary files a/os/0x00000.bin and b/os/0x00000.bin differ
diff --git a/os/0x10000.bin b/os/0x10000.bin
index 20d0a8e..ac44e0e 100644
Binary files a/os/0x10000.bin and b/os/0x10000.bin differ
diff --git a/os/Readme.md b/os/Readme.md
index dccf967..c8a60f3 100644
--- a/os/Readme.md
+++ b/os/Readme.md
@@ -1,16 +1,20 @@
# Firmware was compiled with the following modules:
- * LUA_USE_BUILTIN_STRING // for string.xxx()
- * LUA_USE_BUILTIN_TABLE // for table.xxx()
- * LUA_USE_BUILTIN_COROUTINE // for coroutine.xxx()
- * LUA_USE_BUILTIN_MATH // for math.xxx(), partially work
- * LUA_USE_BUILTIN_DEBUG_MINIMAL // for debug.getregistry() and debug.traceback()
+ * 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
diff --git a/os/blank.bin b/os/blank.bin
new file mode 100644
index 0000000..7de9e36
--- /dev/null
+++ b/os/blank.bin
@@ -0,0 +1 @@
+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
\ No newline at end of file
diff --git a/os/esp_init_data_default.bin b/os/esp_init_data_default.bin
new file mode 100644
index 0000000..3db53cc
Binary files /dev/null and b/os/esp_init_data_default.bin differ
diff --git a/os/esptool.py b/os/esptool.py
index 93ab071..6a9a97f 100755
--- a/os/esptool.py
+++ b/os/esptool.py
@@ -1,15 +1,14 @@
#!/usr/bin/env python
#
-# ESP8266 ROM Bootloader Utility
-# https://github.com/themadinventor/esptool
-#
-# Copyright (C) 2014 Fredrik Ahlberg
+# 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
# Foundation; either version 2 of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
@@ -17,18 +16,179 @@
# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
# Street, Fifth Floor, Boston, MA 02110-1301 USA.
-import sys
-import struct
-import serial
-import math
-import time
+from __future__ import division, print_function
+
import argparse
+import base64
+import binascii
+import copy
+import hashlib
+import inspect
+import io
+import itertools
import os
-import subprocess
+import shlex
+import string
+import struct
+import sys
+import time
+import zlib
-class ESPROM:
+try:
+ import serial
+except ImportError:
+ print("Pyserial is not installed for %s. Check the README for installation instructions." % (sys.executable))
+ raise
- # These are the currently known commands supported by the ROM
+# 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__ = "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
+
+
+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
@@ -39,9 +199,35 @@ class ESPROM:
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
@@ -52,555 +238,3781 @@ class ESPROM:
# Initial state for the checksum routine
ESP_CHECKSUM_MAGIC = 0xef
- # OTP ROM addresses
- ESP_OTP_MAC0 = 0x3ff00050
- ESP_OTP_MAC1 = 0x3ff00054
+ # Flash sector size, minimum unit of erase.
+ FLASH_SECTOR_SIZE = 0x1000
- # Sflash stub: an assembly routine to read from spi flash and send to host
- SFLASH_STUB = "\x80\x3c\x00\x40\x1c\x4b\x00\x40\x21\x11\x00\x40\x00\x80" \
- "\xfe\x3f\xc1\xfb\xff\xd1\xf8\xff\x2d\x0d\x31\xfd\xff\x41\xf7\xff\x4a" \
- "\xdd\x51\xf9\xff\xc0\x05\x00\x21\xf9\xff\x31\xf3\xff\x41\xf5\xff\xc0" \
- "\x04\x00\x0b\xcc\x56\xec\xfd\x06\xff\xff\x00\x00"
+ UART_DATE_REG_ADDR = 0x60000078
- def __init__(self, port = 0, baud = ESP_ROM_BAUD):
- self._port = serial.Serial(port, baud)
+ CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000 # This ROM address has a different value on each chip model
- """ Read bytes from the serial port while performing SLIP unescaping """
- def read(self, length = 1):
- b = ''
- while len(b) < length:
- c = self._port.read(1)
- if c == '\xdb':
- c = self._port.read(1)
- if c == '\xdc':
- b = b + '\xc0'
- elif c == '\xdd':
- b = b + '\xdb'
- else:
- raise Exception('Invalid SLIP escape')
- else:
- b = b + c
- return b
+ 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/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 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):
+ 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:
- # Construct and send request
- 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)
- """ Perform a connection test """
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)
- # RTS = CH_PD (i.e reset)
- # DTR = GPIO0
- self._port.setRTS(True)
- self._port.setDTR(True)
- self._port.setRTS(False)
- time.sleep(0.1)
- self._port.setDTR(False)
+ 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)
- self._port.timeout = 0.5
- for i in xrange(10):
+ 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._setDTR(False) # IO0=HIGH, done
+
+ for _ in range(5):
try:
- self._port.flushInput()
+ self.flush_input()
self._port.flushOutput()
self.sync()
- self._port.timeout = 5
- return
- except:
- time.sleep(0.1)
- raise Exception('Failed to connect')
+ 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):
- mac0 = esp.read_reg(esp.ESP_OTP_MAC0)
- mac1 = esp.read_reg(esp.ESP_OTP_MAC1)
- if ((mac1 >> 16) & 0xff) == 0:
+ """ 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)
+ if (mac3 != 0):
+ oui = ((mac3 >> 16) & 0xff, (mac3 >> 8) & 0xff, mac3 & 0xff)
+ elif ((mac1 >> 16) & 0xff) == 0:
oui = (0x18, 0xfe, 0x34)
elif ((mac1 >> 16) & 0xff) == 1:
oui = (0xac, 0xd0, 0x74)
else:
- raise Exception("Unknown OUI")
+ raise FatalError("Unknown OUI")
return oui + ((mac1 >> 8) & 0xff, mac1 & 0xff, (mac0 >> 24) & 0xff)
- """ 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 = esp.read_reg(0x60000240)
- self.flash_finish(False)
- return flash_id
+ def get_erase_size(self, offset, size):
+ """ Calculate an erase size given a specific size in bytes.
- """ Read SPI flash """
- def flash_read(self, offset, size, count = 1):
- # Create a custom stub
- stub = struct.pack('> 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):
+ raise NotSupportedError(self, "chip_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)
+
+ def get_erase_size(self, offset, size):
+ return size
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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):
+ """ These are constants related to software ESP bootloader, working with 'v2' image files """
+
+ # First byte of the "v2" application image
+ IMAGE_V2_MAGIC = 0xea
+
+ # First 'segment' value in a "v2" application image, appears to be a constant version value?
+ IMAGE_V2_SEGMENT = 4
+
+
+def LoadFirmwareImage(chip, filename):
+ """ Load a firmware image. Can be for any supported SoC.
+
+ 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:
+ 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 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:
+ 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 """
+ segment_data = self.maybe_patch_segment_data(f, segment.data)
+ 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(ESP8266ROMFirmwareImage, self).__init__()
self.flash_mode = 0
self.flash_size_freq = 0
+ self.version = 1
- if filename is not None:
- f = file(filename, 'rb')
- (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack(' 16:
- raise Exception('Invalid firmware image')
-
- for i in xrange(segments):
- (offset, size) = struct.unpack(' 0x40200000 or offset < 0x3ffe0000 or size > 65536:
- raise Exception('Suspicious segment %x,%d' % (offset, size))
- self.segments.append((offset, size, f.read(size)))
+ if load_file is not None:
+ segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
- # Skip the padding. The checksum is stored in the last byte so that the
- # file is a multiple of 16 bytes.
- align = 15-(f.tell() % 16)
- f.seek(align, 1)
+ for _ in range(segments):
+ self.load_segment(load_file)
+ self.checksum = self.read_checksum(load_file)
- self.checksum = ord(f.read(1))
+ self.verify()
- def add_segment(self, addr, data):
- # Data should be aligned on word boundary
- l = len(data)
- if l % 4:
- data += b"\x00" * (4 - l % 4)
- self.segments.append((addr, len(data), data))
+ 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)
+
+
+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(ESP8266V2FirmwareImage, self).__init__()
+ self.version = 2
+ if load_file is not None:
+ segments = self.load_common_header(load_file, ESPBOOTLOADER.IMAGE_V2_MAGIC)
+ if segments != ESPBOOTLOADER.IMAGE_V2_SEGMENT:
+ # segment count is not really segment count here, but we expect to see '4'
+ print('Warning: V2 header has unexpected "segment" count %d (usually 4)' % segments)
+
+ # irom segment comes before the second header
+ #
+ # the file is saved in the image with a zero load address
+ # in the header, so we need to calculate a load address
+ irom_segment = self.load_segment(load_file, True)
+ irom_segment.addr = 0 # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8
+ irom_segment.include_in_checksum = False
+
+ first_flash_mode = self.flash_mode
+ first_flash_size_freq = self.flash_size_freq
+ first_entrypoint = self.entrypoint
+ # load the second header
+
+ segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
+
+ if first_flash_mode != self.flash_mode:
+ print('WARNING: Flash mode value in first header (0x%02x) disagrees with second (0x%02x). Using second value.'
+ % (first_flash_mode, self.flash_mode))
+ if first_flash_size_freq != self.flash_size_freq:
+ print('WARNING: Flash size/freq value in first header (0x%02x) disagrees with second (0x%02x). Using second value.'
+ % (first_flash_size_freq, self.flash_size_freq))
+ if first_entrypoint != self.entrypoint:
+ print('WARNING: Entrypoint address in first header (0x%08x) disagrees with second header (0x%08x). Using second value.'
+ % (first_entrypoint, self.entrypoint))
+
+ # load all the usual 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):
- f = file(filename, 'wb')
- 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):
+ # Load sections from the ELF file
self.name = name
- self.symbols = None
+ 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 "+tool_nm+", do you have Xtensa toolchain in PATH?"
- sys.exit(1)
- for l in proc.stdout:
- fields = l.strip().split()
- self.symbols[fields[2]] = int(fields[0], 16)
+ (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 load_section(self, section):
- tool_objcopy = "xtensa-lx106-elf-objcopy"
- if os.getenv('XTENSA_CORE')=='lx106':
- tool_objcopy = "xt-objcopy"
- subprocess.check_call([tool_objcopy, "--only-section", section, "-Obinary", self.name, ".tmp.section"])
- f = open(".tmp.section", "rb")
- data = f.read()
- f.close()
- os.remove(".tmp.section")
- return data
+ 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))
+
+ # 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()
+
+
+def slip_reader(port, trace_function):
+ """Generator to read SLIP packets from a serial port.
+ Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
+
+ Designed to avoid too many calls to serial.read(1), which can bog
+ down on slow systems.
+ """
+ partial_packet = None
+ in_escape = False
+ while True:
+ waiting = port.inWaiting()
+ read_bytes = port.read(1 if waiting == 0 else waiting)
+ if read_bytes == b'':
+ waiting_for = "header" if partial_packet is None else "content"
+ trace_function("Timed out waiting for packet %s", waiting_for)
+ raise FatalError("Timed out waiting for packet %s" % waiting_for)
+ trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes))
+ for b in read_bytes:
+ if type(b) is int:
+ b = bytes([b]) # python 2/3 compat
+
+ if partial_packet is None: # waiting for packet header
+ if b == b'\xc0':
+ partial_packet = b""
+ else:
+ trace_function("Read invalid data: %s", HexFormatter(read_bytes))
+ trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting())))
+ raise FatalError('Invalid head of packet (0x%s)' % hexify(b))
+ elif in_escape: # part-way through escape sequence
+ in_escape = False
+ if b == b'\xdc':
+ partial_packet += b'\xc0'
+ elif b == b'\xdd':
+ partial_packet += b'\xdb'
+ else:
+ trace_function("Read invalid data: %s", HexFormatter(read_bytes))
+ trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting())))
+ raise FatalError('Invalid SLIP escape (0xdb, 0x%s)' % (hexify(b)))
+ elif b == b'\xdb': # start of escape sequence
+ in_escape = True
+ elif b == b'\xc0': # end of packet
+ trace_function("Received full packet: %s", HexFormatter(partial_packet))
+ yield partial_packet
+ partial_packet = None
+ else: # normal byte in packet
+ partial_packet += b
def arg_auto_int(x):
return int(x, 0)
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(description = 'ESP8266 ROM Bootloader Utility', prog = 'esptool')
+
+def div_roundup(a, b):
+ """ Return a/b rounded up to nearest integer,
+ equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only
+ without possible floating point accuracy errors.
+ """
+ return (int(a) + int(b) - 1) // int(b)
+
+
+def align_file_position(f, size):
+ """ Align the position in the file to the next block of specified size """
+ align = (size - 1) - (f.tell() % size)
+ f.seek(align, 1)
+
+
+def flash_size_bytes(size):
+ """ Given a flash size of the type passed in args.flash_size
+ (ie 512KB or 1MB) then return the size in bytes.
+ """
+ if "MB" in size:
+ return int(size[:size.index("MB")]) * 1024 * 1024
+ elif "KB" in size:
+ return int(size[:size.index("KB")]) * 1024
+ else:
+ raise FatalError("Unknown size %s" % size)
+
+
+def hexify(s, uppercase=True):
+ format_str = '%02X' if uppercase else '%02x'
+ if not PYTHON2:
+ return ''.join(format_str % c for c in s)
+ else:
+ return ''.join(format_str % ord(c) for c in s)
+
+
+class HexFormatter(object):
+ """
+ Wrapper class which takes binary data in its constructor
+ and returns a hex string as it's __str__ method.
+
+ This is intended for "lazy formatting" of trace() output
+ in hex format. Avoids overhead (significant on slow computers)
+ of generating long hex strings even if tracing is disabled.
+
+ Note that this doesn't save any overhead if passed as an
+ argument to "%", only when passed to trace()
+
+ If auto_split is set (default), any long line (> 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):
+ """
+ Wrapper class for runtime errors that aren't caused by internal bugs, but by
+ ESP8266 responses or input content.
+ """
+ def __init__(self, message):
+ RuntimeError.__init__(self, message)
+
+ @staticmethod
+ def WithResult(message, result):
+ """
+ Return a fatal error object that appends the hex values of
+ 'result' as a string formatted argument.
+ """
+ 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
+# argument.
+
+
+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, seg.addr)
+
+ seq = 0
+ 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('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)))
+
+
+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))
+
+
+def dump_mem(esp, args):
+ with open(args.filename, 'wb') as f:
+ for i in range(args.size // 4):
+ d = esp.read_reg(args.address + (i * 4))
+ f.write(struct.pack(b'> 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):
+ # 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
+
+ # 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
+
+ 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()
+ 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
+ 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...')
+ 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.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()
+ 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 = 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):
+ 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.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 = 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]
+
+ if args.elf_sha256_offset:
+ image.elf_sha256 = e.sha256()
+ image.elf_sha256_offset = args.elf_sha256_offset
+
+ image.verify()
+
+ 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()
+
+ 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):
+ 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)...')
+ 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):
+ esp.run()
+
+
+def flash_id(esp, args):
+ flash_id = esp.flash_id()
+ 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):
+ 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 = esp.read_flash(args.address, args.size, flash_progress)
+ t = time.time() - t
+ 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(esp, args):
+ differences = False
+
+ for address, argfile in args.addr_filename:
+ image = pad_to(argfile.read(), 4)
+ argfile.seek(0) # rewind in case we need it again
+
+ 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))
+ # Try digest first, only read if there are differences.
+ digest = esp.flash_md5sum(address, image_size)
+ expected_digest = hashlib.md5(image).hexdigest()
+ if digest == expected_digest:
+ print('-- verify OK (digest matched)')
+ continue
+ else:
+ differences = True
+ if getattr(args, 'diff', 'no') != 'yes':
+ print('-- verify FAILED (digest mismatch)')
+ continue
+
+ flash = esp.read_flash(address, image_size)
+ assert flash != image
+ 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:
+ 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 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__)
+
+#
+# End of operations functions
+#
+
+
+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 = '/dev/ttyUSB0')
+ '--port', '-p',
+ help='Serial port device',
+ default=os.environ.get('ESPTOOL_PORT', None))
parser.add_argument(
- '--baud', '-b',
- help = 'Serial port baud rate',
- type = arg_auto_int,
- default = ESPROM.ESP_ROM_BAUD)
+ '--baud', '-b',
+ help='Serial port baud rate used when flashing/reading',
+ type=arg_auto_int,
+ 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')
+ 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')
- parser_load_ram.add_argument('filename', help = 'Firmware image')
+ 'load_ram',
+ help='Download an image to RAM and execute')
+ parser_load_ram.add_argument('filename', help='Firmware image')
parser_dump_mem = subparsers.add_parser(
- 'dump_mem',
- help = 'Dump arbitrary memory to disk')
- parser_dump_mem.add_argument('address', help = 'Base address', type = arg_auto_int)
- parser_dump_mem.add_argument('size', help = 'Size of region to dump', type = arg_auto_int)
- parser_dump_mem.add_argument('filename', help = 'Name of binary dump')
+ 'dump_mem',
+ help='Dump arbitrary memory to disk')
+ parser_dump_mem.add_argument('address', help='Base address', type=arg_auto_int)
+ parser_dump_mem.add_argument('size', help='Size of region to dump', type=arg_auto_int)
+ parser_dump_mem.add_argument('filename', help='Name of binary dump')
parser_read_mem = subparsers.add_parser(
- 'read_mem',
- help = 'Read arbitrary memory location')
- parser_read_mem.add_argument('address', help = 'Address to read', type = arg_auto_int)
+ 'read_mem',
+ help='Read arbitrary memory location')
+ parser_read_mem.add_argument('address', help='Address to read', type=arg_auto_int)
parser_write_mem = subparsers.add_parser(
- 'write_mem',
- help = 'Read-modify-write to arbitrary memory location')
- parser_write_mem.add_argument('address', help = 'Address to write', type = arg_auto_int)
- 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)
+ 'write_mem',
+ help='Read-modify-write to arbitrary memory location')
+ parser_write_mem.add_argument('address', help='Address to write', type=arg_auto_int)
+ 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, 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=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=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', nargs = '+', help = 'Address and binary file to write there, separated by space')
- parser_write_flash.add_argument('--flash_freq', '-ff', help = 'SPI Flash frequency',
- choices = ['40m', '26m', '20m', '80m'], default = '40m')
- parser_write_flash.add_argument('--flash_mode', '-fm', help = 'SPI Flash mode',
- choices = ['qio', 'qout', 'dio', 'dout'], default = 'qio')
- parser_write_flash.add_argument('--flash_size', '-fs', help = 'SPI Flash size in Mbit',
- choices = ['4m', '2m', '8m', '16m', '32m'], default = '4m')
+ 'write_flash',
+ help='Write a binary blob to flash')
- parser_run = subparsers.add_parser(
- 'run',
- help = 'Run application code in flash')
+ parser_write_flash.add_argument('addr_filename', metavar=' ', help='Address followed by binary filename, separated by space',
+ action=AddrFilenamePairAction)
+ 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 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',
+ help='Run application code in flash')
parser_image_info = subparsers.add_parser(
- 'image_info',
- help = 'Dump headers from an application image')
- parser_image_info.add_argument('filename', help = 'Image file to parse')
+ 'image_info',
+ help='Dump headers from an application image')
+ parser_image_info.add_argument('filename', help='Image file to parse')
parser_make_image = subparsers.add_parser(
- 'make_image',
- help = 'Create an application image from binary files')
- parser_make_image.add_argument('output', help = 'Output image file')
- parser_make_image.add_argument('--segfile', '-f', action = 'append', help = 'Segment input file')
- parser_make_image.add_argument('--segaddr', '-a', action = 'append', help = 'Segment base address', type = arg_auto_int)
- parser_make_image.add_argument('--entrypoint', '-e', help = 'Address of entry point', type = arg_auto_int, default = 0)
+ 'make_image',
+ help='Create an application image from binary files')
+ parser_make_image.add_argument('output', help='Output image file')
+ parser_make_image.add_argument('--segfile', '-f', action='append', help='Segment input file')
+ parser_make_image.add_argument('--segaddr', '-a', action='append', help='Segment base address', type=arg_auto_int)
+ parser_make_image.add_argument('--entrypoint', '-e', help='Address of entry point', type=arg_auto_int, default=0)
parser_elf2image = subparsers.add_parser(
- 'elf2image',
- 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', type = str)
- parser_elf2image.add_argument('--flash_freq', '-ff', help = 'SPI Flash frequency',
- choices = ['40m', '26m', '20m', '80m'], default = '40m')
- parser_elf2image.add_argument('--flash_mode', '-fm', help = 'SPI Flash mode',
- choices = ['qio', 'qout', 'dio', 'dout'], default = 'qio')
- parser_elf2image.add_argument('--flash_size', '-fs', help = 'SPI Flash size in Mbit',
- choices = ['4m', '2m', '8m', '16m', '32m'], default = '4m')
+ 'elf2image',
+ 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')
+ 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)
- parser_read_mac = subparsers.add_parser(
- 'read_mac',
- help = 'Read MAC address from OTP ROM')
+ add_spi_flash_subparsers(parser_elf2image, is_elf2image=True)
+
+ subparsers.add_parser(
+ 'read_mac',
+ help='Read MAC address from OTP ROM')
+
+ subparsers.add_parser(
+ 'chip_id',
+ help='Read Chip ID from OTP ROM')
parser_flash_id = subparsers.add_parser(
- 'flash_id',
- help = 'Read SPI flash manufacturer and device ID')
+ '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')
- 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')
+ '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')
+ parser_read_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true")
+
+ parser_verify_flash = subparsers.add_parser(
+ 'verify_flash',
+ help='Verify a binary blob against flash')
+ parser_verify_flash.add_argument('addr_filename', help='Address and binary file to verify there, separated by space',
+ 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)
parser_erase_flash = subparsers.add_parser(
- 'erase_flash',
- help = 'Perform Chip Erase on SPI flash')
+ 'erase_flash',
+ help='Perform Chip Erase on SPI flash')
+ add_spi_connection_arg(parser_erase_flash)
- args = parser.parse_args()
+ 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)
- # Create the ESPROM connection object, if needed
- esp = None
- if args.operation not in ('image_info','make_image','elf2image'):
- esp = ESPROM(args.port, args.baud)
- esp.connect()
+ subparsers.add_parser(
+ 'version', help='Print esptool version')
- # Do the actual work. Should probably be split into separate functions.
- if args.operation == 'load_ram':
- image = ESPFirmwareImage(args.filename)
+ subparsers.add_parser('get_security_info', help='Get some security-related data')
- print 'RAM boot...'
- for (offset, size, data) in image.segments:
- print 'Downloading %d bytes at %08x...' % (size, offset),
- sys.stdout.flush()
- esp.mem_begin(size, math.ceil(size / float(esp.ESP_RAM_BLOCK)), esp.ESP_RAM_BLOCK, offset)
+ # 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
- seq = 0
- while len(data) > 0:
- esp.mem_block(data[0:esp.ESP_RAM_BLOCK], seq)
- data = data[esp.ESP_RAM_BLOCK:]
- seq += 1
- print 'done!'
+ expand_file_arguments()
- print 'All segments done, executing at %08x' % image.entrypoint
- esp.mem_finish(image.entrypoint)
+ args = parser.parse_args(custom_commandline)
+ print('esptool.py v%s' % __version__)
- elif args.operation == 'read_mem':
- print '0x%08x = 0x%08x' % (args.address, esp.read_reg(args.address))
+ # operation function can take 1 arg (args), 2 args (esp, arg)
+ # or be a member function of the ESPLoader class.
- elif args.operation == 'write_mem':
- esp.write_reg(args.address, args.value, args.mask, 0)
- print 'Wrote %08x, mask %08x to %08x' % (args.value, args.mask, args.address)
+ if args.operation is None:
+ parser.print_help()
+ sys.exit(1)
- elif args.operation == 'dump_mem':
- f = file(args.filename, 'wb')
- for i in xrange(args.size/4):
- d = esp.read_reg(args.address+(i*4))
- f.write(struct.pack(' 0:
- print '\rWriting at 0x%08x... (%d %%)' % (address + seq*esp.ESP_FLASH_BLOCK, 100*(seq+1)/blocks),
- sys.stdout.flush()
- block = image[0:esp.ESP_FLASH_BLOCK]
- # Fix sflash config data
- if address == 0 and seq == 0 and block[0] == '\xe9':
- block = block[0:2] + flash_info + block[4:]
- # Pad the last block
- block = block + '\xff' * (esp.ESP_FLASH_BLOCK-len(block))
- esp.flash_block(block, seq)
- image = image[esp.ESP_FLASH_BLOCK:]
- seq += 1
- print
- print '\nLeaving...'
- esp.flash_finish(False)
+ 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
- elif args.operation == 'run':
- esp.run()
+ 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))
- elif args.operation == 'image_info':
- image = ESPFirmwareImage(args.filename)
- 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):
- 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!')
+ 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)
- elif args.operation == 'make_image':
- image = ESPFirmwareImage()
- if len(args.segfile) == 0:
- raise Exception('No segments specified')
- if len(args.segfile) != len(args.segaddr):
- raise Exception('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)
- image.entrypoint = args.entrypoint
- image.save(args.output)
+ 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()
- elif args.operation == 'elf2image':
- if args.output is None:
- args.output = args.input + '-'
- e = ELFFile(args.input)
- image = ESPFirmwareImage()
- image.entrypoint = e.get_symbol_addr("call_user_start")
- 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)
+ if args.override_vddsdio:
+ esp.override_vddsdio(args.override_vddsdio)
- 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}[args.flash_size]
- image.flash_size_freq += {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq]
+ 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)
- image.save(args.output + "0x00000.bin")
- data = e.load_section(".irom0.text")
- off = e.get_symbol_addr("_irom0_text_start") - 0x40200000
- assert off >= 0
- f = open(args.output + "0x%05x.bin" % off, "wb")
- f.write(data)
- f.close()
+ # 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)
- elif args.operation == 'read_mac':
- mac = esp.read_mac()
- print 'MAC: %s' % ':'.join(map(lambda x: '%02x'%x, mac))
+ 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))
- elif args.operation == 'flash_id':
- flash_id = esp.flash_id()
- print 'Manufacturer: %02x' % (flash_id & 0xff)
- print 'Device: %02x%02x' % ((flash_id >> 8) & 0xff, (flash_id >> 16) & 0xff)
+ try:
+ operation_func(esp, args)
+ finally:
+ try: # Clean up AddrFilenamePairAction files
+ for address, argfile in args.addr_filename:
+ argfile.close()
+ except AttributeError:
+ pass
- elif args.operation == 'read_flash':
- print 'Please wait...'
- file(args.filename, 'wb').write(esp.flash_read(args.address, 1024, int(math.ceil(args.size / 1024.)))[:args.size])
+ # 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
- elif args.operation == 'erase_flash':
- esp.flash_erase()
+ 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):
+ super(AddrFilenamePairAction, self).__init__(option_strings, dest, nargs, **kwargs)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ # validate pair arguments
+ pairs = []
+ for i in range(0, len(values), 2):
+ try:
+ 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')
+ 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)
+
+
+# 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"""
+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\
+""")))
+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"""
+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\
+""")))
+
+
+def _main():
+ try:
+ main()
+ except FatalError as 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 c303c23..beb0894 100755
--- a/os/flash.sh
+++ b/os/flash.sh
@@ -4,7 +4,7 @@ if [ $# -ne 1 ]; then
echo "One parameter required: the device of the serial interface"
echo "$0 "
echo "e.g.:"
- echo "$0 ttyUSB0"
+ echo "$0 /dev/ttyUSB0"
exit 1
fi
@@ -14,8 +14,8 @@ DEVICE=$1
# check the serial connection
-if [ ! -c /dev/$DEVICE ]; then
- echo "/dev/$DEVICE does not exist"
+if [ ! -c $DEVICE ]; then
+ echo "$DEVICE does not exist"
exit 1
fi
@@ -24,13 +24,12 @@ if [ ! -f esptool.py ]; then
echo "esptool.py"
exit 1
fi
-
-./esptool.py --port /dev/$DEVICE $BAUD read_mac
+python3 esptool.py --port $DEVICE $BAUD read_mac
if [ $? -ne 0 ]; then
echo "Error reading the MAC -> set the device into the bootloader!"
exit 1
fi
-
-#./esptool.py --port /dev/$DEVICE $BAUD write_flash 0x00000 nodemcu-master-enduser_setup,file,gpio,net,node,rtcfifo,rtcmem,rtctime,sntp,spi,tmr,uart,wifi,ws2812-integer.bin
-./esptool.py --port /dev/ttyUSB0 $BAUD write_flash 0x00000 0x00000.bin 0x10000 0x10000.bin
+echo "Flashing the new"
+#python3 esptool.py --port $DEVICE $BAUD write_flash -fm dio 0x00000 nodemcu2.bin
+python3 esptool.py --port $DEVICE write_flash -fm dio 0x00000 0x00000.bin 0x10000 0x10000.bin 0x3fc000 esp_init_data_default.bin
diff --git a/os/nodemcu-master-bit,enduser_setup,file,gpio,node,rtcfifo,rtcmem,rtctime,sntp,spi,tmr,uart,wifi,ws2812-integer.bin b/os/nodemcu-master-bit,enduser_setup,file,gpio,node,rtcfifo,rtcmem,rtctime,sntp,spi,tmr,uart,wifi,ws2812-integer.bin
deleted file mode 100644
index c2d7814..0000000
Binary files a/os/nodemcu-master-bit,enduser_setup,file,gpio,node,rtcfifo,rtcmem,rtctime,sntp,spi,tmr,uart,wifi,ws2812-integer.bin and /dev/null differ
diff --git a/os/nodemcu2.bin b/os/nodemcu2.bin
new file mode 100644
index 0000000..1644bdf
Binary files /dev/null and b/os/nodemcu2.bin differ
diff --git a/simulation/.classpath b/simulation/.classpath
new file mode 100644
index 0000000..33968ad
--- /dev/null
+++ b/simulation/.classpath
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/simulation/.gitignore b/simulation/.gitignore
new file mode 100644
index 0000000..ae3c172
--- /dev/null
+++ b/simulation/.gitignore
@@ -0,0 +1 @@
+/bin/
diff --git a/simulation/.project b/simulation/.project
new file mode 100644
index 0000000..eaad9a1
--- /dev/null
+++ b/simulation/.project
@@ -0,0 +1,17 @@
+
+
+ WS2812Emulation
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/simulation/Readme.md b/simulation/Readme.md
new file mode 100644
index 0000000..1c1d3ff
--- /dev/null
+++ b/simulation/Readme.md
@@ -0,0 +1,12 @@
+# Simulation
+
+The simualation should be started with the following arguments at this position:
+ `../init.lua ws28128ClockLayout.txt config.lua`
+
+# 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')`
+
+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`
diff --git a/simulation/config.lua b/simulation/config.lua
new file mode 100644
index 0000000..c6b8f5e
--- /dev/null
+++ b/simulation/config.lua
@@ -0,0 +1,13 @@
+green2=200
+red=128
+blue=200
+
+color=string.char(0, 0, blue)
+color1=string.char(red, 0, 0)
+color2=string.char(tonumber(red*0.8), 0, 0)
+color3=string.char(0, tonumber(green2*0.4), 0)
+color4=string.char(0,0 ,tonumber(blue*0.2))
+
+colorBg=string.char(0,0,0) -- black is the default background color
+sntpserverhostname="ptbtime1.ptb.de"
+timezoneoffset=1
diff --git a/simulation/libs/.gitignore b/simulation/libs/.gitignore
new file mode 100644
index 0000000..44794ca
--- /dev/null
+++ b/simulation/libs/.gitignore
@@ -0,0 +1,3 @@
+luaj-3.0.1.zip
+luaj-3.0.1/
+luaj-sources-3.0.1.jar
diff --git a/simulation/libs/Readme.md b/simulation/libs/Readme.md
new file mode 100644
index 0000000..c76c4af
--- /dev/null
+++ b/simulation/libs/Readme.md
@@ -0,0 +1,7 @@
+# Dependencies
+
+The following file is expected here:
+`luaj-3.0.1.zip`
+
+It can be downloaded here:
+https://sourceforge.net/projects/luaj/files/latest/download
diff --git a/simulation/libs/luaj-jme-3.0.1.jar b/simulation/libs/luaj-jme-3.0.1.jar
new file mode 100644
index 0000000..4541df3
Binary files /dev/null and b/simulation/libs/luaj-jme-3.0.1.jar differ
diff --git a/simulation/libs/luaj-jse-3.0.1.jar b/simulation/libs/luaj-jse-3.0.1.jar
new file mode 100644
index 0000000..e125860
Binary files /dev/null and b/simulation/libs/luaj-jse-3.0.1.jar differ
diff --git a/simulation/src/de/c3ma/ollo/LuaSimulation.java b/simulation/src/de/c3ma/ollo/LuaSimulation.java
new file mode 100644
index 0000000..7c1223d
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/LuaSimulation.java
@@ -0,0 +1,17 @@
+package de.c3ma.ollo;
+
+/**
+ * created at 29.12.2017 - 18:29:07
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public interface LuaSimulation {
+
+ public void rebootTriggered();
+
+ public void setSimulationTime(long timeInMillis);
+
+ public void setADC(int value);
+}
diff --git a/simulation/src/de/c3ma/ollo/LuaThreadTmr.java b/simulation/src/de/c3ma/ollo/LuaThreadTmr.java
new file mode 100644
index 0000000..0a958fd
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/LuaThreadTmr.java
@@ -0,0 +1,54 @@
+package de.c3ma.ollo;
+
+import org.luaj.vm2.LuaValue;
+
+/**
+ * created at 29.12.2017 - 18:48:27
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public class LuaThreadTmr extends Thread {
+
+
+ private boolean running = true;
+
+ private boolean stopped = false;
+
+ private LuaValue code;
+
+ private final int delay;
+
+ private final int timerNumber;
+
+ public LuaThreadTmr(int timerNumber, LuaValue code, boolean endlessloop, int delay) {
+ this.code = code;
+ this.running = endlessloop;
+ this.delay = delay;
+ this.timerNumber = timerNumber;
+ }
+
+ @Override
+ public void run() {
+ try {
+ do {
+ Thread.sleep(delay);
+ if (code != null) {
+ code.call();
+ }
+ } while(running);
+ } catch(InterruptedException ie) {
+ System.err.println("[TMR] Timer" + timerNumber + " interrupted");
+ }
+ stopped = true;
+ }
+
+ public boolean isStopped() { return stopped; }
+
+ public void stopThread() {
+ running = false;
+ code = null;
+ }
+
+}
diff --git a/simulation/src/de/c3ma/ollo/WS2812Simulation.java b/simulation/src/de/c3ma/ollo/WS2812Simulation.java
new file mode 100644
index 0000000..1c4cff9
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/WS2812Simulation.java
@@ -0,0 +1,187 @@
+package de.c3ma.ollo;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import javax.swing.SwingUtilities;
+
+import org.luaj.vm2.Globals;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.jse.JsePlatform;
+
+import de.c3ma.ollo.mockup.DoFileFunction;
+import de.c3ma.ollo.mockup.ESP8266Adc;
+import de.c3ma.ollo.mockup.ESP8266File;
+import de.c3ma.ollo.mockup.ESP8266Net;
+import de.c3ma.ollo.mockup.ESP8266Node;
+import de.c3ma.ollo.mockup.ESP8266Time;
+import de.c3ma.ollo.mockup.ESP8266Tmr;
+import de.c3ma.ollo.mockup.ESP8266Uart;
+import de.c3ma.ollo.mockup.ESP8266Wifi;
+import de.c3ma.ollo.mockup.ESP8266Ws2812;
+
+/**
+ * created at 28.12.2017 - 13:19:32
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ *
+ * @author ollo
+ */
+public class WS2812Simulation implements LuaSimulation {
+
+ private Globals globals = JsePlatform.standardGlobals();
+ private ESP8266Tmr espTmr = new ESP8266Tmr();
+ private ESP8266File espFile = new ESP8266File();
+ private ESP8266Node espNode = new ESP8266Node(this);
+ private DoFileFunction doFile = new DoFileFunction(globals);
+ private ESP8266Ws2812 ws2812 = new ESP8266Ws2812();
+ private ESP8266Adc adc = new ESP8266Adc();
+ private String scriptName;
+
+ public WS2812Simulation(File sourceFolder) {
+ globals.load(new ESP8266Uart());
+ globals.load(ws2812);
+ globals.load(espTmr);
+ globals.load(espFile);
+ globals.load(espNode);
+ globals.load(adc);
+ globals.load(new ESP8266Wifi());
+ globals.load(new ESP8266Net());
+ globals.load(new ESP8266Time());
+ globals.set("dofile", doFile);
+
+ try {
+ File tempFile = File.createTempFile("NodemcuSimuFile", "");
+ File tempDir = new File(tempFile.getParent() + File.separator + "Nodemcu" + System.currentTimeMillis());
+ tempDir.mkdir();
+
+ System.out.println("[Nodemcu] Directory is " + tempDir.getAbsolutePath());
+
+ // Copy all files into the temporary folder
+ for (File f : sourceFolder.listFiles()) {
+ Files.copy(f.toPath(), new File(tempDir.getAbsolutePath() + File.separator + f.getName()).toPath());
+ }
+
+ espFile.setWorkingDirectory(tempDir);
+ espNode.setWorkingDirectory(tempDir);
+ doFile.setWorkingDirectory(tempDir);
+ } catch (IOException e) {
+ System.err.println("[Nodemcu] " + e.getMessage());
+ espFile = null;
+ espNode = null;
+ }
+ }
+
+ public static void main(final String[] args) {
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ if (args.length == 0) {
+ printUsage();
+ return;
+ }
+
+ if (args.length >= 1) {
+ File f = new File(args[0]);
+ if (f.exists()) {
+ WS2812Simulation simu = new WS2812Simulation(f.getParentFile());
+ System.out.println("File : " + f.getAbsolutePath());
+
+ if (args.length >= 2) {
+ simu.setWS2812Layout(new File(args[1]));
+ }
+ try {
+ if (args.length >= 3) {
+ File additionalFile = new File(args[2]);
+ if (additionalFile.exists() && (simu.doFile != null)) {
+ File targetFile = new File(simu.doFile.getWorkingDirectory()
+ + File.separator + additionalFile.getName());
+ if (targetFile.exists()) {
+ if (targetFile.delete()) {
+ System.out.println("Removed original " + targetFile.getName() + "");
+ } else {
+ System.err.println("Cannot removed original " + targetFile.getName() + "");
+ }
+ }
+ Files.copy(additionalFile.toPath(), targetFile.toPath());
+ System.out.println("Integrate " + additionalFile.getName() + " into simulation");
+ } else {
+ System.err.println("Script " + args[2] + " cannot be found");
+ printUsage();
+ System.exit(1);
+ }
+ }
+ if (args.length >= 4) {
+ try {
+ ESP8266Tmr.gTimingFactor = Integer.parseInt(args[3]);
+ } catch (NumberFormatException nfe) {
+ System.err.println("Timing factor not parsable: " + nfe.getMessage());
+ printUsage();
+ }
+ }
+
+ simu.callScript(f.getName());
+ } catch (IOException e) {
+ System.err.println("[Nodemcu] " + e.getMessage());
+ }
+ }
+ } else {
+ printUsage();
+ }
+
+ }
+ });
+ }
+
+ private void setWS2812Layout(File file) {
+ if (file.exists()) {
+ ws2812.setLayout(file, this);
+ } else {
+ throw new RuntimeException("WS2812 Layout: " + file.getAbsolutePath() + " does not exists");
+ }
+ }
+
+ private static void printUsage() {
+ System.out.println("Usage:");
+ System.out.println("one argument required: file to execute.");
+ System.out.println(".e.g: init.lua (ws2812 layout configuration) (additional LUA script) (timing speedup factor)");
+ }
+
+ @Override
+ public void rebootTriggered() {
+ System.out.println("=================== Reboot in Simulation -> call it again =================");
+ this.espTmr.stopAllTimer();
+ try {
+ Thread.sleep(200);
+ if (this.scriptName != null) {
+ System.out.println("Reexecuting...");
+ callScript(this.scriptName);
+ }
+ } catch (InterruptedException e) {
+
+ }
+ }
+
+ private void callScript(String filename) {
+ this.scriptName = filename;
+
+ if ((espFile != null) && (espFile.getFileInWorkingDir(filename) != null)) {
+ LuaValue chunk = globals.loadfile(espFile.getFileInWorkingDir(filename).getAbsolutePath());
+ chunk.call();
+ } else {
+ throw new RuntimeException("Copy into temporary folder failed; script not available");
+ }
+ }
+
+ @Override
+ public void setSimulationTime(long timeInMillis) {
+ ESP8266Time.setOverwrittenTime(timeInMillis);
+ }
+
+ @Override
+ public void setADC(int value) {
+ adc.setADC(value);
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/DoFileFunction.java b/simulation/src/de/c3ma/ollo/mockup/DoFileFunction.java
new file mode 100644
index 0000000..0d3d741
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/DoFileFunction.java
@@ -0,0 +1,52 @@
+package de.c3ma.ollo.mockup;
+
+import java.io.File;
+
+import org.luaj.vm2.Globals;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.OneArgFunction;
+
+/**
+ * created at 29.12.2017 - 20:23:48
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public class DoFileFunction extends OneArgFunction {
+
+ private File workingDir = null;
+ private Globals globals;
+
+ public DoFileFunction(Globals globals) {
+ this.globals = globals;
+ }
+
+ @Override
+ public LuaValue call(LuaValue luaFilename) {
+ String filename = luaFilename.checkjstring();
+
+ File f = new File(workingDir.getAbsolutePath() + File.separator + filename);
+
+ if (f.exists()) {
+ LuaValue chunk = this.globals.loadfile(f.getAbsolutePath());
+ chunk.call();
+ return LuaValue.valueOf(true);
+ } else {
+ return LuaValue.valueOf(false);
+ }
+ }
+
+ public void setWorkingDirectory(File workingDir) {
+ this.workingDir = workingDir;
+ }
+
+ public String getWorkingDirectory() {
+ if (workingDir != null) {
+ return workingDir.getAbsolutePath();
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Adc.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Adc.java
new file mode 100644
index 0000000..4e8b45f
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Adc.java
@@ -0,0 +1,47 @@
+package de.c3ma.ollo.mockup;
+
+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;
+
+/**
+ * created at 24.04.2019 - 21:12:03
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public class ESP8266Adc extends TwoArgFunction {
+
+ private int mAdc = 0;
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable adc = new LuaTable();
+ adc.set("read", new Read(this));
+ env.set("adc", adc);
+ env.get("package").get("loaded").set("adc", adc);
+ return adc;
+ }
+
+ private class Read extends VarArgFunction {
+
+ private ESP8266Adc adc;
+
+ public Read(ESP8266Adc a) {
+ this.adc = a;
+ }
+
+ public Varargs invoke(Varargs varargs) {
+ return LuaValue.valueOf(this.adc.mAdc);
+ }
+ }
+
+ public void setADC(int newValue) {
+ this.mAdc = newValue;
+ System.out.println("[ADC] updated to " + this.mAdc);
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266File.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266File.java
new file mode 100644
index 0000000..27ad122
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266File.java
@@ -0,0 +1,101 @@
+package de.c3ma.ollo.mockup;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.OneArgFunction;
+import org.luaj.vm2.lib.TwoArgFunction;
+
+/**
+ * created at 29.12.2017 - 01:08:53
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public class ESP8266File extends TwoArgFunction {
+
+ private File workingDir = null;
+
+ private File openedFile = null;
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable file = new LuaTable();
+ file.set("open", new OpenFunction());
+ file.set("list", new ListFunction());
+ file.set("remove", new RemoveFunction());
+ env.set("file", file);
+ env.get("package").get("loaded").set("file", file);
+
+ return file;
+ }
+
+ private class ListFunction extends TwoArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue arg1, LuaValue arg2) {
+ final LuaTable fileList = new LuaTable();
+
+ if ((workingDir != null) && (workingDir.exists())) {
+ File[] files = workingDir.listFiles();
+ for (File file : files) {
+ fileList.set(file.getName(), file.getAbsolutePath());
+ }
+ }
+
+ return fileList;
+ }
+
+ }
+
+ private class OpenFunction extends OneArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue fileName) {
+
+ final String codeFileName = fileName.checkjstring();
+ final File f = new File( workingDir.getAbsolutePath() + File.separator + codeFileName);
+ //System.out.println("[FILE] Loading " + codeFileName);
+ if (f.exists()) {
+ ESP8266File.this.openedFile = f;
+ }
+
+ return LuaValue.valueOf((f.exists()));
+ }
+
+ }
+
+ private class RemoveFunction extends OneArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue fileName) {
+
+ final String luaFileName = fileName.checkjstring();
+ System.out.println("[FILE] Removing " + luaFileName);
+ File f = new File(workingDir.getAbsolutePath() + File.separator + fileName);
+ if (f.exists()) {
+ return LuaValue.valueOf(f.delete());
+ } else {
+ return LuaValue.valueOf(false);
+ }
+ }
+
+ }
+
+ public void setWorkingDirectory(File workingDir) {
+ this.workingDir = workingDir;
+ }
+
+ public File getFileInWorkingDir(String filename) {
+ File f = new File (workingDir.getAbsolutePath() + File.separator + filename);
+ if (f.exists()) {
+ return f;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Net.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Net.java
new file mode 100644
index 0000000..6bf43c5
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Net.java
@@ -0,0 +1,83 @@
+package de.c3ma.ollo.mockup;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.file.Files;
+
+import org.luaj.vm2.LuaFunction;
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.OneArgFunction;
+import org.luaj.vm2.lib.TwoArgFunction;
+import org.luaj.vm2.lib.ZeroArgFunction;
+
+import de.c3ma.ollo.LuaSimulation;
+
+/**
+ * created at 29.12.2017 - 01:29:40
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public class ESP8266Net extends TwoArgFunction {
+
+ public static final int PORTNUMBER_OFFSET = 4000;
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable net = new LuaTable();
+ net.set("createServer", new CreateServerFunction());
+
+ //FIXME net.set("send", new SendFunction());
+ net.set("TCP", "TCP");
+ env.set("net", net);
+ env.get("package").get("loaded").set("net", net);
+ return net;
+ }
+
+ private class CreateServerFunction extends OneArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue arg) {
+ final LuaTable srv = new LuaTable();
+ srv.set("listen", new ListenFunction());
+ return srv;
+ }
+
+ }
+
+ private class ListenFunction extends TwoArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue port, LuaValue function) {
+ int portnumber = port.checkint();
+ LuaFunction onListenFunction = function.checkfunction();
+
+ System.out.println("[Net] listening " + portnumber + "(simulating at " + (PORTNUMBER_OFFSET+ portnumber) + ")");
+
+ try
+ {
+ ServerSocket serverSocket = new ServerSocket(PORTNUMBER_OFFSET+portnumber);
+ serverSocket.setSoTimeout( 60000 ); // Timeout after one minute
+
+ Socket client = serverSocket.accept();
+
+ }
+ catch ( InterruptedIOException iioe )
+ {
+ System.err.println( "Timeout nach einer Minute!" );
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ System.out.println("[Net] server running");
+ return LuaValue.valueOf(true);
+ }
+
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Node.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Node.java
new file mode 100644
index 0000000..1ff7824
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Node.java
@@ -0,0 +1,79 @@
+package de.c3ma.ollo.mockup;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.OneArgFunction;
+import org.luaj.vm2.lib.TwoArgFunction;
+import org.luaj.vm2.lib.ZeroArgFunction;
+
+import de.c3ma.ollo.LuaSimulation;
+
+/**
+ * created at 29.12.2017 - 01:29:40
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public class ESP8266Node extends TwoArgFunction {
+
+ private File workingDir = null;
+ private LuaSimulation nodemcuSimu;
+
+ public ESP8266Node(LuaSimulation nodemcuSimu) {
+ this.nodemcuSimu = nodemcuSimu;
+ }
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable node = new LuaTable();
+ node.set("compile", new CompileFunction());
+ node.set("restart", new RestartFunction());
+ env.set("node", node);
+ env.get("package").get("loaded").set("node", node);
+ return node;
+ }
+
+ private class CompileFunction extends OneArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue fileName) {
+ final String codeFileName = fileName.checkjstring();
+ final String compiledFileName = fileName.checkjstring().replace(".lua", ".lc");
+ final File f = new File( workingDir.getAbsolutePath() + File.separator + codeFileName);
+ System.out.println("[Node] Compiling " + compiledFileName);
+ final File outf = new File( workingDir.getAbsolutePath() + File.separator + compiledFileName);
+ if (f.exists()) {
+ //Simply copy the file as .lc file
+ try {
+ Files.copy(f.toPath(), outf.toPath());
+ } catch (IOException e) {
+ return LuaValue.valueOf(false);
+ }
+ }
+
+ return LuaValue.valueOf(f.exists());
+ }
+
+ }
+
+ private class RestartFunction extends ZeroArgFunction {
+
+ @Override
+ public LuaValue call() {
+ System.out.println("[Node] Restart");
+ nodemcuSimu.rebootTriggered();
+ return LuaValue.valueOf(false);
+ }
+
+ }
+
+ public void setWorkingDirectory(File workingDir) {
+ this.workingDir = workingDir;
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Time.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Time.java
new file mode 100644
index 0000000..a8f48df
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Time.java
@@ -0,0 +1,102 @@
+package de.c3ma.ollo.mockup;
+
+import org.luaj.vm2.LuaFunction;
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.ThreeArgFunction;
+import org.luaj.vm2.lib.TwoArgFunction;
+import org.luaj.vm2.lib.ZeroArgFunction;
+
+/**
+ * created at 29.12.2017 - 00:07:22
+ * creator: ollo
+ * project: Time Emulation
+ *
+ * Simulating the following modules:
+ * Sntp
+ * rtctime
+ *
+ * $Id: $
+ * @author ollo
+ */
+public class ESP8266Time extends TwoArgFunction {
+
+ private static long gSimulationStartTime = 0;
+
+ private static long gOverwrittenTime = 0;
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable sntp = new LuaTable();
+ sntp.set("sync", new SyncFunction());
+ env.set("sntp", sntp);
+ final LuaTable rtctime = new LuaTable();
+ rtctime.set("get", new GetFunction());
+ env.set("rtctime", rtctime);
+ env.get("package").get("loaded").set("sntp", sntp);
+ env.get("package").get("loaded").set("rtctime", rtctime);
+
+ return sntp;
+ }
+
+ /**
+ * Generate a time. If there is no speedup, it is simply the current system time.
+ * Otherwise the time is speedup by the given factor
+ * @return
+ */
+ private long generateCurrenttime() {
+ if (gSimulationStartTime == 0) {
+ gSimulationStartTime = System.currentTimeMillis();
+ }
+
+ if (gOverwrittenTime == 0) {
+ /* Time simulation is disabled -> calculate something according to the speedup factor */
+ long time = System.currentTimeMillis();
+ if (ESP8266Tmr.gTimingFactor > 1) {
+ time = gSimulationStartTime + ((time - gSimulationStartTime) * ESP8266Tmr.gTimingFactor);
+ }
+ return time;
+ } else {
+ return gOverwrittenTime;
+ }
+ }
+
+ private class SyncFunction extends ThreeArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue server, LuaValue callbackSuccess, LuaValue callbackFailure) {
+ String serverName = server.checkjstring();
+ LuaFunction cb = callbackSuccess.checkfunction();
+ System.out.println("[SNTP] sync " + serverName);
+ /*FIXME also make it possible to simulate the time */
+ long time = generateCurrenttime();
+ int seconds = (int) (time / 1000);
+ int useconds = (int) (time % 1000);
+ cb.call(LuaValue.valueOf(seconds), LuaValue.valueOf(useconds), LuaValue.valueOf(serverName));
+ return LuaValue.valueOf(true);
+ }
+
+ }
+
+ private class GetFunction extends ZeroArgFunction {
+
+ @Override
+ public LuaValue call() {
+ LuaValue[] v = new LuaValue[2];
+ /*FIXME also make it possible to simulate the time */
+ long time = generateCurrenttime();
+ int seconds = (int) (time / 1000);
+ int useconds = (int) (time % 1000);
+ v[0] = LuaValue.valueOf(seconds);
+ v[1] = LuaValue.valueOf(useconds);
+ return LuaValue.varargsOf(v).arg1();
+ }
+
+ }
+
+ public static void setOverwrittenTime(long timeInMillis) {
+ gOverwrittenTime = timeInMillis;
+ }
+
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Tmr.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Tmr.java
new file mode 100644
index 0000000..4c1782a
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Tmr.java
@@ -0,0 +1,95 @@
+package de.c3ma.ollo.mockup;
+
+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 de.c3ma.ollo.LuaThreadTmr;
+
+/**
+ * created at 29.12.2017 - 00:07:22
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public class ESP8266Tmr extends TwoArgFunction {
+
+ private static final int MAXTHREADS = 7;
+
+ private static LuaThreadTmr[] allThreads = new LuaThreadTmr[MAXTHREADS];
+
+ public static int gTimingFactor = 1;
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable tmr = new LuaTable();
+ tmr.set("stop", new stop());
+ tmr.set("alarm", new alarm());
+ env.set("tmr", tmr);
+ env.get("package").get("loaded").set("tmr", tmr);
+
+ /* initialize the Threads */
+ for (Thread t : allThreads) {
+ t = null;
+ }
+
+ return tmr;
+ }
+
+ private boolean stopTmr(int i) {
+ if (allThreads[i] != null) {
+ allThreads[i].stopThread();
+ allThreads[i] = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private class stop extends OneArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue arg) {
+ final int timerNumer = arg.toint();
+ System.out.println("[TMR] Timer" + timerNumer + " stopped");
+ return LuaValue.valueOf(stopTmr(timerNumer));
+ }
+
+ }
+
+ private class alarm extends VarArgFunction {
+ public Varargs invoke(Varargs varargs) {
+ if (varargs.narg()== 4) {
+ final int timerNumer = varargs.arg(1).toint();
+ final byte endlessloop = varargs.arg(3).tobyte();
+ final int delay = varargs.arg(2).toint();
+ final LuaValue code = varargs.arg(4);
+ System.out.println("[TMR] Timer" + timerNumer );
+
+ if ((timerNumer < 0) || (timerNumer > timerNumer)) {
+ return LuaValue.error("[TMR] Timer" + timerNumer + " is not available, choose 0 to 6");
+ }
+
+ if (stopTmr(timerNumer)) {
+ System.err.println("[TMR] Timer" + timerNumer + " stopped");
+ }
+
+ /* The cycletime is at least 1 ms */
+ allThreads[timerNumer] = new LuaThreadTmr(timerNumer, code, (endlessloop == 1), Math.max(delay / gTimingFactor, 1));
+ allThreads[timerNumer].start();
+ }
+ return LuaValue.valueOf(true);
+ }
+ }
+
+ public void stopAllTimer() {
+ for (int i = 0; i < allThreads.length; i++) {
+ stopTmr(i);
+ }
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Uart.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Uart.java
new file mode 100644
index 0000000..9cb59c4
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Uart.java
@@ -0,0 +1,38 @@
+package de.c3ma.ollo.mockup;
+
+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;
+
+/**
+ * created at 28.12.2017 - 23:05:05
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ * @author ollo
+ */
+public class ESP8266Uart extends TwoArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable uart = new LuaTable();
+ uart.set("setup", new setup());
+ env.set("uart", uart);
+ env.get("package").get("loaded").set("uart", uart);
+ return uart;
+ }
+
+ private class setup extends VarArgFunction {
+ public Varargs invoke(Varargs varargs) {
+ if (varargs.narg()== 6) {
+ System.out.println("[UART] " + varargs.arg(2) + " " + varargs.arg(3)
+ + ((varargs.arg(4).checkint() > 0) ? "Y" : "N")
+ + varargs.arg(5));
+ }
+ return LuaValue.valueOf(true);
+ }
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Wifi.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Wifi.java
new file mode 100644
index 0000000..3f0d41d
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Wifi.java
@@ -0,0 +1,81 @@
+package de.c3ma.ollo.mockup;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.OneArgFunction;
+import org.luaj.vm2.lib.TwoArgFunction;
+import org.luaj.vm2.lib.ZeroArgFunction;
+
+import de.c3ma.ollo.LuaSimulation;
+
+/**
+ * created at 29.12.2017 - 01:29:40
+ * creator: ollo
+ * project: WifiEmulation
+ * $Id: $
+ * @author ollo
+ */
+public class ESP8266Wifi extends TwoArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable wifi = new LuaTable();
+ wifi.set("setmode", new SetModeFunction());
+ final LuaTable ap = new LuaTable();
+ ap.set("config", new ConfigFunction());
+ wifi.set("ap", ap);
+ final LuaTable sta = new LuaTable();
+ sta.set("status", new StatusFunction());
+ sta.set("getip", new GetIpFunction());
+ wifi.set("sta", sta);
+ wifi.set("SOFTAP", "SOFTAP");
+ wifi.set("STATION", "STATION");
+ env.set("wifi", wifi);
+ env.get("package").get("loaded").set("wifi", wifi);
+ return wifi;
+ }
+
+ private class SetModeFunction extends OneArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue apmode) {
+ final String APmodeString = apmode.checkjstring();
+ System.out.println("[Wifi] set mode " + APmodeString);
+ return LuaValue.valueOf(true);
+ }
+
+ }
+
+ private class ConfigFunction extends OneArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue arg) {
+ System.out.println("[Wifi] config");
+ return LuaValue.valueOf(true);
+ }
+
+ }
+
+ private class StatusFunction extends ZeroArgFunction {
+
+ @Override
+ public LuaValue call() {
+ return LuaValue.valueOf(5);
+ }
+
+ }
+
+ private class GetIpFunction extends ZeroArgFunction {
+
+ @Override
+ public LuaValue call() {
+ return LuaValue.valueOf("127.0.0.1");
+ }
+
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ESP8266Ws2812.java b/simulation/src/de/c3ma/ollo/mockup/ESP8266Ws2812.java
new file mode 100644
index 0000000..ee5e04e
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ESP8266Ws2812.java
@@ -0,0 +1,88 @@
+package de.c3ma.ollo.mockup;
+
+import java.io.File;
+
+import javax.swing.SwingUtilities;
+
+import org.luaj.vm2.LuaString;
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.OneArgFunction;
+import org.luaj.vm2.lib.TwoArgFunction;
+import org.luaj.vm2.lib.ZeroArgFunction;
+
+import de.c3ma.ollo.LuaSimulation;
+import de.c3ma.ollo.mockup.ui.WS2812Layout;
+
+/**
+ * created at 28.12.2017 - 23:34:04
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ *
+ * @author ollo
+ */
+public class ESP8266Ws2812 extends TwoArgFunction {
+
+ private static WS2812Layout layout = null;
+
+ @Override
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ env.checkglobals();
+ final LuaTable ws2812 = new LuaTable();
+ ws2812.set("init", new init());
+ ws2812.set("write", new write());
+ env.set("ws2812", ws2812);
+ env.get("package").get("loaded").set("ws2812", ws2812);
+ return ws2812;
+ }
+
+ private class init extends ZeroArgFunction {
+
+ @Override
+ public LuaValue call() {
+ System.out.println("[WS2812] init");
+ return LuaValue.valueOf(true);
+ }
+
+ }
+
+ private class write extends OneArgFunction {
+
+ @Override
+ public LuaValue call(LuaValue arg) {
+ if (arg.isstring()) {
+ LuaString jstring = arg.checkstring();
+ final int length = jstring.rawlen();
+ if ((length % 3) == 0) {
+ final byte[] array = jstring.m_bytes;
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < length; i += 3) {
+ if (ESP8266Ws2812.layout != null) {
+ int r = array[i + 0]+(Byte.MIN_VALUE*-1);
+ int b = array[i + 1]+(Byte.MIN_VALUE*-1);
+ int g = array[i + 2]+(Byte.MIN_VALUE*-1);
+ ESP8266Ws2812.layout.updateLED(i / 3, r, g, b);
+ }
+ }
+ }
+ });
+ }
+
+ if (ESP8266Ws2812.layout == null) {
+ System.out.println("[WS2812] write length:" + length);
+ } else {
+ }
+ }
+ return LuaValue.valueOf(true);
+ }
+ }
+
+ public void setLayout(File file, LuaSimulation nodemcuSimu) {
+ if (ESP8266Ws2812.layout == null) {
+ ESP8266Ws2812.layout = WS2812Layout.parse(file, nodemcuSimu);
+ }
+ }
+}
diff --git a/simulation/src/de/c3ma/ollo/mockup/ui/WS2812Layout.java b/simulation/src/de/c3ma/ollo/mockup/ui/WS2812Layout.java
new file mode 100644
index 0000000..f59fa4b
--- /dev/null
+++ b/simulation/src/de/c3ma/ollo/mockup/ui/WS2812Layout.java
@@ -0,0 +1,323 @@
+package de.c3ma.ollo.mockup.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JTextField;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import de.c3ma.ollo.LuaSimulation;
+
+/**
+ * created at 02.01.2018 - 12:57:02
+ * creator: ollo
+ * project: WS2812Emulation
+ * $Id: $
+ *
+ * @author ollo
+ */
+public class WS2812Layout extends JFrame {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -6815557232118826140L;
+
+ /**
+ * Parameter for the ADC brightness control
+ */
+ private static final int ADC_INIT = 512;
+ private static final int ADC_MIN = 0;
+ private static final int ADC_MAX = 1024;
+
+ private ArrayList mLines = new ArrayList();
+ private int mColumn = 0;
+ private int mRow = 0;
+ private Element[][] mElements;
+
+ private LuaSimulation nodemcuSimu;
+
+ public WS2812Layout(LuaSimulation nodemcuSimu) {
+ this.nodemcuSimu = nodemcuSimu;
+ }
+
+ public static WS2812Layout parse(File file, LuaSimulation nodemcuSimu) {
+ WS2812Layout layout = null;
+ try {
+ BufferedReader br = new BufferedReader(new FileReader(file));
+ try {
+ String line = br.readLine();
+ if (line != null) {
+ layout = new WS2812Layout(nodemcuSimu);
+ }
+
+ while (line != null) {
+ if (!line.startsWith("#")) {
+ layout.mLines.add(line);
+ layout.mRow++;
+ layout.mColumn = Math.max(layout.mColumn, line.length());
+ }
+ /* get the next line */
+ line = br.readLine();
+ }
+
+ /* parse each line */
+ layout.parse();
+ layout.createAndDisplayGUI();
+ } finally {
+ if (br != null) {
+ br.close();
+ }
+ }
+ } catch (IOException ioe) {
+
+ }
+ return layout;
+ }
+
+ private void createAndDisplayGUI() {
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+
+ JPanel contentPane = new JPanel();
+ contentPane.setLayout(new BorderLayout());
+ contentPane.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY, 2));
+
+ JPanel ledPanel = new JPanel();
+ ledPanel.setBackground(Color.BLACK);
+ ledPanel.setLayout(new GridLayout(this.mRow, this.mColumn, 10, 10));
+ for (int i = 0; i < this.mRow; i++) {
+ for (int j = 0; j < this.mColumn; j++) {
+ if (this.mElements[i][j] != null) {
+ ledPanel.add(this.mElements[i][j]);
+ }
+ }
+ }
+ contentPane.add(ledPanel, BorderLayout.CENTER);
+
+ JSlider adc = new JSlider(JSlider.VERTICAL,
+ ADC_MIN, ADC_MAX, ADC_INIT);
+ adc.addChangeListener(new ChangeListener() {
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ nodemcuSimu.setADC(adc.getValue());
+ }
+ });
+
+ contentPane.add(adc, BorderLayout.EAST);
+
+ JPanel bottomPanel = new JPanel();
+
+ final JTextField dateTime = new JTextField("yyyy-mm-dd HH:MM:SS");
+ dateTime.getDocument().addDocumentListener(new DocumentListener() {
+ public void changedUpdate(DocumentEvent e) {
+ warn();
+ }
+ public void removeUpdate(DocumentEvent e) {
+ warn();
+ }
+ public void insertUpdate(DocumentEvent e) {
+ warn();
+ }
+
+ public void warn() {
+ final String pattern = "(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})";
+ final String current = dateTime.getText();
+
+ if (current.length() <=0) {
+ /* color "nothing" green */
+ dateTime.setForeground(Color.GREEN);
+ /* disable the time simulation */
+ nodemcuSimu.setSimulationTime(0);
+ return;
+ }
+
+ if (!current.matches(pattern)) {
+ dateTime.setForeground(Color.RED);
+ } else {
+ dateTime.setForeground(Color.BLACK);
+ Pattern dateTimePattern = Pattern.compile(pattern);
+ Matcher matcher = dateTimePattern.matcher(current);
+ int year=0;
+ int month=0;
+ int day=0;
+ int hour=0;
+ int minute=0;
+ int second=0;
+ matcher.find();
+ for (int g = 1; g <= matcher.groupCount(); g++) {
+ switch(g) {
+ case 1: /* Year */
+ year = Integer.parseInt(matcher.group(g));
+ break;
+ case 2: /* Month */
+ month = Integer.parseInt(matcher.group(g));
+ break;
+ case 3: /* Day */
+ day = Integer.parseInt(matcher.group(g));
+ break;
+ case 4: /* Hour */
+ hour = Integer.parseInt(matcher.group(g));
+ break;
+ case 5: /* Minute */
+ minute = Integer.parseInt(matcher.group(g));
+ break;
+ case 6: /* Second */
+ second = Integer.parseInt(matcher.group(g));
+ break;
+ }
+ }
+ System.out.println("[GUI] Set time to: " + year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second);
+ GregorianCalendar gc = new GregorianCalendar(year, month, day, hour, minute, second);
+
+ nodemcuSimu.setSimulationTime(gc.getTimeInMillis());
+ }
+ }
+ });
+ bottomPanel.add(dateTime);
+
+ final JButton btnSetCurrentTime = new JButton("Set time");
+ btnSetCurrentTime.setActionCommand("Set time");
+ btnSetCurrentTime.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent ae) {
+ JButton but = (JButton) ae.getSource();
+ if (but.equals(btnSetCurrentTime)) {
+ GregorianCalendar gc = new GregorianCalendar();
+ dateTime.setText(String.format("%d-%02d-%02d %02d:%02d:%02d",
+ gc.get(Calendar.YEAR), gc.get(Calendar.MONTH), gc.get(Calendar.DAY_OF_MONTH),
+ gc.get(Calendar.HOUR_OF_DAY), gc.get(Calendar.MINUTE), gc.get(Calendar.SECOND)));
+ }
+ }
+ });
+ bottomPanel.add(btnSetCurrentTime);
+
+ final JButton btnReboot = new JButton("Reboot");
+ btnReboot.setActionCommand("Reboot simulation");
+ btnReboot.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent ae) {
+ JButton but = (JButton) ae.getSource();
+ if (but.equals(btnReboot)) {
+ System.out.println("[Node] Restart");
+ nodemcuSimu.rebootTriggered();
+ }
+ }
+ });
+ bottomPanel.add(btnReboot);
+
+ contentPane.add(bottomPanel, BorderLayout.SOUTH);
+
+ setContentPane(contentPane);
+ pack();
+ setLocationByPlatform(true);
+ setVisible(true);
+ }
+
+ private void parse() {
+ this.mElements = new Element[this.mRow][this.mColumn];
+ int row = 0;
+ for (String line : this.mLines) {
+ for (int i = 0; i < line.length(); i++) {
+ char c = line.charAt(i);
+ if ((('A' <= c) && (c <= 'Z')) || (('0' <= c) && (c <= '9')) || (c == 'Ä') || (c == 'Ö')
+ || (c == 'Ü')) {
+ this.mElements[row][i] = new Element(c);
+ } else {
+ this.mElements[row][i] = new Element();
+ }
+ this.mElements[row][i].setColor(0, 0, 0);
+ }
+ row++;
+ }
+ }
+
+ public class Element extends JLabel {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -3933903441113933637L;
+
+ private boolean noText = false;
+
+ /**
+ * Draw a simple rect
+ */
+ public Element() {
+ super();
+ this.noText = true;
+ this.setBackground(Color.BLACK);
+ }
+
+ /**
+ * Draw a character
+ *
+ * @param character
+ * to show
+ */
+ public Element(char character) {
+ super("" + character);
+ setFont(new Font("Dialog", Font.BOLD, 24));
+ setHorizontalAlignment(CENTER);
+ // FIXME: Background color is not updated:
+ this.setBackground(Color.BLACK);
+ }
+
+ public void setColor(int red, int green, int blue) {
+ this.setForeground(new Color(red, green, blue));
+ this.repaint();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ if (noText) {
+ sb.append(" ");
+ } else {
+ sb.append("" + this.getText());
+ }
+ sb.append("|" + Integer.toHexString(this.getForeground().getRed()) + " "
+ + Integer.toHexString(this.getForeground().getGreen()) + " "
+ + Integer.toHexString(this.getForeground().getBlue()));
+
+ return sb.toString();
+ }
+ }
+
+ public void updateLED(int index, int r, int g, int b) {
+ if (mElements != null) {
+ int i = (index / mColumn);
+ int j = (index % mColumn);
+ // Swap each second row
+ if (i % 2 == 1) {
+ j = (mColumn-1) - j;
+ }
+ if ((i < mElements.length) && (j < mElements[i].length) && (mElements[i][j] != null)) {
+ Element curlbl = mElements[i][j];
+ curlbl.setColor(r, g, b);
+ }
+ }
+ }
+
+}
diff --git a/simulation/ws28128ClockLayout.txt b/simulation/ws28128ClockLayout.txt
new file mode 100644
index 0000000..2fda3e4
--- /dev/null
+++ b/simulation/ws28128ClockLayout.txt
@@ -0,0 +1,17 @@
+# This file describes the layout of the WS2812 LEDs.
+# _ will only draw a rect
+# A-Z or 1-9 will draw the text or number
+# each element will be updated with its color with the known commands of the nodemcu firmware
+#
+# Here the configuration for the wordclock:
+ESKISTLFÃœNF
+ZEHNZWANZIG
+DREIVIERTEL
+TGNACHVORJM
+HALBXZWÖLFP
+ZWEINSIEBEN
+KDREIRHFÃœNF
+ELFNEUNVIER
+WACHTZEHNRS
+BSECHSFMUHR
+____
diff --git a/telnet.lua b/telnet.lua
new file mode 100644
index 0000000..9bac3bc
--- /dev/null
+++ b/telnet.lua
@@ -0,0 +1,36 @@
+-- Telnet Server
+function startTelnetServer()
+ s=net.createServer(net.TCP, 180)
+ s:listen(23,function(c)
+ global_c=c
+ printlines = {}
+ function s_output(str)
+ if(global_c~=nil) then
+ if #printlines > 0 then
+ printlines[ #printlines + 1] = str
+ else
+ printlines[ #printlines + 1] = str
+ global_c:send("\r") -- Send something, so the queue is read after sending
+ end
+ end
+ end
+ node.output(s_output, 0)
+ c:on("receive",function(c,l)
+ node.input(l)
+ end)
+ c:on("disconnection",function(c)
+ node.output(nil)
+ global_c=nil
+ end)
+ c:on("sent", function()
+ if #printlines > 0 then
+ global_c:send(table.remove(printlines, 1))
+ end
+ end)
+ print("Welcome to the Wordclock.")
+ print("- mydofile(\"commands\")")
+ print("- storeConfig()")
+ print("Visite https://github.com/nodemcu/nodemcu-firmware/wiki/nodemcu_api_en for further commands")
+ end)
+ print("Telnetserver is up")
+end
\ No newline at end of file
diff --git a/tools/Readme.md b/tools/Readme.md
index 2b6725d..57da26a 100644
--- a/tools/Readme.md
+++ b/tools/Readme.md
@@ -1,2 +1,4 @@
-# Source:
+# luatool.py
+**Not** supported with python 3.x
+## Source:
https://github.com/4refr0nt/luatool/tree/master/luatool
diff --git a/tools/initialFlash.sh b/tools/initialFlash.sh
index ebcf25c..b1d5efc 100755
--- a/tools/initialFlash.sh
+++ b/tools/initialFlash.sh
@@ -3,6 +3,14 @@
LUATOOL=./tools/luatool.py
DEVICE=$1
+BAUD=115200
+
+# 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
@@ -11,23 +19,38 @@ if [ ! -c $DEVICE ]; then
exit 1
fi
-
-if [ $# -ne 1 ]; then
+if [ $# -eq 0 ]; then
echo ""
- echo "e.g. usage $0 "
+ echo "e.g. usage $0 []"
exit 1
fi
-FILES="displayword.lua main.lua timecore.lua webpage.lua webserver.lua wordclock.lua init.lua"
+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
+
+
# Format filesystem first
echo "Format the complete ESP"
-$LUATOOL -p $DEVICE -w
+$LUATOOL -p $DEVICE -w -b $BAUD
if [ $? -ne 0 ]; then
echo "STOOOOP"
exit 1
fi
-echo
+#stty -F $DEVICE $BAUD
+#echo "Reboot the ESP"
+#echo "node.restart()" >> $DEVICE
+#sleep 1
+#for i in $(seq 0 5); do
+# echo "Stop TMR $i"
+# echo "tmr.stop($i)" >> $DEVICE
+# sleep 1
+#done
+
+#echo
echo "Start Flasing ..."
for f in $FILES; do
if [ ! -f $f ]; then
@@ -36,7 +59,7 @@ for f in $FILES; do
exit 1
fi
echo "------------- $f ------------"
- $LUATOOL -p $DEVICE -f $f -t $f
+ $LUATOOL -p $DEVICE -f $f -b $BAUD -t $f
if [ $? -ne 0 ]; then
echo "STOOOOP"
exit 1
@@ -44,6 +67,6 @@ for f in $FILES; do
done
echo "Reboot the ESP"
-$LUATOOL -p $DEVICE -r
+echo "node.restart()" >> $DEVICE
exit 0
diff --git a/tools/remoteFlash.sh b/tools/remoteFlash.sh
new file mode 100755
index 0000000..c8d3446
--- /dev/null
+++ b/tools/remoteFlash.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+
+IP=$1
+
+FLASHTOOL=./tools/tcpFlash.py
+
+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"
+ echo "usage:"
+ echo "$0 "
+ echo "$0 192.168.0.2"
+ exit 1
+fi
+
+# check the connection
+echo "Searching $IP ..."
+ping $IP -c 2 >> /dev/null
+if [ $? -ne 0 ]; then
+ echo "Entered IP address: $IP is NOT online"
+ exit 2
+fi
+echo "Upgrading $IP"
+
+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
+
+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
+ if [ $? -ne 0 ]; then
+ echo "STOOOOP"
+ exit 1
+ fi
+done
+
+echo "TODO: Reboot the ESP"
+#echo "node.restart()" | nc $IP 80
+
+exit 0
diff --git a/tools/tcpFlash.py b/tools/tcpFlash.py
new file mode 100755
index 0000000..4f7526b
--- /dev/null
+++ b/tools/tcpFlash.py
@@ -0,0 +1,145 @@
+#!/usr/bin/python
+
+import argparse
+import socket
+import os.path
+import sys #for exit and flushing of stdout
+import time
+
+def sendRecv(s, message, answer):
+ msg = message + "\n"
+ s.sendall(msg)
+ reply = s.recv(4096)
+ i=1
+ while ((not (answer in reply)) and (i < 10)):
+ reply += s.recv(4096)
+ i = i + 1
+ if answer not in reply:
+ return False
+ else:
+ return True
+
+def sendCmd(s, message, cleaningEnter=False):
+ msg = message + "\n"
+ s.sendall(msg)
+ time.sleep(0.050)
+ reply = s.recv(4096)
+ i=1
+ while ((not (">" in reply)) and (i < 10)):
+ time.sleep((0.050) * i)
+ reply += s.recv(4096)
+ i = i + 1
+
+# print "Send\t" + message
+# print "Got\t" + reply
+ if (cleaningEnter):
+ s.sendall("\n")
+ if "stdin:1:" in reply:
+ print "ERROR, received : " + reply
+ return False
+ elif ">" in reply:
+ return True
+ else:
+ print "ERROR, received : " + reply
+ return False
+
+def main(nodeip, luafile, volatile=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, 23))
+ time.sleep(0.050)
+ s.sendall("\n")
+ # Receive the hello Message of answer of the ESP
+ if (not sendRecv(s, "\n", "Welcome to ") ):
+ print "Cannot connect to the ESP"
+ s.close()
+ sys.exit(2)
+
+ # Read all lines from the welcome message
+ i=0
+ reply = s.recv(4096)
+ while ((reply is not None) and (not (">" in reply)) and (i < 100)):
+ reply = s.recv(4096)
+ i = i + 1
+
+
+ # Communication tests
+ if ( not sendRecv(s, "print(12345)", "12345") ):
+ 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)
+ sendCmd(s, "w= file.writeline", True)
+ sendCmd(s, "file.open(\"" + luafile + "\",\"w+\");", True)
+ else:
+ print "Executing " + luafile + " on nodemcu"
+
+ with open(luafile) as f:
+ contents = f.readlines()
+ i=1
+ for line in contents:
+ print "\rSending " + str(i) + "/" + str(len(contents)) + " ...",
+ sys.stdout.flush()
+ l = line.rstrip()
+ if ( l.endswith("]") ):
+ l = l + " "
+ print "add a space at the end"
+
+ if (volatile is None):
+ if (not sendCmd(s, "w([==[" + l + "]==]);")):
+ print "Cannot write line " + str(i)
+ s.close()
+ sys.exit(4)
+ else:
+ if (not sendCmd(s, l)):
+ print "Cannot write line " + str(i)
+ s.close()
+ sys.exit(4)
+ i=i+1
+
+ if (volatile is None):
+ # Finished with updating the file in LUA
+ if (not sendCmd(s, "w([[" + "--EOF" + "]]);")):
+ print "Cannot write line " + "-- EOF"
+ if (not sendCmd(s, "file.close();")):
+ print "Cannot close the file"
+ sys.exit(4)
+
+ # Check if the file exists:
+ if (not sendRecv(s, "=file.exists(\"" + luafile + "\")", "true")):
+ print("Cannot send " + luafile + " to the ESP")
+ sys.exit(4)
+ else:
+ print("Updated " + luafile + " successfully")
+ else:
+ print("Send " + luafile + " successfully")
+
+ # Cleaning the socket by closing it
+ s.close()
+ sys.exit(0) # Report that the flashing was succesfull
+ except socket.error, msg:
+ print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1]
+ sys.exit(1)
+
+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('-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)
+ elif (args.target and args.file):
+ main(args.target, args.file)
+ else:
+ parser.print_help()
diff --git a/unit/testWordClock.lua b/unit/testWordClock.lua
index e722669..ea33f55 100644
--- a/unit/testWordClock.lua
+++ b/unit/testWordClock.lua
@@ -56,7 +56,6 @@ expected.itis=1
expected.one=1
expected.clock=1
checkWords(leds, expected, 1, 0)
-checkCharacter(display_countwords_de(leds), 11)
leds=display_timestat(2,5)
expected={}
@@ -64,7 +63,6 @@ expected.two=1
expected.fiveMin=1
expected.after=1
checkWords(leds, expected, 2 , 5)
-checkCharacter(display_countwords_de(leds), 12)
leds=display_timestat(3,10)
expected={}
@@ -72,7 +70,6 @@ expected.three=1
expected.tenMin=1
expected.after=1
checkWords(leds, expected, 3 , 10)
-checkCharacter(display_countwords_de(leds), 12)
leds=display_timestat(4,15)
expected={}
@@ -80,7 +77,6 @@ expected.four=1
expected.after=1
expected.quater=1
checkWords(leds, expected, 4 , 15)
-checkCharacter(display_countwords_de(leds), 15)
leds=display_timestat(5,20)
expected={}
@@ -88,7 +84,6 @@ expected.five=1
expected.twenty=1
expected.after=1
checkWords(leds, expected, 5 , 20)
-checkCharacter(display_countwords_de(leds), 15)
leds=display_timestat(6,25)
expected={}
@@ -97,7 +92,6 @@ expected.fiveMin=1
expected.before=1
expected.half=1
checkWords(leds, expected, 6 , 25)
-checkCharacter(display_countwords_de(leds), 17)
leds=display_timestat(7,30)
expected={}
@@ -105,7 +99,6 @@ expected.itis=1
expected.eight=1
expected.half=1
checkWords(leds, expected, 7 , 30)
-checkCharacter(display_countwords_de(leds), 13)
leds=display_timestat(8,35)
expected={}
@@ -114,7 +107,6 @@ expected.half=1
expected.fiveMin=1
expected.after=1
checkWords(leds, expected, 8 , 35)
-checkCharacter(display_countwords_de(leds), 16)
leds=display_timestat(9,40)
expected={}
@@ -122,7 +114,6 @@ expected.ten=1
expected.twenty=1
expected.before=1
checkWords(leds, expected, 9 , 40)
-checkCharacter(display_countwords_de(leds), 14)
leds=display_timestat(10,45)
expected={}
@@ -130,7 +121,6 @@ expected.eleven=1
expected.quater=1
expected.before=1
checkWords(leds, expected, 10 , 45)
-checkCharacter(display_countwords_de(leds), 13)
leds=display_timestat(11,50)
expected={}
@@ -138,7 +128,6 @@ expected.twelve=1
expected.tenMin=1
expected.before=1
checkWords(leds, expected, 11 , 50)
-checkCharacter(display_countwords_de(leds), 12)
leds=display_timestat(12,55)
expected={}
@@ -146,7 +135,6 @@ expected.oneLong=1
expected.fiveMin=1
expected.before=1
checkWords(leds, expected, 12 , 55)
-checkCharacter(display_countwords_de(leds), 11)
leds=display_timestat(13,00)
expected={}
@@ -154,7 +142,6 @@ expected.itis=1
expected.one=1
expected.clock=1
checkWords(leds, expected, 13 , 00)
-checkCharacter(display_countwords_de(leds), 11)
-- test the minutes inbetween
leds=display_timestat(14,01)
@@ -164,7 +151,6 @@ expected.two=1
expected.min1=1
expected.clock=1
checkWords(leds, expected, 14 , 01)
-checkCharacter(display_countwords_de(leds), 12)
leds=display_timestat(15,02)
expected={}
@@ -173,7 +159,6 @@ expected.three=1
expected.min2=1
expected.clock=1
checkWords(leds, expected, 15 , 02)
-checkCharacter(display_countwords_de(leds), 12)
leds=display_timestat(16,03)
expected={}
@@ -182,7 +167,6 @@ expected.four=1
expected.min3=1
expected.clock=1
checkWords(leds, expected, 16 , 03)
-checkCharacter(display_countwords_de(leds), 12)
leds=display_timestat(17,04)
expected={}
@@ -191,7 +175,6 @@ expected.five=1
expected.min4=1
expected.clock=1
checkWords(leds, expected, 17 , 04)
-checkCharacter(display_countwords_de(leds), 12)
leds=display_timestat(18,06)
expected={}
@@ -200,7 +183,6 @@ expected.after=1
expected.min1=1
expected.six=1
checkWords(leds, expected, 18 , 06)
-checkCharacter(display_countwords_de(leds), 13)
leds=display_timestat(19,09)
expected={}
@@ -209,7 +191,6 @@ expected.after=1
expected.min4=1
expected.seven=1
checkWords(leds, expected, 19 , 09)
-checkCharacter(display_countwords_de(leds), 14)
leds=display_timestat(20,17)
expected={}
@@ -218,7 +199,6 @@ expected.after=1
expected.min2=1
expected.eight=1
checkWords(leds, expected, 20 , 17)
-checkCharacter(display_countwords_de(leds), 15)
diff --git a/webpage.html b/webpage.html
index 7e1dbdd..7266fa7 100644
--- a/webpage.html
+++ b/webpage.html
@@ -45,16 +45,9 @@ Please note that all settings are mandatory
diff --git a/webserver.lua b/webserver.lua
index c61ea10..d0e2d60 100644
--- a/webserver.lua
+++ b/webserver.lua
@@ -1,7 +1,7 @@
--TODO:
-
configFile="config.lua"
+httpSending=false
sentBytes=0
function sendPage(conn, nameOfFile, replaceMap)
collectgarbage()
@@ -11,8 +11,9 @@ function sendPage(conn, nameOfFile, replaceMap)
conn:close()
print("Page sent")
collectgarbage()
+ httpSending=false
else
- print("Next")
+ collectgarbage()
sendPage(conn, nameOfFile, replaceMap)
end
end)
@@ -32,9 +33,6 @@ function sendPage(conn, nameOfFile, replaceMap)
local line = file.readline()
while (line ~= nil) do
- -- increase the amount of sent bytes
- sentBytes=sentBytes+string.len(line)
-
-- all placeholder begin with a $, so search for it in the current line
if (line:find("$") ~= nil) then
-- Replace the placeholder with the dynamic content
@@ -45,10 +43,15 @@ function sendPage(conn, nameOfFile, replaceMap)
end
end
end
+
+
+ -- increase the amount of sent bytes
+ sentBytes=sentBytes+string.len(line)
+
buf = buf .. line
- -- Sent after 1k data
- if (string.len(buf) >= 700) then
+ -- Sent after 500 bytes data
+ if ( (string.len(buf) >= 500) or (node.heap() < 2000) ) then
line=nil
conn:send(buf)
print("Sent part of " .. sentBytes .. "B")
@@ -62,10 +65,11 @@ 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()
@@ -118,24 +122,61 @@ function fillDynamicMap()
replaceMap["$HEXCOLOR3"]=hexColor3
replaceMap["$HEXCOLOR4"]=hexColor4
replaceMap["$HEXCOLORBG"]=hexColorBg
+ replaceMap["$INV46"]=((inv46 ~= nil and inv46 == "on") and "checked" or "")
+ replaceMap["$AUTODIM"]=((dim ~= nil and dim == "on") and "checked" or "")
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)
conn:on("receive", function(conn,payload)
-
+ if (httpSending) then
+ print("HTTP sending... be patient!")
+ return
+ end
if (payload:find("GET /") ~= nil) then
- --here is code for handling http request from a web-browser
-
+ 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 ...")
+ print("Sending webpage.html (" .. tostring(node.heap()) .. "B free) ...")
-- Load the sendPagewebcontent
replaceMap=fillDynamicMap()
sendPage(conn, "webpage.html", replaceMap)
end
-
else if (payload:find("POST /") ~=nil) then
--code for handling the POST-request (updating settings)
_, postdatastart = payload:find("\r\n\r\n")
@@ -165,7 +206,9 @@ function startWebServer()
file.remove(configFile .. ".new")
sec, _ = rtctime.get()
file.open(configFile.. ".new", "w+")
- file.write("-- Config\n" .. "wifi.sta.config(\"" .. _POST.ssid .. "\",[[" .. _POST.password .. "]])\n" .. "sntpserverhostname=\"" .. _POST.sntpserver .. "\"\n" .. "timezoneoffset=\"" .. _POST.timezoneoffset .. "\"\n")
+ file.write("-- Config\n" .. "station_cfg={}\nstation_cfg.ssid=\"" .. _POST.ssid .. "\"\nstation_cfg.pwd=\"" .. _POST.password .. "\"\nstation_cfg.save=false\nwifi.sta.config(station_cfg)\n")
+ file.write("sntpserverhostname=\"" .. _POST.sntpserver .. "\"\n" .. "timezoneoffset=\"" .. _POST.timezoneoffset .. "\"\n".. "inv46=\"" .. tostring(_POST.inv46) .. "\"\n" .. "dim=\"" .. tostring(_POST.dim) .. "\"\n")
+
if ( _POST.fcolor ~= nil) then
-- color=string.char(_POST.green, _POST.red, _POST.blue)
print ("Got fcolor: " .. _POST.fcolor)
@@ -277,19 +320,18 @@ function startWebServer()
node.output(nil)
global_c=nil
end)
- print("Welcome to Word Clock")
-
+ print("Welcome to Word Clock")
end
end
end)
-
conn:on("disconnection", function(c)
print("Goodbye")
node.output(nil) -- un-register the redirect output function, output goes to serial
-
+ collectgarbage()
--reset amount of sent bytes, as we reached the end
sentBytes=0
end)
end)
end
+--FileView done.
diff --git a/wordclock.lua b/wordclock.lua
index f99d815..0228c51 100755
--- a/wordclock.lua
+++ b/wordclock.lua
@@ -12,7 +12,7 @@ function display_timestat(hours, minutes, longmode)
end
-- generate an empty return type
- local ret = { itis=0, fiveMin=0, tenMin=0, after=0, before=0, threeHour=0, quater=0, threequater=0, half=0, s=0,
+ 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 }
@@ -31,7 +31,8 @@ function display_timestat(hours, minutes, longmode)
if ((longmode==1)
or (minutes==0)
or (minutes==6)) then
- ret.itis=1
+ ret.it=1
+ ret.is=1
end
-- Handle minutes
@@ -63,7 +64,7 @@ function display_timestat(hours, minutes, longmode)
ret.before=1
elseif (minutes==9) then
-- Hande if three quater or quater before is displayed
- if (threequater ~= nil) then
+ if ((threequater ~= nil) and (threequater==true or threequater=="on")) then
ret.threequater=1
else
ret.quater = 1
@@ -104,10 +105,10 @@ function display_timestat(hours, minutes, longmode)
end
if (hours == 1) then
- if (ret.before == 1) then
- ret.oneLong = 1
- else
+ if ((ret.it == 1) and (ret.half == 0) ) then
ret.one=1
+ else
+ ret.oneLong=1
end
elseif (hours == 2) then
ret.two=1
@@ -135,82 +136,3 @@ function display_timestat(hours, minutes, longmode)
collectgarbage()
return ret
end
-
--- @fn display_countwords
--- Count the amount of characters, used to describe the current time in words
--- @param words the same structure, as generated with the function @see display_timestat
--- @return the amount of words, used to describe the time or 0
on errors
-function display_countwords_de(words)
- local amount=0
- if (words.itis == 1) then
- amount = amount + 5
- end
- if (words.fiveMin == 1) then
- amount = amount + 4
- end
- if (words.tenMin == 1) then
- amount = amount + 4
- end
- if (words.twenty == 1) then
- amount = amount + 7
- end
- if (words.threequater == 1) then
- amount = amount + 11
- end
- if (words.quater == 1) then
- amount = amount + 7
- end
- if (words.before == 1) then
- amount = amount + 3
- end
- if (words.after == 1) then
- amount = amount + 4
- end
- if (words.half == 1) then
- amount = amount + 4
- end
- if (words.twelve == 1) then
- amount = amount + 5
- end
- if (words.seven == 1) then
- amount = amount + 6
- end
- if (words.one == 1) then
- amount = amount + 3
- end
- if (words.oneLong == 1) then
- amount = amount + 4
- end
- if (words.two == 1) then
- amount = amount + 4
- end
- if (words.three == 1) then
- amount = amount + 4
- end
- if (words.five == 1) then
- amount = amount + 4
- end
- if (words.four == 1) then
- amount = amount + 4
- end
- if (words.nine == 1) then
- amount = amount + 4
- end
- if (words.eleven == 1) then
- amount = amount + 3
- end
- if (words.eight == 1) then
- amount = amount + 4
- end
- if (words.ten == 1) then
- amount = amount + 4
- end
- if (words.clock == 1) then
- amount = amount + 3
- end
- if (words.six == 1) then
- amount = amount + 5
- end
-
- return amount
-end