From f8668a539616c8bc2371bd176394fbad23a36318 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Wed, 11 May 2022 09:52:40 +0200 Subject: [PATCH] Partition Wizard v1 --- .../modules/Partition_Wizard/autoexec.be | 2 + .../Partition_Wizard/partition_wizard.be | 801 ++++++++++++++++++ tasmota/berry/modules/Partition_wizard.tapp | Bin 0 -> 31911 bytes 3 files changed, 803 insertions(+) create mode 100644 tasmota/berry/modules/Partition_Wizard/autoexec.be create mode 100644 tasmota/berry/modules/Partition_Wizard/partition_wizard.be create mode 100644 tasmota/berry/modules/Partition_wizard.tapp diff --git a/tasmota/berry/modules/Partition_Wizard/autoexec.be b/tasmota/berry/modules/Partition_Wizard/autoexec.be new file mode 100644 index 000000000..968e1a502 --- /dev/null +++ b/tasmota/berry/modules/Partition_Wizard/autoexec.be @@ -0,0 +1,2 @@ +# rm Partition_wizard.tapp; zip -j -0 Partition_wizard.tapp Partition_Wizard/* +import partition_wizard diff --git a/tasmota/berry/modules/Partition_Wizard/partition_wizard.be b/tasmota/berry/modules/Partition_Wizard/partition_wizard.be new file mode 100644 index 000000000..c73814e01 --- /dev/null +++ b/tasmota/berry/modules/Partition_Wizard/partition_wizard.be @@ -0,0 +1,801 @@ +####################################################################### +# Partition Wizard for ESP32 - ESP32C3 - ESP32S2 +# +# use : `import partition_wizard` +# +# Provides low-level objects and a Web UI +####################################################################### + +var partition_wizard = module('partition_wizard') + +################################################################################# +# Partition_wizard_UI +# +# WebUI for the partition manager +################################################################################# +class Partition_wizard_UI + static app_size_min = 832 # Min OTA size - let's set it at a safe 896KB for minimal Tasmota32 with TLS + static app_size_max = 3072 # Max OTA size - (4096 - 896 - 128) + static _default_safeboot_URL = "https://raw.githubusercontent.com/arendst/Tasmota-firmware/main/firmware/tasmota32/tasmota32%s-safeboot.bin" + + def init() + import persist + + if persist.find("factory_migrate") == true + # remove marker to avoid bootloop if something goes wrong + persist.remove("factory_migrate") + persist.save() + + # continue the migration process 5 seconds after Wifi is connected + def continue_after_5s() + tasmota.remove_rule("parwiz_5s") # first remove rule to avoid firing it again at Wifi reconnect + tasmota.set_timer(5000, /-> self.do_safeboot_partitioning()) # delay by 5 s + end + tasmota.add_rule("Wifi#Connected=1", continue_after_5s, "parwiz_5s") + + end + end + + def default_safeboot_URL() + import string + var arch_sub = tasmota.arch() + if arch_sub[0..4] == "esp32" + arch_sub = arch_sub[5..] # get the esp32 variant + end + return string.format(self._default_safeboot_URL, arch_sub) + end + + # create a method for adding a button to the main menu + # the button 'Partition Wizard' redirects to '/part_wiz?' + def web_add_button() + import webserver + webserver.content_send( + "
") + end + + #- ---------------------------------------------------------------------- -# + #- Get fs unallocated size + #- ---------------------------------------------------------------------- -# + def get_unallocated_k(p) + var last_slot = p.slots[-1] + if last_slot.is_spiffs() + # verify that last slot is filesystem + var flash_size_k = self.get_max_flash_size_k(p) + var partition_end_k = (last_slot.start + last_slot.size) / 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 + + #- ---------------------------------------------------------------------- -# + #- Get max fs start address when expanded to maximum + #- ---------------------------------------------------------------------- -# + def get_max_fs_start_k(p) + var last_slot = p.slots[-1] + if last_slot.is_spiffs() # verify that last slot is filesystem + # get end of previous partition slot + var last_app = p.slots[-2] + # round upper 64kB + var max_fs_start_k = 64 * (((last_app.start + last_app.get_image_size() + 1023) / 1024 + 63) / 64) + return max_fs_start_k + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Get max falsh size + # + # Takes into account that the flash size written may not be accurate + # and the flash chip may be larger + #- ---------------------------------------------------------------------- -# + def get_max_flash_size_k(p) + 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(p) != nil + flash_size_k = flash_size_real_k # try to expand the flash size definition + end + return flash_size_k + end + + #- ---------------------------------------------------------------------- -# + #- Resize flash definition if needed + #- ---------------------------------------------------------------------- -# + def resize_max_flash_size_k(p) + 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(p) + 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 + 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 + + #- ---------------------------------------------------------------------- -# + #- Get current fs size + #- ---------------------------------------------------------------------- -# + def get_cur_fs_size_k(p) + var last_slot = p.slots[-1] + if last_slot.is_spiffs() # verify that last slot is filesystem + return (last_slot.size + 1023) / 1024 + end + return 0 + end + + #- ---------------------------------------------------------------------- -# + #- Get flash sector for flash chip definition + # It appears to be at 0x1000 for ESP32, but at 0x0000 for ESP32C3/S3 + # + # returns offset of sector containing flash information + # or `nil` if not found + # + # Internally looks first at 0x0000 then at 0x1000 for magic byte + #- ---------------------------------------------------------------------- -# + def get_flash_definition_sector(p) + import flash + for i:0..1 + var offset = i * 0x1000 + if flash.read(offset, 1) == bytes('E9') return offset end + end + end + + #- ---------------------------------------------------------------------- -# + #- Propose to resize FS to max if some memory in unallocated + #- ---------------------------------------------------------------------- -# + def show_resize_fs(p) + import webserver + import string + var unallocated = self.get_unallocated_k(p) + + # if there is unallocated space, propose only to claim it + if unallocated > 0 + webserver.content_send("
 Resize FS to max 

