From 437c662575f928c37d3ea45a8723d234f9771c8c Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Sun, 17 Sep 2023 19:23:28 +0200 Subject: [PATCH] add all OTA convertible ESP32 Shellys --- raw/esp32/Shelly_PlugIT/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_PlugIT/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_PlugIT/bootloader.be | 108 +++ raw/esp32/Shelly_PlugIT/init.bat | 3 + raw/esp32/Shelly_PlugIT/migrate_shelly.be | 70 ++ .../Shelly_PlugIT/partition_core_shelly.be | 645 ++++++++++++++++++ raw/esp32/Shelly_PlugUK/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_PlugUK/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_PlugUK/bootloader.be | 108 +++ raw/esp32/Shelly_PlugUK/init.bat | 3 + raw/esp32/Shelly_PlugUK/migrate_shelly.be | 70 ++ .../Shelly_PlugUK/partition_core_shelly.be | 645 ++++++++++++++++++ raw/esp32/Shelly_PlugUS/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_PlugUS/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_PlugUS/bootloader.be | 108 +++ raw/esp32/Shelly_PlugUS/init.bat | 5 + raw/esp32/Shelly_PlugUS/migrate_shelly.be | 70 ++ .../Shelly_PlugUS/partition_core_shelly.be | 645 ++++++++++++++++++ raw/esp32/Shelly_PlusHT/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_PlusHT/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_PlusHT/bootloader.be | 108 +++ raw/esp32/Shelly_PlusHT/init.bat | 2 + raw/esp32/Shelly_PlusHT/migrate_shelly.be | 70 ++ .../Shelly_PlusHT/partition_core_shelly.be | 645 ++++++++++++++++++ raw/esp32/Shelly_PlusI4/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_PlusI4/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_PlusI4/bootloader.be | 108 +++ raw/esp32/Shelly_PlusI4/init.bat | 3 + raw/esp32/Shelly_PlusI4/migrate_shelly.be | 70 ++ .../Shelly_PlusI4/partition_core_shelly.be | 645 ++++++++++++++++++ .../Shelly_Plus_PlugS/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_Plus_PlugS/bootloader.be | 108 +++ raw/esp32/Shelly_Plus_PlugS/init.bat | 3 + raw/esp32/Shelly_Plus_PlugS/migrate_shelly.be | 70 ++ .../partition_core_shelly.be | 645 ++++++++++++++++++ .../Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes .../Shelly_Plus_PlugWallDimmer/bootloader.be | 108 +++ raw/esp32/Shelly_Plus_PlugWallDimmer/init.bat | 2 + .../migrate_shelly.be | 70 ++ .../partition_core_shelly.be | 645 ++++++++++++++++++ raw/esp32/Shelly_Pro_1/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_Pro_1/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_Pro_1/bootloader.be | 108 +++ raw/esp32/Shelly_Pro_1/init.bat | 3 + raw/esp32/Shelly_Pro_1/migrate_shelly.be | 70 ++ .../Shelly_Pro_1/partition_core_shelly.be | 645 ++++++++++++++++++ .../Shelly_Pro_1PM/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_Pro_1PM/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_Pro_1PM/bootloader.be | 108 +++ raw/esp32/Shelly_Pro_1PM/init.bat | 3 + raw/esp32/Shelly_Pro_1PM/migrate_shelly.be | 70 ++ .../Shelly_Pro_1PM/partition_core_shelly.be | 645 ++++++++++++++++++ raw/esp32/Shelly_Pro_2/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_Pro_2/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_Pro_2/bootloader.be | 108 +++ raw/esp32/Shelly_Pro_2/init.bat | 3 + raw/esp32/Shelly_Pro_2/migrate_shelly.be | 70 ++ .../Shelly_Pro_2/partition_core_shelly.be | 645 ++++++++++++++++++ .../Shelly_Pro_2PM/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_Pro_2PM/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_Pro_2PM/bootloader.be | 108 +++ raw/esp32/Shelly_Pro_2PM/init.bat | 3 + raw/esp32/Shelly_Pro_2PM/migrate_shelly.be | 70 ++ .../Shelly_Pro_2PM/partition_core_shelly.be | 645 ++++++++++++++++++ raw/esp32/Shelly_Pro_3/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_Pro_3/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_Pro_3/bootloader.be | 108 +++ raw/esp32/Shelly_Pro_3/init.bat | 2 + raw/esp32/Shelly_Pro_3/migrate_shelly.be | 70 ++ .../Shelly_Pro_3/partition_core_shelly.be | 645 ++++++++++++++++++ .../Shelly_Pro_3EM/Partition_Wizard.tapp | Bin 0 -> 17544 bytes .../Shelly_Pro_3EM/bootloader-tasmota-32.bin | Bin 0 -> 15728 bytes raw/esp32/Shelly_Pro_3EM/bootloader.be | 108 +++ raw/esp32/Shelly_Pro_3EM/init.bat | 3 + raw/esp32/Shelly_Pro_3EM/migrate_shelly.be | 70 ++ .../Shelly_Pro_3EM/partition_core_shelly.be | 645 ++++++++++++++++++ 78 files changed, 10737 insertions(+) create mode 100644 raw/esp32/Shelly_PlugIT/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_PlugIT/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_PlugIT/bootloader.be create mode 100644 raw/esp32/Shelly_PlugIT/init.bat create mode 100644 raw/esp32/Shelly_PlugIT/migrate_shelly.be create mode 100644 raw/esp32/Shelly_PlugIT/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_PlugUK/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_PlugUK/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_PlugUK/bootloader.be create mode 100644 raw/esp32/Shelly_PlugUK/init.bat create mode 100644 raw/esp32/Shelly_PlugUK/migrate_shelly.be create mode 100644 raw/esp32/Shelly_PlugUK/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_PlugUS/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_PlugUS/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_PlugUS/bootloader.be create mode 100644 raw/esp32/Shelly_PlugUS/init.bat create mode 100644 raw/esp32/Shelly_PlugUS/migrate_shelly.be create mode 100644 raw/esp32/Shelly_PlugUS/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_PlusHT/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_PlusHT/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_PlusHT/bootloader.be create mode 100644 raw/esp32/Shelly_PlusHT/init.bat create mode 100644 raw/esp32/Shelly_PlusHT/migrate_shelly.be create mode 100644 raw/esp32/Shelly_PlusHT/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_PlusI4/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_PlusI4/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_PlusI4/bootloader.be create mode 100644 raw/esp32/Shelly_PlusI4/init.bat create mode 100644 raw/esp32/Shelly_PlusI4/migrate_shelly.be create mode 100644 raw/esp32/Shelly_PlusI4/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_Plus_PlugS/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_Plus_PlugS/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_Plus_PlugS/bootloader.be create mode 100644 raw/esp32/Shelly_Plus_PlugS/init.bat create mode 100644 raw/esp32/Shelly_Plus_PlugS/migrate_shelly.be create mode 100644 raw/esp32/Shelly_Plus_PlugS/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_Plus_PlugWallDimmer/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_Plus_PlugWallDimmer/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_Plus_PlugWallDimmer/bootloader.be create mode 100644 raw/esp32/Shelly_Plus_PlugWallDimmer/init.bat create mode 100644 raw/esp32/Shelly_Plus_PlugWallDimmer/migrate_shelly.be create mode 100644 raw/esp32/Shelly_Plus_PlugWallDimmer/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_Pro_1/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_Pro_1/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_Pro_1/bootloader.be create mode 100644 raw/esp32/Shelly_Pro_1/init.bat create mode 100644 raw/esp32/Shelly_Pro_1/migrate_shelly.be create mode 100644 raw/esp32/Shelly_Pro_1/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_Pro_1PM/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_Pro_1PM/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_Pro_1PM/bootloader.be create mode 100644 raw/esp32/Shelly_Pro_1PM/init.bat create mode 100644 raw/esp32/Shelly_Pro_1PM/migrate_shelly.be create mode 100644 raw/esp32/Shelly_Pro_1PM/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_Pro_2/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_Pro_2/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_Pro_2/bootloader.be create mode 100644 raw/esp32/Shelly_Pro_2/init.bat create mode 100644 raw/esp32/Shelly_Pro_2/migrate_shelly.be create mode 100644 raw/esp32/Shelly_Pro_2/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_Pro_2PM/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_Pro_2PM/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_Pro_2PM/bootloader.be create mode 100644 raw/esp32/Shelly_Pro_2PM/init.bat create mode 100644 raw/esp32/Shelly_Pro_2PM/migrate_shelly.be create mode 100644 raw/esp32/Shelly_Pro_2PM/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_Pro_3/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_Pro_3/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_Pro_3/bootloader.be create mode 100644 raw/esp32/Shelly_Pro_3/init.bat create mode 100644 raw/esp32/Shelly_Pro_3/migrate_shelly.be create mode 100644 raw/esp32/Shelly_Pro_3/partition_core_shelly.be create mode 100644 raw/esp32/Shelly_Pro_3EM/Partition_Wizard.tapp create mode 100644 raw/esp32/Shelly_Pro_3EM/bootloader-tasmota-32.bin create mode 100644 raw/esp32/Shelly_Pro_3EM/bootloader.be create mode 100644 raw/esp32/Shelly_Pro_3EM/init.bat create mode 100644 raw/esp32/Shelly_Pro_3EM/migrate_shelly.be create mode 100644 raw/esp32/Shelly_Pro_3EM/partition_core_shelly.be diff --git a/raw/esp32/Shelly_PlugIT/Partition_Wizard.tapp b/raw/esp32/Shelly_PlugIT/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlugIT/bootloader-tasmota-32.bin b/raw/esp32/Shelly_PlugIT/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlugIT/bootloader.be b/raw/esp32/Shelly_PlugIT/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_PlugIT/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_PlugIT/init.bat b/raw/esp32/Shelly_PlugIT/init.bat new file mode 100644 index 00000000..99526083 --- /dev/null +++ b/raw/esp32/Shelly_PlugIT/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_PlugIT.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Plug EU","GPIO":[0,0,0,0,224,2688,0,0,96,288,289,0,290,0],"FLAG":0,"BASE":18} +Module 0 diff --git a/raw/esp32/Shelly_PlugIT/migrate_shelly.be b/raw/esp32/Shelly_PlugIT/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_PlugIT/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_PlugIT/partition_core_shelly.be b/raw/esp32/Shelly_PlugIT/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_PlugIT/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_PlugUK/Partition_Wizard.tapp b/raw/esp32/Shelly_PlugUK/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlugUK/bootloader-tasmota-32.bin b/raw/esp32/Shelly_PlugUK/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlugUK/bootloader.be b/raw/esp32/Shelly_PlugUK/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_PlugUK/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_PlugUK/init.bat b/raw/esp32/Shelly_PlugUK/init.bat new file mode 100644 index 00000000..c48da59c --- /dev/null +++ b/raw/esp32/Shelly_PlugUK/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_PlugUK.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Plug EU","GPIO":[0,0,0,0,224,2688,0,0,96,288,289,0,290,0],"FLAG":0,"BASE":18} +Module 0 diff --git a/raw/esp32/Shelly_PlugUK/migrate_shelly.be b/raw/esp32/Shelly_PlugUK/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_PlugUK/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_PlugUK/partition_core_shelly.be b/raw/esp32/Shelly_PlugUK/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_PlugUK/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_PlugUS/Partition_Wizard.tapp b/raw/esp32/Shelly_PlugUS/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlugUS/bootloader-tasmota-32.bin b/raw/esp32/Shelly_PlugUS/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlugUS/bootloader.be b/raw/esp32/Shelly_PlugUS/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_PlugUS/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_PlugUS/init.bat b/raw/esp32/Shelly_PlugUS/init.bat new file mode 100644 index 00000000..082a5350 --- /dev/null +++ b/raw/esp32/Shelly_PlugUS/init.bat @@ -0,0 +1,5 @@ +Br load("Shelly_PlugUS.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Plug US","GPIO":[52,0,57,0,21,134,0,0,131,17,132,157,0],"FLAG":0,"BASE":45} +Module 0 +rule1 on power1#state do backlog ledpower1 %value%; ledpower2 %value% endon on power1#boot do backlog ledpower1 %value%; ledpower2 %value% endon + diff --git a/raw/esp32/Shelly_PlugUS/migrate_shelly.be b/raw/esp32/Shelly_PlugUS/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_PlugUS/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_PlugUS/partition_core_shelly.be b/raw/esp32/Shelly_PlugUS/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_PlugUS/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_PlusHT/Partition_Wizard.tapp b/raw/esp32/Shelly_PlusHT/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlusHT/bootloader-tasmota-32.bin b/raw/esp32/Shelly_PlusHT/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlusHT/bootloader.be b/raw/esp32/Shelly_PlusHT/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_PlusHT/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_PlusHT/init.bat b/raw/esp32/Shelly_PlusHT/init.bat new file mode 100644 index 00000000..cefdbce4 --- /dev/null +++ b/raw/esp32/Shelly_PlusHT/init.bat @@ -0,0 +1,2 @@ +Br load("Shelly_PlusHT.autoconf#migrate_shelly.be") + diff --git a/raw/esp32/Shelly_PlusHT/migrate_shelly.be b/raw/esp32/Shelly_PlusHT/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_PlusHT/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_PlusHT/partition_core_shelly.be b/raw/esp32/Shelly_PlusHT/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_PlusHT/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_PlusI4/Partition_Wizard.tapp b/raw/esp32/Shelly_PlusI4/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlusI4/bootloader-tasmota-32.bin b/raw/esp32/Shelly_PlusI4/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_PlusI4/bootloader.be b/raw/esp32/Shelly_PlusI4/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_PlusI4/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_PlusI4/init.bat b/raw/esp32/Shelly_PlusI4/init.bat new file mode 100644 index 00000000..c4ee23f0 --- /dev/null +++ b/raw/esp32/Shelly_PlusI4/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_PlusI4.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Plus i4","GPIO":[0,0,0,0,0,0,0,0,192,0,193,0,0,0,0,0,0,0,0,0,0,0,195,194,0,0,0,0,0,0,0,0,0,0,0,0],"FLAG":0,"BASE":1,"CMND":"SwitchMode1 1 | SwitchMode2 1 | SwitchMode3 1 | SwitchMode4 1 | SwitchTopic 0 | SetOption114 1"} +Module 0 diff --git a/raw/esp32/Shelly_PlusI4/migrate_shelly.be b/raw/esp32/Shelly_PlusI4/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_PlusI4/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_PlusI4/partition_core_shelly.be b/raw/esp32/Shelly_PlusI4/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_PlusI4/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_Plus_PlugS/Partition_Wizard.tapp b/raw/esp32/Shelly_Plus_PlugS/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Plus_PlugS/bootloader-tasmota-32.bin b/raw/esp32/Shelly_Plus_PlugS/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Plus_PlugS/bootloader.be b/raw/esp32/Shelly_Plus_PlugS/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_Plus_PlugS/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_Plus_PlugS/init.bat b/raw/esp32/Shelly_Plus_PlugS/init.bat new file mode 100644 index 00000000..7594c0e0 --- /dev/null +++ b/raw/esp32/Shelly_Plus_PlugS/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_Plus_PlugS.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Plus Plug S","GPIO":[0,0,0,0,224,0,32,2720,0,0,0,0,0,0,0,2624,0,0,2656,0,0,288,289,0,0,0,0,0,0,4736,0,0,0,0,0,0],"FLAG":0,"BASE":1} +Module 0 diff --git a/raw/esp32/Shelly_Plus_PlugS/migrate_shelly.be b/raw/esp32/Shelly_Plus_PlugS/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_Plus_PlugS/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_Plus_PlugS/partition_core_shelly.be b/raw/esp32/Shelly_Plus_PlugS/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_Plus_PlugS/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_Plus_PlugWallDimmer/Partition_Wizard.tapp b/raw/esp32/Shelly_Plus_PlugWallDimmer/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Plus_PlugWallDimmer/bootloader-tasmota-32.bin b/raw/esp32/Shelly_Plus_PlugWallDimmer/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Plus_PlugWallDimmer/bootloader.be b/raw/esp32/Shelly_Plus_PlugWallDimmer/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_Plus_PlugWallDimmer/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_Plus_PlugWallDimmer/init.bat b/raw/esp32/Shelly_Plus_PlugWallDimmer/init.bat new file mode 100644 index 00000000..062c383b --- /dev/null +++ b/raw/esp32/Shelly_Plus_PlugWallDimmer/init.bat @@ -0,0 +1,2 @@ +Br load("Shelly_Plus_PlugWallDimmer.autoconf#migrate_shelly.be") + diff --git a/raw/esp32/Shelly_Plus_PlugWallDimmer/migrate_shelly.be b/raw/esp32/Shelly_Plus_PlugWallDimmer/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_Plus_PlugWallDimmer/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_Plus_PlugWallDimmer/partition_core_shelly.be b/raw/esp32/Shelly_Plus_PlugWallDimmer/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_Plus_PlugWallDimmer/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_Pro_1/Partition_Wizard.tapp b/raw/esp32/Shelly_Pro_1/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 zcmb_k&u<$^b}n`|n@zGwwkTSrWm>M$P?p9kOO&jccs-Kr@z}FF8I7&=&>0V+fTGBj z#5F~7$<|oj5Lg9BusR5c4-OI_mtDZYE|5L=kN`Rax#X~?%^#3s4ncwd$t8ze*2?#) zy2&O*dW_7{Fj-w)UBBzS?|tvJ)<09V2&2yr|5|_am;dz7Ki@%`{zqAc75AOGePEa7 zw(LjwFjEg+{@2zYmG_8Rr3!yBCnpNa}2>z%&-=imI>AN=qK|2qGhJB-o)6#Cj< z9DOc{PTw?tQKtWWo&Lw)yw=cnmZmR!G`3f))KYAIKt(Lb7)O~XW1A@Xm?^BynDP$_ zV@QkNzr*jzEmhA4bmcjvPoqd1GOb7P%ajwQ=qCXM>9}$dX37Dxl$VSlPt}f@qP1B( zauN<2NLw3BGujcA(gwA4f_4wlKC`SdL$yX(clPCE6&`&$GB6i4uy^o3&9;jrr`~*4 z*sJU`i;i8etCgL~R@EMUXYk}GL4}>CYTeETf<`*PA_t5a>rB%QmChi{HB~dX|DGD? z4wf--KjXiT!HYU*rK}LEJH^6Y@c{cy%)^fD6ueQis&&VTGRmqJTgY!wAr`}A?broc z)56|Pv+r{7fC3?f&55V=mrRMgR8$kIV73D+?ocP0t{tjDnWqJ;W2UFET2?!#TE{v| zKc;jDX{=(~z;D>i(^>K$g6pVzZ6Y00(??W3&Xfk-i@Wy{zIrWRdc>Ee=acU9qexSG z==LEUNh{2r+FLEV`L*4&;=UbIsyA)xJS*+mr6+|(v+{MNYVX+PLa|h`TP-}+*-2Ik zk2$4XF?sT2i-%*ZRIfR9%_+1TOe^+d=MgTCzLo42v1?_Ndselve7nU~q1fE9H1@Pu zaV)c^lvQENjn&V^W`q>sjb@#C#Gh8GRW4fMl^TDaw`??DZY^&$S3fWAR7$+k;=2$v zcCAv|ndMcx__fW;_Scn?&5PA~&F0(nCcn=2Dz$ybZp|&vH&!tt-Way_9j9Ky&bjj_ zpMP+_@cBod-nUBV3fe@kSEip<%Fgc1>x;R@!H3hU%UfQ4sa~x&Z@# zPuywRt;&yWY-KE%$ncJ;Q}_10HPY?){g1wP@AG^2^Zto->J71zO@=8di0UvAjKyht z6pH0?K~6yb^^lW+qYhDskNz@%W34a?yPf0{J5=%2v+Menx6PKZ5PQ5*UO|(c6X{K7 zxzcJ>i_dQIt!lmW%$ViycS*)}Qtwv?rm1SX8uH!7(jlzO2Ii$0u!md+iS>$CP zK(Z3hZ43jn5Y9)*I#LnDM42rX=)0xErZog zwL{@F>PxqcXyzznX0nV;9E3yqt4JVo91UlWqS2WnX3lOfBhxn344%v4xlB8TG*dF@ zLuNgci9C-5-Rp^o6DyH99znSzn|aK1ltJDo@>1@z7|7u{d|{dzcC%G!k+8>cCLu%< zg8Y`##2I`Kho=JZ)T|Y&1-sd-H*p+DE0FG>c@@edK;I}yH!6FiA?RpW3Tyq?y2dtf zRu(q7Q?K*ZUJ>HQsq<33@r-X*ntM-+O(9bDI2FrnS_WiE;~CXYDA+70wouJsgPn(q#{$psq8s6cAy1Q*Z^|TDXoVx$01q}V`f7v z5;gG(t2q2ILVeCv-ph-W-Vir&bGbh zM}6g@ZW>c`R-x+dW`p#l9kh?V2B`%QRW3Y{;G|FZ-GlI}*wr$&#p*IC+*%nDZ1qZQ ztJU}r%Zbg$Kly_88Q_h~C0(*S@0FEO%+eVNwW4FKk+S@%zAyBPeb6Y@%G}w-#)LZO zt!FqCdwlB|pQ`ZB?#z*{{M2b-4-365RqB?_tM!&6RnfF7oTctsnHC_p&?9vlC_K%f z8ll_vxArQ|%5>9q_M0`TmJY_uWPTUASm^7!_tx&^?};9Hjn=o%bdWk-D(<(SsJ&f3 zH~Aj4to4Y7M{?U0^SoBvgD%G8KpRf;5+Gh{Wm-yls8_o@z1lyEXA}YH{R^FX=ez6S z&g~#g03<^TEZdcMr|<;8sFR6~>`Jxz#N7-~=jfxCxB6BK|ArO+e856uDqFh{c2D|e z^etBUGqzl*HTE6uJZoS^Yx{d!b`!d_Qd^n+U7&bR zBF}Uvx=;$&nWNLhWZsBp*kv|}{8T##<)$SloAw`ok#C z6fU3@8R!xpl#4>kY}2-bVrkmNGGsy>2*Gwp(bzxAK}%qFN+~3A7Azq4X6gE(RDt%7 zpg{^)#0o02#cX%c#G)@bSZ}BaMbHD#J1||XHBA#>!U+VeObEb1>JbeLq0tNP_iC0QUD`k03)e&79lW_+GGawkdgwxNCAvM4<+?iF!I-eS}LQl zF~_7bnk*AR8RQ)pzP#Yr7z@%f=`onisdfzOhk1t{Nw?#a26*{3_5r{P*8pCizcy(- z2%Ll_CRm8;uST$bbe&A_w8kb;hHtWX7G+B)eSowvtTejFh% zg#B`HBztsvdNm(OW;ui2%Z%!;E=9A)7e{8XpR(-hp8^9FK`aZ$Fb#zjqVBNdNP4>MyI6#9AuG(O?AcA~uECb8#l5X^adk*9VPg@`h|exV zFC@vU9n!Pd=VW_(!m~rwVBV=bijXYuAJ(Owi@_}_s)9!7BUw(76qg;7vTIxpu0n;@ z|4=+@+b4*uvY9aP~N)%;I_x*V#7gdvVRI9O_zvAjq~)6HtL>S`)v3Qt~&e zo&NcENM)JgH$Gz`oid=lm~rJKIGRPjW{*vnVx9_Q`fNK!{X{$eavEqbL-s!oHR$yd zLp@B;cp+z{==Ag)Et7wjNSnH2FfGc{^06@E2|jTl$#Vx&xep$baB@>Kq&IE~@&2AX zMJ~(=GP_x9*=Sb?ZdeAwT3>J0cK~-?h2WB4;!YXdBlQXA%%bu54B|z^{8CRq z_75Q=hu#=6wz$H!MU7`E2yjpiPAqtK>sWI+C`x;L6s0|`ozfv+86%Vi=guET0@>qe zU={}%`(B%Eo9^BsSWlcgJx66@ZrQl6Y+^`R%Pl+NE1MircGNAK@|8U|pzN5h@8{h# z%Oy`P06LGm*B9M%hR!u$G}ak?&m3Rsqhr|e*vpH6xR=p3$M^*-#CT!qpnUfD9fFr8 z&MwXvnJSb&>2hq8e%Hshf!OP1biJeYCavE`e0=pL`&W|vHl03CwDOK<1u`Rk_6?@7 zAPMpyJ-S%HeF(%qxc8P|{wuW-%%m1DB%N%cLSR03h&&((qD-zy%0!YOUz1V04o3or ziI#2iizn4jh#@Nni=rVCru zV(rQFs&~ulAIk6V0rVEhRj@o?6kG=|E*ka*47Q>pyhiB87cj{A0z^|wxG<8;rOYJ* z3eM26`#8}!BdFhH5uw$+dKNqN`1RDHH7=#9e@d}BF4Nh4{QAJhuS1TMc8fIt>h3b` z6&t>Gmj*t#ga?H*$6|m>>nxA;_Po@cx8kJ)+tlKFFoh}&FlCV3#I?3u+R4wz64k1+ z2Tt)(vkE0EKpD=%?=#v$;O-veKjOG0XgbP~tO~pv)AfaE4qrye-mO>5c5`JKMakak zQYS6)Uo=;JsLy2H2`KENy&(o$!E5}Bk3P9~=fQ)#*8@HSZ8Pm~Is5PVJG0o=8Sl}- zJsLv1xjvJ@bj}wvgkp}QU zt8slXaC&bv=^KjMH^|{KSa4T9q^eG>XQQUq*?n zHzctVVA?NZAomrP{#69OEIxT+0DYPuI|QmTp@oTb2gwm}Ne&j*4q+6)MQMQGNv|^# z*KS*J(w=Tx312&!kE~G=AkskKi02r*N@gZ=iLT=lCkDJn7M>R*3VDMboF2=BgG2FmBz&ds2oIrJ0#BF6^x)yScraCYc3Z6by`bM3Cp1z#J~2WDx9#wo6hjbWfg($A!y}&rG%6gQA3)E1U;`Xp6DgoYv|AEY0g_u8R2q0E=B$b9UyhQZ8DND@c{v#n|}AGJsi{}fwp z!P|uO5YZ8C6h!Wm-1O?*2lwwS&(qyiZe1Bz?vsyD?%}8P@8QD-550mk z5aydRUvlv96}Z3);;C=p0{5}i{_vh4+~FbDqY1&arW}sP;Y9$;92(&oghI-~{g}#+LKc4!hd`x! zVK*;9*KsKn7@s^D!8N(ig(nl{J018?BE$Y2=CuA^^0|IB`TYE~<=mOmKdNRwxi*!A66tQNsbJ6S(Om-H&I`({Ag}Lr}L@y#uW<^H)<bwrUbuo zatS8=gj)vpyG*oasrwK7bsSvjus`dspYZsd4v2L5C7A6L*foeflvL~tgAWw$d2-Pc zi!B`VoY+F?(eG49b@UL!XF>rcz#j@Hi7*=|^oT0!;#zBpD)41^gz{EpN7x-MWFkJe zKbiw$cZs=vspk^U@@}il_iONumiZU!pMC=OaHso{m_W$G{ua6MBy}2O)goe!5Rc$N zNx)X>BXxlw8W_H_VF`#9cv_(-FR#CYgA97suIFON;e+f6(c$aiEhH)J(On{@;SCDn z4Fs|{)`Q)!!*T~&EhUa zj{CrjU=oCL%CI;^&VV9Do_46CR-abW*&evq<})mY{G3@s0>%daWy-ghAj zT8LOHz%e3i3DL8madJa%3>oXV0#=tkF5jerFAG~o{xBB{ZbY48D&cPH{P(H2WCqt5O)2$eEzru_@-k&AS)l$6*b8DAJsG@8yP zv7cU_UdZce=17mHUyQ~$>dzj*-3oqFxLr+VWi~Xp%44uHE`eJ+RX@nb;!B3$lp{9` z76B3l&rPUV!~UJ1{R%hmWf5j1Ef*+mgc7BXxqFY&@3?7fmcvPW0j+i8X*;S*L?jEJ ze4NQ>FY>XAj6NRh{+oEPk7WdCSH8DA66kCQ(6UoRjP@$YbfH#s3KFCkvIr$SAs%A; zwI{Xu)7ly5m^5BRpj(Gt3NxW)rYciYz-?14SFPNHn^QV3Z@T2T;KpSdqK(KUa?N6P zMm$wiKxnF~*FCK>J`f2C#Q29$R)6FIJZp5HwjvR>!kJYXPl&08K17o0?#2Sj& zE;p9mxmO)R7jkUMOxXY!u*M4X2N^SehH82cbiPU`z-f8!5Ta6L+ljqq3l%7VVh*KI3>_6I8z42>< z@`ld}@c|Ul%-oP>Z$^@siQi%-n3xIk#Vx3iKqzyhgrtU~psLrZ46Ztrzve2L9mu~W ze@!xrmBSCBKU6V9 zP=rUpP;Y>=4%17+a6{xUB8Q9)x(rSj>UJ0l=Q*Onz94DWD&(YGBJL)k1t*2ZSY|C0 z$?yQEswje}W&WkS9^-Jx8vnH<0Z6GhphFn#M7bI4q6h0g|M1KN158 zieq7(*I&hmIgLo>^!4ebd>ljtPUeeK6`^qEjFCvV1jWR}bIT2Z8;MHklSKHO$MkOF z*ioA)x6Ku_dCu1+$QtMTHT@MG%{Y!DVa?y3zJql3$Vg^hq!Tk32UzQKGw9pZg9|7- zPHb#M5-%5tMUH?)1`%Q;XS59Ydn3~49A>%AOX?DECosCU1lgZ!-(tY1=_MTX51dGP zDXXy$4lW0mfIm?t4c1m)YRAU(R|3nXmy853b$onDOC)X_#WFXvIL7-9@#c~ zST>7r9^R&P`3`I$aUcDe#X6JCcnN9p9E{_ap1X$UF2jbHBY(Nib1?6E4sxhx!L4Tz zY3kE;{L(W^ea}!^@8jMFstmvVJ-58n!Q^?6BH#uRgK_@w_vOxzHzKRn&T5CZ!nGMHXU z8Sz*ydI40vY6Lllc=}2coCA2O`CHAFvw~T1YX$o@5F?&>=_bqW%6IU}b$~^R1l*+; z^E_(Gg?eX|Miv0QCa;n)*P$ZYaW;w<*8*r9|4(-%7^~cbTGQ zHyV$2gUcEB!0ExNO+h>;Igm|eY!cWdGB5xNOVKTatr4+j-IZXs3;Tt$6etj6A^RUd zd2EO^LWLp@%sp@tllzc6br7`U5J&OQ13dJ`kl~CftVAvOEgHXHoOn(pexkTMCx9=a ztGYa=?sI*pT8iJNAbkqLH&XmFO6!O;jE}!0z7uZ(!Z(>k)S-OYA)MggP_t`7Q|pNQ zJ2#sNusQtIfb3%q32(aBV3G0}T&rv@A5FJyg#LZ*qM;&?4NNdsLEFZ;nI5GPooXEY13(cIH4X1NCGdb~Bu_%}= z13v3GSR`WJl*N18}KbdtB&2>2q(V(0%hQmf%Zlj=|P*_WU1VSVEZQLSTsv7 zFt9_4oXK!wckT%q5dH14`tB+VM2o!JHWQigaTjeQ}L*QMW5#FUT=Rz~P_d1)_# zGPl?Xpj>)6e2QMtLM=TGJ)I&Bm!XgRj4>C-HIIDOY<>Fps?1gxza}IJGtcplqUz~5+BA%?9 zGu680>B6}!y!Fss^fQnmBEq)Icvm0q@}DtV97S2_<^| ztVfWEKkL!=*D?GRp8abZ{t7|evW{blUKymn{UQGL25KJgcQ@8Q3zBODfzJy2Tm1g| HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_1/bootloader-tasmota-32.bin b/raw/esp32/Shelly_Pro_1/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_1/bootloader.be b/raw/esp32/Shelly_Pro_1/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_Pro_1/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_Pro_1/init.bat b/raw/esp32/Shelly_Pro_1/init.bat new file mode 100644 index 00000000..9d169020 --- /dev/null +++ b/raw/esp32/Shelly_Pro_1/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_Pro_1.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Pro 1","GPIO":[0,1,0,1,768,0,0,0,672,704,736,0,0,0,5600,6214,0,0,0,5568,0,0,0,0,0,0,0,0,0,0,0,32,4736,0,160,0],"FLAG":0,"BASE":1,"CMND":"AdcParam1 2,10000,10000,3350"} +Module 0 diff --git a/raw/esp32/Shelly_Pro_1/migrate_shelly.be b/raw/esp32/Shelly_Pro_1/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_Pro_1/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_Pro_1/partition_core_shelly.be b/raw/esp32/Shelly_Pro_1/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_Pro_1/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_Pro_1PM/Partition_Wizard.tapp b/raw/esp32/Shelly_Pro_1PM/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_1PM/bootloader-tasmota-32.bin b/raw/esp32/Shelly_Pro_1PM/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_1PM/bootloader.be b/raw/esp32/Shelly_Pro_1PM/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_Pro_1PM/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_Pro_1PM/init.bat b/raw/esp32/Shelly_Pro_1PM/init.bat new file mode 100644 index 00000000..aaf71cd6 --- /dev/null +++ b/raw/esp32/Shelly_Pro_1PM/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_Pro_1PM.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Pro 1PM","GPIO":[9568,1,9472,1,768,0,0,0,672,704,736,0,0,0,5600,6214,0,0,0,5568,0,0,0,0,0,0,0,0,3459,0,0,32,4736,0,160,0],"FLAG":0,"BASE":1,"CMND":"AdcParam1 2,10000,10000,3350"} +Module 0 diff --git a/raw/esp32/Shelly_Pro_1PM/migrate_shelly.be b/raw/esp32/Shelly_Pro_1PM/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_Pro_1PM/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_Pro_1PM/partition_core_shelly.be b/raw/esp32/Shelly_Pro_1PM/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_Pro_1PM/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_Pro_2/Partition_Wizard.tapp b/raw/esp32/Shelly_Pro_2/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_2/bootloader-tasmota-32.bin b/raw/esp32/Shelly_Pro_2/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_2/bootloader.be b/raw/esp32/Shelly_Pro_2/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_Pro_2/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_Pro_2/init.bat b/raw/esp32/Shelly_Pro_2/init.bat new file mode 100644 index 00000000..e10aed9a --- /dev/null +++ b/raw/esp32/Shelly_Pro_2/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_Pro_2.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Pro 2","GPIO":[0,1,0,1,768,0,0,0,672,704,736,0,0,0,5600,6214,0,0,0,5568,0,0,0,0,0,0,0,0,0,0,0,32,4736,4737,160,161],"FLAG":0,"BASE":1,"CMND":"AdcParam1 2,10000,10000,3350 | AdcParam2 2,10000,10000,3350"} +Module 0 diff --git a/raw/esp32/Shelly_Pro_2/migrate_shelly.be b/raw/esp32/Shelly_Pro_2/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_Pro_2/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_Pro_2/partition_core_shelly.be b/raw/esp32/Shelly_Pro_2/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_Pro_2/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_Pro_2PM/Partition_Wizard.tapp b/raw/esp32/Shelly_Pro_2PM/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_2PM/bootloader-tasmota-32.bin b/raw/esp32/Shelly_Pro_2PM/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_2PM/bootloader.be b/raw/esp32/Shelly_Pro_2PM/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_Pro_2PM/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_Pro_2PM/init.bat b/raw/esp32/Shelly_Pro_2PM/init.bat new file mode 100644 index 00000000..b40b7033 --- /dev/null +++ b/raw/esp32/Shelly_Pro_2PM/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_Pro_2PM.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly Pro 2PM","GPIO":[9568,1,9472,1,768,0,0,0,672,704,736,9569,0,0,5600,6214,0,0,0,5568,0,0,0,0,0,0,0,0,3460,0,0,32,4736,4737,160,161],"FLAG":0,"BASE":1,"CMND":"AdcParam1 2,10000,10000,3350 | AdcParam2 2,10000,10000,3350"} +Module 0 diff --git a/raw/esp32/Shelly_Pro_2PM/migrate_shelly.be b/raw/esp32/Shelly_Pro_2PM/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_Pro_2PM/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_Pro_2PM/partition_core_shelly.be b/raw/esp32/Shelly_Pro_2PM/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_Pro_2PM/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_Pro_3/Partition_Wizard.tapp b/raw/esp32/Shelly_Pro_3/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_3/bootloader-tasmota-32.bin b/raw/esp32/Shelly_Pro_3/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_3/bootloader.be b/raw/esp32/Shelly_Pro_3/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_Pro_3/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_Pro_3/init.bat b/raw/esp32/Shelly_Pro_3/init.bat new file mode 100644 index 00000000..559f179e --- /dev/null +++ b/raw/esp32/Shelly_Pro_3/init.bat @@ -0,0 +1,2 @@ +Br load("Shelly_Pro_3.autoconf#migrate_shelly.be") + diff --git a/raw/esp32/Shelly_Pro_3/migrate_shelly.be b/raw/esp32/Shelly_Pro_3/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_Pro_3/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_Pro_3/partition_core_shelly.be b/raw/esp32/Shelly_Pro_3/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_Pro_3/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-# diff --git a/raw/esp32/Shelly_Pro_3EM/Partition_Wizard.tapp b/raw/esp32/Shelly_Pro_3EM/Partition_Wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21b98ba0a2f175f3df902054f3f209d9854 GIT binary patch literal 17544 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 HW4ii39kNrx literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_3EM/bootloader-tasmota-32.bin b/raw/esp32/Shelly_Pro_3EM/bootloader-tasmota-32.bin new file mode 100644 index 0000000000000000000000000000000000000000..e7967ddc1d92aa0df10073aaaa993a13155878fd GIT binary patch literal 15728 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 N{P^6TmY?4D{{S&UtResa literal 0 HcmV?d00001 diff --git a/raw/esp32/Shelly_Pro_3EM/bootloader.be b/raw/esp32/Shelly_Pro_3EM/bootloader.be new file mode 100644 index 00000000..83a3b611 --- /dev/null +++ b/raw/esp32/Shelly_Pro_3EM/bootloader.be @@ -0,0 +1,108 @@ +# +# Flash bootloader from URL or filesystem +# + +class bootloader + static var _addr = [0x1000, 0x0000] # possible addresses for bootloader + static var _sign = bytes('E9') # signature of the bootloader + static var _addr_high = 0x8000 # address of next partition after bootloader + + # get the bootloader address, 0x1000 for Xtensa based, 0x0000 for RISC-V based (but might have some exception) + # we prefer to probed what's already in place rather than manage a hardcoded list of architectures + # (there is a low risk of collision if the address is 0x0000 and offset 0x1000 is actually E9) + def get_bootloader_address() + import flash + # let's see where we find 0xE9, trying first 0x1000 then 0x0000 + for addr : self._addr + if flash.read(addr, size(self._sign)) == self._sign + return addr + end + end + return nil + end + + # + # download from URL and store to `bootloader.bin` + # + def download(url) + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil raise "internal_error", "can't find address for bootloader" end + + var cl = webclient() + cl.begin(url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var bl_size = cl.get_size() + if bl_size <= 8291 raise "internal_error", "wrong bootloader size "+str(bl_size) end + if bl_size > (0x8000 - addr) raise "internal_error", "bootloader is too large "+str(bl_size / 1024)+"kB" end + + cl.write_file("bootloader.bin") + cl.close() + end + + # returns true if ok + def flash(url) + var fname = "bootloader.bin" # default local name + if url != nil + if url[0..3] == "http" # if starts with 'http' download + self.download(url) + else + fname = url # else get from file system + end + end + # address to flash the bootloader + var addr = self.get_bootloader_address() + if addr == nil tasmota.log("OTA: can't find address for bootloader", 2) return false end + + var bl = open(fname, "r") + if bl.readbytes(size(self._sign)) != self._sign + tasmota.log("OTA: file does not contain a bootloader signature", 2) + return false + end + bl.seek(0) # reset to start of file + + var bl_size = bl.size() + if bl_size <= 8291 tasmota.log("OTA: wrong bootloader size "+str(bl_size), 2) return false end + if bl_size > (0x8000 - addr) tasmota.log("OTA: bootloader is too large "+str(bl_size / 1024)+"kB", 2) return false end + + tasmota.log("OTA: Flashing bootloader", 2) + # from now on there is no turning back, any failure means a bricked device + import flash + # read current value for bytes 2/3 + var cur_config = flash.read(addr, 4) + + flash.erase(addr, self._addr_high - addr) # erase the bootloader + var buf = bl.readbytes(0x1000) # read by chunks of 4kb + # put back signature + buf[2] = cur_config[2] + buf[3] = cur_config[3] + while size(buf) > 0 + flash.write(addr, buf, true) # set flag to no-erase since we already erased it + addr += size(buf) + buf = bl.readbytes(0x1000) # read next chunk + end + bl.close() + tasmota.log("OTA: Booloader flashed, please restart", 2) + return true + end +end + +return bootloader + +#- + +### FLASH +import bootloader +bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dio_40m.bin') + +#bootloader().flash('https://raw.githubusercontent.com/espressif/arduino-esp32/master/tools/sdk/esp32/bin/bootloader_dout_40m.bin') + +### FLASH from local file +bootloader().flash("bootloader-tasmota-c3.bin") + +#### debug only +bl = bootloader() +print(format("0x%04X", bl.get_bootloader_address())) + +-# \ No newline at end of file diff --git a/raw/esp32/Shelly_Pro_3EM/init.bat b/raw/esp32/Shelly_Pro_3EM/init.bat new file mode 100644 index 00000000..0f26380c --- /dev/null +++ b/raw/esp32/Shelly_Pro_3EM/init.bat @@ -0,0 +1,3 @@ +Br load("Shelly_Pro_3EM.autoconf#migrate_shelly.be") +Template {"NAME":"Shelly 3EM","GPIO":[1,1,288,1,32,8065,0,0,640,8064,608,224,8096,0],"FLAG":0,"BASE":18} +Module 0 diff --git a/raw/esp32/Shelly_Pro_3EM/migrate_shelly.be b/raw/esp32/Shelly_Pro_3EM/migrate_shelly.be new file mode 100644 index 00000000..9fc449f5 --- /dev/null +++ b/raw/esp32/Shelly_Pro_3EM/migrate_shelly.be @@ -0,0 +1,70 @@ +# migration script for Shelly + +# simple function to copy from autoconfig archive to filesystem +# return true if ok +def cp(from, to) + import path + if to == nil to = from end # to is optional + if !path.exists(to) + try + # tasmota.log("f_in="+tasmota.wd + from) + var f_in = open(tasmota.wd + from) + var f_content = f_in.readbytes() + f_in.close() + var f_out = open(to, "w") + f_out.write(f_content) + f_out.close() + except .. as e,m + tasmota.log("OTA: Couldn't copy "+to+" "+e+" "+m,2) + return false + end + return true + end + return true +end + +# make some room if there are some leftovers from shelly +import path +path.remove("index.html.gz") + +# copy some files from autoconf to filesystem +var ok +ok = cp("bootloader-tasmota-32.bin") +ok = cp("Partition_Wizard.tapp") + +# use an alternative to partition_core that can read Shelly's otadata +tasmota.log("OTA: loading "+tasmota.wd + "partition_core_shelly.be", 2) +load(tasmota.wd + "partition_core_shelly.be") + +# load bootloader flasher +tasmota.log("OTA: loading "+tasmota.wd + "bootloader.be", 2) +load(tasmota.wd + "bootloader.be") + + +# all good +if ok + # do some basic check that the bootloader is not already in place + import flash + if flash.read(0x2000, 4) == bytes('0030B320') + tasmota.log("OTA: bootloader already in place, not flashing it") + else + ok = global.bootloader().flash("bootloader-tasmota-32.bin") + end + if ok + var p = global.partition_core_shelly.Partition() + p.save() # save with otadata compatible with new bootloader + tasmota.log("OTA: Shelly migration successful", 2) + end +end + +# dump logs to file +var lr = tasmota_log_reader() +var f_logs = open("migration_logs.txt", "w") +var logs = lr.get_log(2) +while logs != nil + f_logs.write(logs) + logs = lr.get_log(2) +end +f_logs.close() + +# Done diff --git a/raw/esp32/Shelly_Pro_3EM/partition_core_shelly.be b/raw/esp32/Shelly_Pro_3EM/partition_core_shelly.be new file mode 100644 index 00000000..80c809aa --- /dev/null +++ b/raw/esp32/Shelly_Pro_3EM/partition_core_shelly.be @@ -0,0 +1,645 @@ +####################################################################### +# Partition manager for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_core_shelly` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_core_shelly = module('partition_core_shelly') + +####################################################################### +# Class for a partition table entry +# +# typedef struct { +# uint16_t magic; +# uint8_t type; +# uint8_t subtype; +# uint32_t offset; +# uint32_t size; +# uint8_t label[16]; +# uint32_t flags; +# } esp_partition_info_t_simplified; +# +####################################################################### +class Partition_info + var type + var subtype + var start + var sz + var label + var flags + + #- remove trailing NULL chars from a bytes buffer before converting to string -# + #- Berry strings can contain NULL, but this messes up C-Berry interface -# + static def remove_trailing_zeroes(b) + var sz = size(b) + var i = 0 + while i < sz + if b[-1-i] != 0 break end + i += 1 + end + if i > 0 + b.resize(size(b)-i) + end + return b + end + + # Init the Parition information structure, either from a bytes() buffer or an empty if no buffer is provided + def init(raw) + self.type = 0 + self.subtype = 0 + self.start = 0 + self.sz = 0 + self.label = '' + self.flags = 0 + + if !issubclass(bytes, raw) # no payload, empty partition information + return + end + + #- we have a payload, parse it -# + var magic = raw.get(0,2) + if magic == 0x50AA #- partition entry -# + + self.type = raw.get(2,1) + self.subtype = raw.get(3,1) + self.start = raw.get(4,4) + self.sz = raw.get(8,4) + self.label = self.remove_trailing_zeroes(raw[12..27]).asstring() + self.flags = raw.get(28,4) + + # elif magic == 0xEBEB #- MD5 -# + else + import string + raise "internal_error", string.format("invalid magic number %02X", magic) + end + + end + + # check if the parition is an OTA partition + # if yes, return OTA number (starting at 0) + # if no, return nil + def is_ota() + var sub_type = self.subtype + if self.type == 0 && (sub_type >= 0x10 && sub_type < 0x20) + return sub_type - 0x10 + end + end + + # check if factory 'safeboot' partition + def is_factory() + return self.type == 0 && self.subtype == 0 + end + + # check if the parition is a SPIFFS partition + # returns bool + def is_spiffs() + return self.type == 1 && self.subtype == 130 + end + + # get the actual image size give of the partition + # returns -1 if the partition is not an app ota partition + def get_image_size() + import flash + if self.is_ota() == nil && !self.is_factory() return -1 end + try + var addr = self.start + var sz = self.sz + var magic_byte = flash.read(addr, 1).get(0, 1) + if magic_byte != 0xE9 return -1 end + + var seg_count = flash.read(addr+1, 1).get(0, 1) + # print("Segment count", seg_count) + + var seg_offset = addr + 0x20 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) = 24 + 8 + + var seg_num = 0 + while seg_num < seg_count + # print(string.format("Reading 0x%08X", seg_offset)) + var segment_header = flash.read(seg_offset - 8, 8) + var seg_start_addr = segment_header.get(0, 4) + var seg_size = segment_header.get(4,4) + # print(string.format("Segment %i: flash_offset=0x%08X start_addr=0x%08X sz=0x%08X", seg_num, seg_offset, seg_start_addr, seg_size)) + + seg_offset += seg_size + 8 # add segment_length + sizeof(esp_image_segment_header_t) + if seg_offset >= (addr + sz) return -1 end + + seg_num += 1 + end + var total_size = seg_offset - addr + 1 # add 1KB for safety + + # print(string.format("Total size = %i KB", total_size/1024)) + + return total_size + except .. as e, m + tasmota.log("BRY: Exception> '" + e + "' - " + m, 2) + return -1 + end + end + + def type_to_string() + if self.type == 0 return "app" + elif self.type == 1 return "data" + end + import string + return string.format("0x%02X", self.type) + end + + def subtype_to_string() + if self.type == 0 + if self.subtype == 0 return "factory" + elif self.subtype >= 0x10 && self.subtype < 0x20 return "ota_" + str(self.subtype - 0x10) + elif self.subtype == 0x20 return "test" + end + elif self.type == 1 + if self.subtype == 0x00 return "otadata" + elif self.subtype == 0x01 return "phy" + elif self.subtype == 0x02 return "nvs" + elif self.subtype == 0x03 return "coredump" + elif self.subtype == 0x04 return "nvskeys" + elif self.subtype == 0x05 return "efuse_em" + elif self.subtype == 0x80 return "esphttpd" + elif self.subtype == 0x81 return "fat" + elif self.subtype == 0x82 return "spiffs" + end + end + import string + return string.format("0x%02X", self.subtype) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + def tostring() + import string + var type_s = self.type_to_string() + var subtype_s = self.subtype_to_string() + + # reformat strings + if type_s != "" type_s = " (" + type_s + ")" end + if subtype_s != "" subtype_s = " (" + subtype_s + ")" end + return string.format("", + self.type, type_s, + self.subtype, subtype_s, + self.start, self.sz, + self.label, self.flags) + end + + def tobytes() + #- convert to raw bytes -# + var b = bytes('AA50') #- set magic number -# + b.resize(32).resize(2) #- pre-reserve 32 bytes -# + b.add(self.type, 1) + b.add(self.subtype, 1) + b.add(self.start, 4) + b.add(self.sz, 4) + var label = bytes().fromstring(self.label) + label.resize(16) + b = b + label + b.add(self.flags, 4) + return b + end + +end +partition_core_shelly.Partition_info = Partition_info + +#------------------------------------------------------------- + - OTA Data + - + - Selection of the active OTA partition + - + typedef struct { + uint32_t ota_seq; + uint8_t seq_label[20]; + uint32_t ota_state; + uint32_t crc; /* CRC32 of ota_seq field only */ + } esp_ota_select_entry_t; + + - Excerp from esp_ota_ops.c + esp32_idf use two sector for store information about which partition is running + it defined the two sector as ota data partition,two structure esp_ota_select_entry_t is saved in the two sector + named data in first sector as otadata[0], second sector data as otadata[1] + e.g. + if otadata[0].ota_seq == otadata[1].ota_seq == 0xFFFFFFFF,means ota info partition is in init status + so it will boot factory application(if there is),if there's no factory application,it will boot ota[0] application + if otadata[0].ota_seq != 0 and otadata[1].ota_seq != 0,it will choose a max seq ,and get value of max_seq%max_ota_app_number + and boot a subtype (mask 0x0F) value is (max_seq - 1)%max_ota_app_number,so if want switch to run ota[x],can use next formulas. + for example, if otadata[0].ota_seq = 4, otadata[1].ota_seq = 5, and there are 8 ota application, + current running is (5-1)%8 = 4,running ota[4],so if we want to switch to run ota[7], + we should add otadata[0].ota_seq (is 4) to 4 ,(8-1)%8=7,then it will boot ota[7] + if A=(B - C)%D + then B=(A + C)%D + D*n ,n= (0,1,2...) + so current ota app sub type id is x , dest bin subtype is y,total ota app count is n + seq will add (x + n*1 + 1 - seq)%n + -------------------------------------------------------------# +class Partition_otadata + var maxota # number of highest OTA partition, default 1 (double ota0/ota1) + var has_factory # is there a factory partition + var offset # offset of the otadata partition (0x2000 in length), default 0xE000 + var active_otadata # which otadata block is active, 0 or 1, i.e. 0xE000 or 0xF000 -- or -1 if no OTA active, i.e. boot on factory + var seq0 # ota_seq of first block + var seq1 # ota_seq of second block + + #- crc32 for ota_seq as 32 bits unsigned, with init vector -1 -# + static def crc32_ota_seq(seq) + import crc + return crc.crc32(0xFFFFFFFF, bytes().add(seq, 4)) + end + + #---------------------------------------------------------------------# + # Rest of the class + #---------------------------------------------------------------------# + def init(maxota, has_factory, offset) + self.maxota = maxota + self.has_factory = has_factory + if self.maxota == nil self.maxota = 1 end + self.offset = offset + if self.offset == nil self.offset = 0xE000 end + self.active_otadata = -1 + self.load() + end + + #- update ota_max, needs to recompute everything -# + def set_ota_max(n) + self.maxota = n + end + + # change the active OTA partition + def set_active(n) + var seq_max = 0 #- current highest seq number -# + var block_act = 0 #- block number containing the highest seq number -# + + if self.seq0 != nil + seq_max = self.seq0 + block_act = 0 + end + if self.seq1 != nil && self.seq1 > seq_max + seq_max = self.seq1 + block_act = 1 + end + + #- compute the next sequence number -# + var actual_ota = (seq_max - 1) % (self.maxota + 1) + if actual_ota != n #- change only if different -# + if n > actual_ota seq_max += n - actual_ota + else seq_max += (self.maxota + 1) - actual_ota + n + end + + #- update internal structure -# + if block_act == 1 #- current block is 1, so update block 0 -# + self.seq0 = seq_max + else #- or write to block 1 -# + self.seq1 = seq_max + end + self._validate() + end + end + + #- load otadata from SPI Flash -# + def load() + import flash + var otadata0 = flash.read(self.offset, 32) + var otadata1 = flash.read(self.offset + 0x1000, 32) + self.seq0 = otadata0.get(0, 4) #- ota_seq for block 1 -# + self.seq1 = otadata1.get(0, 4) #- ota_seq for block 2 -# + # var valid0 = otadata0.get(28, 4) == self.crc32_ota_seq(self.seq0) #- is CRC32 valid? -# + # var valid1 = otadata1.get(28, 4) == self.crc32_ota_seq(self.seq1) #- is CRC32 valid? -# + # if !valid0 self.seq0 = nil end + # if !valid1 self.seq1 = nil end + + self._validate() + end + + #- internally used, validate data -# + def _validate() + self.active_otadata = self.has_factory ? -1 : 0 # if no valid otadata, then use factory (-1) if any, or ota_0 + if self.seq0 != nil + self.active_otadata = (self.seq0 - 1) % (self.maxota + 1) + end + if self.seq1 != nil && (self.seq0 == nil || self.seq1 > self.seq0) + self.active_otadata = (self.seq1 - 1) % (self.maxota + 1) + end + end + + # Save partition information to SPI Flash + def save() + import flash + #- check the block number to save, 0 or 1. Choose the highest ota_seq -# + var block_to_save = -1 #- invalid -# + var seq_to_save = -1 #- invalid value -# + + # check seq0 + if self.seq0 != nil + seq_to_save = self.seq0 + block_to_save = 0 + end + if (self.seq1 != nil) && (self.seq1 > seq_to_save) + seq_to_save = self.seq1 + block_to_save = 1 + end + # if none was good + if block_to_save < 0 block_to_save = 0 end + if seq_to_save < 0 seq_to_save = 1 end + + var offset_to_save = self.offset + 0x1000 * block_to_save #- default 0xE000 or 0xF000 -# + + var bytes_to_save = bytes() + bytes_to_save.add(seq_to_save, 4) + bytes_to_save += bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + bytes_to_save.add(self.crc32_ota_seq(seq_to_save), 4) + + #- erase flash area and write -# + flash.erase(offset_to_save, 0x1000) + flash.write(offset_to_save, bytes_to_save) + end + + # Produce a human-readable representation of the object with relevant information + def tostring() + import string + return string.format("", + self.active_otadata >= 0 ? "ota_" + str(self.active_otadata) : "factory", + self.seq0, self.seq1, self.maxota) + end +end +partition_core_shelly.Partition_otadata = Partition_otadata + +#------------------------------------------------------------- + - Class for a partition table entry + -------------------------------------------------------------# +class Partition + var raw #- raw bytes of the partition table in flash -# + var md5 #- md5 hash of partition list -# + var slots + var otadata #- instance of Partition_otadata() -# + + def init() + self.slots = [] + self.load() + self.parse() + self.load_otadata() + end + + # Load partition information from SPI Flash + def load() + import flash + self.raw = flash.read(0x8000,0x1000) + end + + #- parse the raw bytes to a structured list of partition items -# + def parse() + for i:0..94 # there are maximum 95 slots + md5 (0xC00) + var item_raw = self.raw[i*32..(i+1)*32-1] + var magic = item_raw.get(0,2) + if magic == 0x50AA #- partition entry -# + var slot = partition_core_shelly.Partition_info(item_raw) + self.slots.push(slot) + elif magic == 0xEBEB #- MD5 -# + self.md5 = self.raw[i*32+16..i*33-1] + break + else + break + end + end + end + + def get_ota_slot(n) + for slot: self.slots + if slot.is_ota() == n return slot end + end + return nil + end + + def get_factory_slot() + for slot: self.slots + if slot.is_factory() return slot end + end + end + + def has_factory() + return self.get_factory_slot() != nil + end + + #- compute the highest ota partition -# + def ota_max() + var ota_max = nil + for slot:self.slots + if slot.type == 0 && (slot.subtype >= 0x10 && slot.subtype < 0x20) + var ota_num = slot.subtype - 0x10 + if (ota_max == nil) || (ota_num > ota_max) ota_max = ota_num end + end + end + return ota_max + end + + # get the active OTA app partition number + def get_active() + return self.otadata.active_otadata + end + + def load_otadata() + #- look for otadata partition offset, and max_ota -# + var otadata_offset = 0xE000 #- default value -# + var ota_max = self.ota_max() + for slot:self.slots + if slot.type == 1 && slot.subtype == 0 #- otadata -# + otadata_offset = slot.start + end + end + + self.otadata = partition_core_shelly.Partition_otadata(ota_max, self.has_factory(), otadata_offset) + end + + #- change the active partition -# + def set_active(n) + if n < 0 || n > self.ota_max() raise "value_error", "Invalid ota partition number" end + self.otadata.set_ota_max(self.ota_max()) #- update ota_max if it changed -# + self.otadata.set_active(n) + end + + # Human readable version of Partition information + # this method is not included in the solidified version to save space, + # it is included only in the optional application `tapp` version + #- convert to human readble -# + def tostring() + var ret = " 95 raise "value_error", "Too many partiition slots" end + var b = bytes() + for slot: self.slots + b += slot.tobytes() + end + #- compute MD5 -# + var md5 = MD5() + md5.update(b) + #- add the last segment -# + b += bytes("EBEBFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + b += md5.finish() + #- complete -# + return b + end + + #- write back to flash -# + def save() + import flash + var b = self.tobytes() + #- erase flash area and write -# + flash.erase(0x8000, 0x1000) + flash.write(0x8000, b) + self.otadata.save() + end + + # Internal: returns which flash sector contains the partition definition + # Returns 0 or 1, or `nil` if something went wrong + # Note: partition flash sector vary from ESP32 to ESP32C3/S3 + static def get_flash_definition_sector() + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + # Internal: returns the maximum flash size possible + # Returns max flash size ok kB + def get_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + if (flash_size_k != flash_size_real_k) && self.get_flash_definition_sector() != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + # Internal: returns the unallocated flash size (in kB) beyond the file-system + # this indicates that the file-system can be extended (although erased at the same time) + def get_unallocated_k() + var last_slot = self.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k() + var partition_end_k = (last_slot.start + last_slot.sz) / 1024 # last kb used for fs + if partition_end_k < flash_size_k + return flash_size_k - partition_end_k + end + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k() + var flash_size_k = tasmota.memory()['flash'] + var flash_size_real_k = tasmota.memory().find("flash_real", flash_size_k) + var flash_definition_sector = self.get_flash_definition_sector() + if (flash_size_k != flash_size_real_k) && flash_definition_sector != nil + import flash + import string + + flash_size_k = flash_size_real_k # try to expand the flash size definition + + var flash_def = flash.read(flash_definition_sector, 4) + var size_before = flash_def[3] + + var flash_size_code + var flash_size_real_m = flash_size_real_k / 1024 # size in MB + if flash_size_real_m == 1 flash_size_code = 0x00 + elif flash_size_real_m == 2 flash_size_code = 0x10 + elif flash_size_real_m == 4 flash_size_code = 0x20 + elif flash_size_real_m == 8 flash_size_code = 0x30 + elif flash_size_real_m == 16 flash_size_code = 0x40 + elif flash_size_real_m == 32 flash_size_code = 0x50 + elif flash_size_real_m == 64 flash_size_code = 0x60 + elif flash_size_real_m == 128 flash_size_code = 0x70 + end + + if flash_size_code != nil + # apply the update + var old_def = flash_def[3] + flash_def[3] = (flash_def[3] & 0x0F) | flash_size_code + flash.write(flash_definition_sector, flash_def) + tasmota.log(string.format("UPL: changing flash definition from 0x02X to 0x%02X", old_def, flash_def[3]), 3) + else + raise "internal_error", "wrong flash size "+str(flash_size_real_m) + end + end + end + + # Called at first boot + # Try to expand FS to max of flash size + def resize_fs_to_max() + import string + try + var unallocated = self.get_unallocated_k() + if unallocated <= 0 return nil end + + tasmota.log(string.format("BRY: Trying to expand FS by %i kB", unallocated), 2) + + self.resize_max_flash_size_k() # resize if needed + # since unallocated succeeded, we know the last slot is FS + var fs_slot = self.slots[-1] + fs_slot.sz += unallocated * 1024 + self.save() + self.invalidate_spiffs() # erase SPIFFS or data is corrupt + + # restart + tasmota.global.restart_flag = 2 + tasmota.log("BRY: Successfully resized FS, restarting", 2) + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + end + end + + #- invalidate SPIFFS partition to force format at next boot -# + #- we simply erase the first byte of the first 2 blocks in the SPIFFS partition -# + def invalidate_spiffs() + import flash + #- we expect the SPIFFS partition to be the last one -# + var spiffs = self.slots[-1] + if !spiffs.is_spiffs() raise 'value_error', 'No SPIFFS partition found' end + + var b = bytes("00") #- flash memory: we can turn bits from '1' to '0' -# + flash.write(spiffs.start , b) #- block #0 -# + flash.write(spiffs.start + 0x1000, b) #- block #1 -# + end + + # switch to safeboot `factory` partition + def switch_factory(force_ota) + import flash + flash.factory(force_ota) + end +end +partition_core_shelly.Partition = Partition + +# init method to force the global `partition_core_shelly` is defined even if the import is done within a function +def init(m) + import global + global.partition_core_shelly = m + return m +end +partition_core_shelly.init = init + +return partition_core_shelly + +#- Example + +import partition_core_shelly + +# read +p = partition_core_shelly.Partition() +print(p) + +-#