") + + webserver.content_send(string.format("

You can expand the file system by %i KB.
Its content will be lost.

", unallocated)) + + webserver.content_send("
") + webserver.content_send("

") + + webserver.content_send("

") + elif self.has_factory_layout(p) + # else propose to expand or shrink the file system + var max_fs_start_k = self.get_max_fs_start_k(p) + var flash_size_k = self.get_max_flash_size_k() + var fs_max_size_k = flash_size_k - max_fs_start_k + var current_fs_size_k = self.get_cur_fs_size_k(p) + #print(string.format(">>> max_fs_start_k=0x%X flash_size_k=0x%X fs_max_size_k=%i current_fs_size_k=%i", max_fs_start_k, flash_size_k, fs_max_size_k, current_fs_size_k)) + + if max_fs_start_k > 0 && fs_max_size_k > 64 + webserver.content_send("
 Resize FS 

") + + webserver.content_send("

You can expand of shrink the file system.
Its content will be lost.

") + + webserver.content_send("
") + webserver.content_send(string.format("", fs_max_size_k, current_fs_size_k)) + webserver.content_send("

") + + webserver.content_send("

") + end + end + end + + #- ---------------------------------------------------------------------- -# + #- Tests for factory layout + #- ---------------------------------------------------------------------- -# + # Returns if the device already has a factory layout: + # devices has 1 factory partition + # device has at least 1 OTA partition + # last partition is FS + # + # returns true or false + def has_factory_layout(p) + return p.has_factory() && p.ota_max() != nil && p.slots[-1].is_spiffs() + end + + #- ---------------------------------------------------------------------- -# + #- Tests for factory migration + #- ---------------------------------------------------------------------- -# + # Returns if the device is eligible for a migration to factory layout: + # devices has 2x OTA partitions + # device has no factory partition + # + # returns true or false + def factory_migrate_eligible(p) + if p.ota_max() <= 0 return false end # device does not have 2x OTA + if p.get_factory_slot() != nil return false end + if !p.slots[-1].is_spiffs() return false end + return true # device does not have factory partition + end + + # ---------------------------------------------------------------------- + # Step 1: + # - pre-condition: + # factory_migrate_eligible(p) returns true + # - DONE state: + # boot on `app1` + # - READY state: + # boot on `app0` + # - Needed steps: + # check that `app1` is large enough for firmware in `app0` + # copy `app0` to `app1` + # restart on `app1` + # set continuation marker in persist to continue migration process at next boto + # + # Returns: + # - false if READY + # - true if DONE + # - string if ERROR, indicating the error + def test_step_1(p) + import string + if !self.factory_migrate_eligible(p) return "not eligible to migration" end + + var cur_part = p.otadata.active_otadata # -1=factory 0=ota_0 1=ota_1... + if cur_part == 1 return true end + if cur_part != 0 return string.format("active_otadata=%i", cur_part) end # unsupported configuration + # current partition is `app0` + # get size of firmware in `app0` and check if it fits on `app1` + var app0 = p.get_ota_slot(0) + var app1 = p.get_ota_slot(0) + var app0_firmware_size = (app0 != nil) ? app0.get_image_size() : -1 + var app1_size = (app1 != nil) ? app1.size : -1 + if app0_firmware_size < 0 || app1_size < 0 return "can't find app0/1 sizes" end + if app0_firmware_size >= app1_size return "`app1` is too small" end + return false + end + + # ---------------------------------------------------------------------- + # Step 2: + # - pre-condition: + # factory_migrate_eligible(p) returns true + # - DONE state: + # `safeboot` flashed to `app0` + # `safeboot` is smaller than 832KB + # - READY state: + # false `safeboot` to `app0` + # - Needed steps: + # get `safeboot` URL + # check that `app0` is large enough for `safeboot` + # check that `safeboot` is smaller than 832KB + # flash `safeboot` on `app0` + # + # Returns: + # - false if READY + # - true if DONE + # - string if ERROR, indicating the error + def test_step_2(p) + import string + if !self.factory_migrate_eligible(p) return "not eligible to migration" end + + var app0 = p.get_ota_slot(0) + if app0.size < (self.app_size_min * 1024) return "`app0` is too small for `safeboot`" end + var app0_image_size = app0.get_image_size() + if (app0_image_size > 0) && (app0_image_size < (self.app_size_min * 1024)) return true end + return false + end + + # ---------------------------------------------------------------------- + # Step 3: + # - pre-condition: + # booted on `app1` and `safeboot` flashed to `app0` + # - DONE state: + # Partition map is: + # `factory` with `safeboot` flashed, sized to 832KB + # `app0` resized to take all the remaining size but empty + # - READY state: + # `app0` is flashed with `safeboot` + # - Needed steps: + # `app0` renamed to `safeboot` + # `app0` changed subtype to `factory` + # `app1` moved to right after `factory` and resized + # `app1` chaned subtype to `app0` and renamed `app0` + # + # Returns: + # - false if READY + # - true if DONE + # - string if ERROR, indicating the error + def test_step_3(p) + import string + if !self.factory_migrate_eligible(p) return "not eligible to migration" end + + return false + # var app0 = p.get_ota_slot(0) + # if app0.get_image_size() > (self.app_size_min * 1024) return "`app0` is too small for `safeboot`" end + end + + + # ---------------------------------------------------------------------- + # Step 4: + # - pre-condition: + # + # Returns: + # - false if READY + # - true if DONE + # - string if ERROR, indicating the error + def test_step_4(p) + import string + + return false + # var app0 = p.get_ota_slot(0) + # if app0.get_image_size() > (self.app_size_min * 1024) return "`app0` is too small for `safeboot`" end + end + + static def copy_ota(from_addr, to_addr, size) + import flash + import string + var size_left = size + var offset = 0 + + tasmota.log(string.format("UPL: Copy flash from 0x%06X to 0x%06X (size: %ikB)", from_addr, to_addr, size / 1024), 2) + while size_left > 0 + var b = flash.read(from_addr + offset, 4096) + flash.erase(to_addr + offset, 4096) + flash.write(to_addr + offset, b, true) + size_left -= 4096 + offset += 4096 + if ((offset-4096) / 102400) < (offset / 102400) + tasmota.log(string.format("UPL: Progress %ikB", offset/1024), 3) + end + end + tasmota.log("UPL: done", 2) + end + + def do_step_1(p) + var step1_state = self.test_step_1(p) + if step1_state == true return true end + if type(step1_state) == 'string)' raise "internal_error", step1_state end + + # copy firmware frop `app0` to `app1` + var app0 = p.get_ota_slot(0) + var app1 = p.get_ota_slot(1) + var app0_size = app0.get_image_size() + if app0_size > app1.size raise "internal_error", "`app1` too small to copy firmware form `app0`" end + self.copy_ota(app0.start, app1.start, app0_size) + + p.set_active(1) + p.save() + + tasmota.log("UPL: restarting on `app1`", 2) + tasmota.cmd("Restart 1") + end + + def do_step_2(p, safeboot_url) + import string + if safeboot_url == nil || safeboot_url == "" + safeboot_url = self.default_safeboot_URL() + tasmota.log("UPL: no `safeboot` URL, defaulting to "+safeboot_url, 2) + end + + var step2_state = self.test_step_2(p) + if step2_state == true return true end + if type(step2_state) == 'string)' raise "internal_error", step2_state end + if safeboot_url == nil || size(safeboot_url) == 0 raise "internal_error", "wrong safeboot URL "+str(safeboot_url) end + + var cl = webclient() + cl.begin(safeboot_url) + var r = cl.GET() + if r != 200 raise "network_error", "GET returned "+str(r) end + var safeboot_size = cl.get_size() + if safeboot_size <= 500000 raise "internal_error", "wrong safeboot size "+str(safeboot_size) end + if safeboot_size > (self.app_size_min * 1024) raise "internal_error", "safeboot is too large "+str(safeboot_size / 1024)+"kB" end + tasmota.log(string.format("UPL: flashing `safeboot` from %s %ikB", safeboot_url, (safeboot_size / 1024) + 1), 2) + var app0 = p.get_ota_slot(0) + if app0.start != 0x10000 raise "internal_error", "`app0` offset is not 0x10000" end + cl.write_flash(app0.start) + cl.close() + return true + end + + + def do_step_3(p) + import string + import flash + + var step3_state = self.test_step_3(p) + if step3_state == true return true end + if type(step3_state) == 'string)' raise "internal_error", step3_state end + + var app0 = p.get_ota_slot(0) + var app1 = p.get_ota_slot(1) + if app0 == nil || app1 == nil raise "internal_error", "there are no `app0` or `app1` partitions" end + var factory_size = self.app_size_min * 1024 + + var firm0_size = app0.get_image_size() + if firm0_size <= 0 raise "internal_error", "invalid size in app0 partition" end + if firm0_size >= factory_size raise "internal_error", "app0 partition is too big for factory" end + + # do the change + app0.subtype = 0 # factory subtype + app0.size = factory_size + app0.label = 'safeboot' + + app1.subtype = 0x10 # app1 becomes app0 + app1.label = 'app0' + var f1_start = app1.start + app1.start = app0.start + factory_size + app1.size += f1_start - app1.start + + # swicth partitions + p.set_active(0) + p.save() + + p.switch_factory(true) + tasmota.cmd("Restart 1") + + return true + end + + + # display the step state DONE/READY/ERROR with color and either step description or error message + # arg + # state: true=DONE, false=READY, string=ERROR with message + # msg: description if DONE or READY + # returns HTML string + def display_step_state(state, msg) + if state == true + return "DONE "+msg + elif state == false + return "READY "+msg + else + return "ERROR "+str(state) + end + end + #- ---------------------------------------------------------------------- -# + #- Show page to migrate to factory layout + single OTA + #- ---------------------------------------------------------------------- -# + def show_migrate_to_factory(p) + # display ota partitions + import webserver + import string + + if !self.factory_migrate_eligible(p) return end + + webserver.content_send("
 Migrate to safeboot partition layout 

") + + webserver.content_send("

The `safeboot` layout allows for increased size
of firmware or file-system.

") + webserver.content_send("

Please see Safeboot layout documentation

") + webserver.content_send("

 

") + + webserver.content_send(string.format("

Step 1: %s

", self.display_step_state(self.test_step_1(p), "boot on `app1`"))) + webserver.content_send(string.format("

Step 2: %s

", self.display_step_state(self.test_step_2(p), "flash `safeboot` to `app0`"))) + webserver.content_send(string.format("

Step 3: %s

", self.display_step_state(self.test_step_3(p), "change partition map"))) + webserver.content_send(string.format("

Step 4: %s

", self.display_step_state(self.test_step_4(p), "flash final firmware"))) + + webserver.content_send("
") + var ota_url = tasmota.cmd("OtaUrl").find("OtaUrl", "") + webserver.content_send(string.format("
OTA Url

", + ota_url)) + + import persist + var safeboot_url = persist.find("safeboot_url", self.default_safeboot_URL()) + webserver.content_send(string.format("
SAFEBOOT Url (don't change)
", + safeboot_url)) + + webserver.content_send("

") + + webserver.content_send("

") + end + + #- ---------------------------------------------------------------------- -# + #- Show each partition one after the other - only OTA and SPIFFS + #- ---------------------------------------------------------------------- -# + def show_current_partitions(p) + # display ota partitions + import webserver + import string + var cur_part = p.otadata.active_otadata # -1=factory 0=ota_0 1=ota_1... + + webserver.content_send("
 Current partitions 

") + + # don't show portion + #webserver.content_send("") + + for slot:p.slots + var is_ota = slot.is_ota() + var is_factory = slot.is_factory() + if (is_ota != nil) || is_factory # display only partitions with app type + var current_boot_partition = (is_ota == cur_part) || (is_factory && cur_part == -1) + + var usage_str = "unknown" + var used = slot.get_image_size() + if (used >= 0) && (used <= slot.size) + usage_str = string.format("used %i%%", ((used / 1024) * 100) / (slot.size / 1024)) + end + var title = string.format("%ssubtype:%s offset:0x%06X size:0x%06X", current_boot_partition ? "booted " : "", slot.subtype_to_string(), slot.start, slot.size) + var col_before = "" + var col_after = "" + if current_boot_partition + col_before = "[" + col_after = "]" + end + # webserver.content_send(string.format("

%s [%s]: %i KB (%s)

", slot.label, slot.subtype_to_string(), slot.size / 1024, usage_str)) + webserver.content_send(string.format("", + title, col_before, slot.label, col_after, slot.size / 1024, usage_str)) + elif slot.is_spiffs() + # spiffs partition + var title = string.format("subtype:%s offset:0x%06X size:0x%06X", slot.subtype_to_string(), slot.start, slot.size) + webserver.content_send(string.format("", title, slot.size / 1024)) + end + end + + var unallocated = self.get_unallocated_k(p) + if unallocated > 0 + var last_slot = p.slots[-1] + # verify that last slot is file-system + var partition_end_k = (last_slot.start + last_slot.size) / 1024 # last kb used for fs + webserver.content_send(string.format("", + partition_end_k * 1024, unallocated * 1024, unallocated)) + end + webserver.content_send("
<sys>:  64 KB
%s%s%s %i KB  (%s)
fs %i KB
<free>:  %i KB
") + + # display if layout is factory + if self.has_factory_layout(p) + webserver.content_send("

This device uses the safeboot layout

") + end + + webserver.content_send("

") + + end + + ####################################################################### + # Display the complete page + ####################################################################### + def page_part_mgr() + import webserver + import string + import partition_core + if !webserver.check_privileged_access() return nil end + var p = partition_core.Partition() # load partition layout + + webserver.content_start("Partition Wizard") #- title of the web page -# + webserver.content_send_style() #- send standard Tasmota styles -# + + if webserver.has_arg("wait") + webserver.content_send("

Migration process will start in 5 seconds
Magic is happening, leave device alone for 3 minutes.

") + webserver.content_button(webserver.BUTTON_MAIN) #- button back to main page -# + else + webserver.content_send("

Warning: actions below can brick your device.

") + self.show_current_partitions(p) + self.show_resize_fs(p) + # show page for migration to factory + self.show_migrate_to_factory(p) + webserver.content_button(webserver.BUTTON_MANAGEMENT) #- button back to management page -# + end + + webserver.content_stop() #- end of web page -# + end + + ####################################################################### + # Web Controller, called by POST to `/part_wiz` + ####################################################################### + def page_part_ctl() + import webserver + if !webserver.check_privileged_access() return nil end + + import string + import partition_core + import persist + + + #- check that the partition is valid -# + var p = partition_core.Partition() + + try + + #---------------------------------------------------------------------# + # Resize FS to max + #---------------------------------------------------------------------# + if webserver.has_arg("max_fs") + var unallocated = self.get_unallocated_k(p) + if unallocated <= 0 raise "value_error", "FS already at max size" end + + self.resize_max_flash_size_k(p) # resize if needed + + # since unallocated succeeded, we know the last slot is FS + var fs_slot = p.slots[-1] + fs_slot.size += unallocated * 1024 + p.save() + p.invalidate_spiffs() # erase SPIFFS or data is corrupt + + #- and force restart -# + webserver.redirect("/?rst=") + + #---------------------------------------------------------------------# + # Resize FS to arbitrary size + #---------------------------------------------------------------------# + elif webserver.has_arg("resize_fs") + if !self.has_factory_layout(p) raise "internal_error", "Device does not avec safeboot layout" end + + var fs = p.slots[-1] + var last_app = p.slots[-2] + if (last_app.get_image_size() <= 0) raise "internal_error", "last `app` partition has no firmware" end + + var max_fs_start_k = self.get_max_fs_start_k(p) + var flash_size_k = self.get_max_flash_size_k(p) + + var fs_max_size_k = flash_size_k - max_fs_start_k + var current_fs_size_k = self.get_cur_fs_size_k(p) + + var fs_target = int(webserver.arg("fs_size")) + if (fs_target < 64) || (fs_target > fs_max_size_k) raise "value_error", string.format("Invalid FS #%d", fs_target) end + + # apply the change + # shrink last OTA App + var delta = (fs_target * 1024) - fs.size + last_app.size -= delta + + # move fs + fs.start -= delta + fs.size += delta + p.save() + p.invalidate_spiffs() + + #- and force restart -# + webserver.redirect("/?rst=") + #---------------------------------------------------------------------# + # Switch OTA partition from one to another + #---------------------------------------------------------------------# + elif webserver.has_arg("factory") + var ota_url = webserver.arg("o1") + var safeboot_url = webserver.arg("o2") + + if safeboot_url != nil && safeboot_url != "" + persist.safeboot_url = safeboot_url + persist.save() + end + + if ota_url != nil && ota_url != "" + tasmota.cmd("OtaUrl "+ota_url) + end + + tasmota.set_timer(5000, /-> self.do_safeboot_partitioning()) + webserver.redirect("/part_wiz?wait=") + + else + raise "value_error", "Unknown command" + end + except .. as e, m + tasmota.log(string.format("BRY: Exception> '%s' - %s", e, m), 2) + #- display error page -# + webserver.content_start("Parameter error") #- title of the web page -# + webserver.content_send_style() #- send standard Tasmota styles -# + + webserver.content_send(string.format("

Exception:
'%s'
%s

", e, m)) + # webserver.content_send("

") + + webserver.content_button(webserver.BUTTON_MANAGEMENT) #- button back to management page -# + webserver.content_stop() #- end of web page -# + end + end + + + + #---------------------------------------------------------------------# + # Apply the repartitioning process + #---------------------------------------------------------------------# + # returns: + # `true`: already completed + # `false`: in progress + # string: error with description of error + def do_safeboot_partitioning() + import webserver + import string + import partition_core + + var p = partition_core.Partition() # load partition layout + if !self.factory_migrate_eligible(p) return true end + + # STEP 1 + var step1_state = self.test_step_1(p) + if type(step1_state) == 'string' return step1_state end + if step1_state == false + import persist + tasmota.log("UPL: Starting step 1", 2) + try + self.do_step_1(p) + except .. as e, m + tasmota.log(string.format("UPL: error (%s) %s", e, m), 2) + return m + end + persist.factory_migrate = true + persist.save() + return false + end + tasmota.log("UPL: Step 1 Done", 2) + + # STEP 2 + var step2_state = self.test_step_2(p) + if type(step2_state) == 'string' return step2_state end + if step2_state == false + tasmota.log("UPL: Starting step 2", 2) + import persist + var safeboot_url = persist.find("safeboot_url") + try + self.do_step_2(p, safeboot_url) + except .. as e, m + tasmota.log(string.format("UPL: error (%s) %s", e, m), 2) + return m + end + end + tasmota.log("UPL: Step 2 Done", 2) + + # STEP 3 + var step3_state = self.test_step_3(p) + if type(step3_state) == 'string' return step3_state end + if step3_state == false + tasmota.log("UPL: Starting step 3", 2) + try + self.do_step_3(p) + except .. as e, m + tasmota.log(string.format("UPL: error (%s) %s", e, m), 2) + return m + end + end + tasmota.log("UPL: Step 3 Done", 2) + + # STEP 4 + # Nothing to do, it's automatic + return false + end + + #- ---------------------------------------------------------------------- -# + # respond to web_add_handler() event to register web listeners + #- ---------------------------------------------------------------------- -# + #- this is called at Tasmota start-up, as soon as Wifi/Eth is up and web server running -# + def web_add_handler() + import webserver + #- we need to register a closure, not just a function, that captures the current instance -# + webserver.on("/part_wiz", / -> self.page_part_mgr(), webserver.HTTP_GET) + webserver.on("/part_wiz", / -> self.page_part_ctl(), webserver.HTTP_POST) + end +end +partition_wizard.Partition_wizard_UI = Partition_wizard_UI + + +#- create and register driver in Tasmota -# +if tasmota + import partition_core + var partition_wizard_ui = partition_wizard.Partition_wizard_UI() + tasmota.add_driver(partition_wizard_ui) + ## can be removed if put in 'autoexec.bat' + partition_wizard_ui.web_add_handler() +end + +return partition_wizard + +#- Example + +import partition + +# read +p = partition.Partition() +print(p) + +-# diff --git a/tasmota/berry/modules/Partition_wizard.tapp b/tasmota/berry/modules/Partition_wizard.tapp new file mode 100644 index 0000000000000000000000000000000000000000..9c9f5cd3036d5e7d9b2a1d028a5e6d9fdf8ea0d0 GIT binary patch literal 31911 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