Extension Manager, replacing loading of Partition Wizard (#23955)

This commit is contained in:
s-hadinger 2025-09-27 17:20:34 +02:00 committed by GitHub
parent 8a5a12b48d
commit f2006566d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1727 additions and 1521 deletions

View File

@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
- Extend state JSON message with functional hostname and ipaddress which could be WiFi or Ethernet
- Berry multiplication between string and int (#23850)
- Support for RX8030 RTC (#23855)
- Extension Manager, replacing loading of Partition Wizard
### Breaking Changed
- Berry `animate` framework is DEPRECATED, will be replace by `animation` framework (#23854)

View File

@ -2,7 +2,6 @@
#
#
var extension_manager = module("extension_manager")
#@ solidify:extension_manager
@ -40,10 +39,10 @@ class Extension_manager
# tapp_name(wd)
#
# Takes a working directory 'wd' and extract the name of the tapp file.
# Ex: '/.extensions/Leds_Panel.tapp#' becomes 'Leds_Panel.tapp'
# Ex: '/.extensions/Leds_Panel.tapp#' becomes 'Leds_Panel'
#
# @param wv: string - the Working Dir of the tapp file like '/.extensions/Leds_Panel.tapp#'
# @return string - the raw name of the tapp file, like 'Leds_Panel.tapp'
# @return string - the raw name of the tapp file, like 'Leds_Panel'
#####################################################################################################
static def tapp_name(wd)
import string
@ -113,6 +112,10 @@ class Extension_manager
# Return a map of installed tap files, by tapp file name
# tapp_name -> path {'Leds_Panel.tapp': '/.extensions/Leds_Panel.tapp'}
#
# Example:
# > extension_manager.list_installed_ext()
# {'Leds_Panel': '/.extensions/Leds_Panel.tapp', 'Partition_Wizard': '/.extensions/Partition_Wizard.tapp_'}
#
# @return map: tapp_namt -> full path (or wd)
static def list_installed_ext()
# Read extensions in file system
@ -130,6 +133,10 @@ class Extension_manager
#
# List all extensions in file-system, whether they are running or not
#
# Example:
# > extension_manager.list_extensions_in_fs()
# {'Leds Panel': '/.extensions/Leds_Panel.tapp', 'Partition Wizard': '/.extensions/Partition_Wizard.tapp_'}
#
# @return sortedmap: with Name of App as key, and following map:
# name, description, version (int), autorun (bool)
static def list_extensions_in_fs()
@ -195,19 +202,97 @@ class Extension_manager
# all good, created successfully
end
#####################################################################################################
# run_stop_ext(tapp_fname, run_stop)
#
# @param tapp_fname : string - name of tapp file to install from repository (ex: "Leds_Panel")
# @param run_stop : bool - `true` to run , `false` to stop
# @return bool - `true` if success
static def run_stop_ext(tapp_fname, run_stop)
# sanitize
tapp_fname = _class.tapp_name(tapp_fname)
# get the path for actual file
var tapp_path = _class.list_installed_ext().find(tapp_fname)
if tapp_path != nil
if run_stop
return tasmota.load(tapp_path)
else
return tasmota.unload_extension(tapp_path)
end
else
return false
end
end
#####################################################################################################
# enable_disable_ext(tapp_fname, run_stop)
#
# @param tapp_fname : string - name of tapp file to enable or disable (ex: "Leds_Panel")
# @param enable : bool - `true` to enable , `false` to disable
# @return bool - `true` if success
static def enable_disable_ext(tapp_fname, enable)
import string
# sanitize
tapp_fname = _class.tapp_name(tapp_fname)
# get the path for actual file
var tapp_path = _class.list_installed_ext().find(tapp_fname)
if tapp_path != nil
var new_name
if enable && string.endswith(tapp_path, ".tapp_")
new_name = tapp_path[0..-2] # remove trailing '_'
elif !enable && string.endswith(tapp_path, ".tapp")
new_name = tapp_path + '_' # add trailing '_'
end
if new_name
import path
var success = path.rename(tapp_path, new_name)
if (success) # update any running extension with its new name
if (tasmota._ext != nil) && tasmota._ext.contains(tapp_path)
tasmota._ext[new_name] = tasmota._ext[tapp_path]
tasmota._ext.remove(tapp_path)
end
end
return success
end
end
return false
end
#####################################################################################################
# delete_ext(tapp_fname)
#
# @param tapp_fname : string - name of tapp file to delete from file system (ex: "Leds_Panel")
# @return bool - `true` if success
static def delete_ext(tapp_fname)
# sanitize
tapp_fname = _class.tapp_name(tapp_fname)
# get the path for actual file
var tapp_path = _class.list_installed_ext().find(tapp_fname)
if tapp_path != nil
import path
_class.run_stop_ext(tapp_fname, false) # stop the extension if it's running
var success = path.remove(tapp_path)
return success
else
return false
end
end
#####################################################################################################
# install_from_store(tapp_fname)
#
# @param tapp_fname : string - name of tapp file to install from repository
# @return bool : 'true' if success
def install_from_store(tapp_fname)
import string
import path
# sanitize
tapp_fname = self.tapp_name(tapp_fname)
# add '.tapp' extension if it is not present
if !string.endswith(tapp_fname, ".tapp")
tapp_fname += '.tapp'
end
tapp_fname = self.tapp_name(tapp_fname) + ".tapp"
# full url
var ext_url = f"{self.EXT_REPO}{self.EXT_REPO_FOLDER}{tapp_fname}"
log(f"EXT: installing from '{ext_url}'", 3)
@ -222,19 +307,20 @@ class Extension_manager
var r = cl.GET()
if r != 200
log(f"EXT: return_code={r}", 2)
return
return false
end
var ret = cl.write_file(local_file)
cl.close()
# test if file exists and tell its size
if ret > 0 && path.exists(local_file)
log(f"EXT: successfully installed '{local_file}' {ret} bytes", 3)
return true
else
raise "io_error", f"could not download into '{local_file}' ret={ret}"
end
except .. as e, m
tasmota.log(format("EXT: exception '%s' - '%s'", e, m), 2)
return nil
log(format("EXT: exception '%s' - '%s'", e, m), 2)
return false
end
end
@ -278,7 +364,6 @@ class Extension_manager
webserver.content_send("<script>"
"function loadext() {"
"eb('store').disabled=true;"
# "eb('store').innerHTML = '[ <span style=\"color:var(--c_btnsv);\">Loading from Store...</span> ]';"
"x=new XMLHttpRequest();"
"x.timeout=4000;"
"x.onreadystatechange = () => {"
@ -286,8 +371,6 @@ class Extension_manager
"if(x.status==200){"
"eb('inet').style.display='none';"
"eb('store').outerHTML=x.responseText;"
# "}else{"
# "eb('store').innerHTML='<b>[ <span style=\"color:var(--c_btnrsthvr);\">Error loading manifest.</span> ]</b>';"
"}"
"}"
"};"
@ -327,7 +410,8 @@ class Extension_manager
"</script>"
)
webserver.content_send("<fieldset style='padding:0 5px;'>"
webserver.content_send(
"<fieldset style='padding:0 5px;'>"
"<style>"
# Fix for small text - the key is width: min-content on parent */
".ext-item{width:min-content;min-width:100%;}"
@ -343,7 +427,6 @@ class Extension_manager
"@keyframes pulse{0%{opacity:1;}50%{opacity:0.5;}100%{opacity:1;}}"
# for store
# /* Extension Store specific styles */
".store-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;}"
".store-stats{font-size:0.9em;color:var(--c_in);}"
@ -379,24 +462,23 @@ class Extension_manager
while ext_nb < size(installed_ext)
if (ext_nb > 0) webserver.content_send("<hr style='margin:2px 0 0 0;'>") end
var ext_path = installed_ext.get_by_index(ext_nb) # ex: '/.extensions/Partition_Wizard.tapp'
var ext_path_html = webserver.html_escape(ext_path)
var tapp_name = self.tapp_name(ext_path)
var tapp_name_html = webserver.html_escape(tapp_name)
var details = tasmota.read_extension_manifest(ext_path)
# log(f"EXT: {details=}")
var running = tasmota._ext ? tasmota._ext.contains(ext_path) : false
var running_indicator = running ? " <span class='running-indicator' title='Running'></span>" : ""
var autorun = details.find("autorun", false)
var back_green = "style='background:var(--c_btnsvhvr);'"
var dark_blue = "style='background:var(--c_btnoff);'"
webserver.content_send("<div class='ext-item'>")
webserver.content_send(f"<span title='path: {ext_path_html}'><b>{webserver.html_escape(details['name'])}</b>{running_indicator}</span><br>")
webserver.content_send(f"<span title='path: {tapp_name_html}'><b>{webserver.html_escape(details['name'])}</b>{running_indicator}</span><br>")
webserver.content_send(f"<small>{webserver.html_escape(details['description'])}</small>")
webserver.content_send("<div class='ext-controls' style='padding-top:0px;padding-bottom:0px;'>")
webserver.content_send("<form action='/ext' method='post' class='ext-controls'>")
webserver.content_send(f"<button type='submit' class='btn-small' {running ? back_green :: dark_blue} name='{running ? 's' :: 'r'}{ext_path_html}'>{running ? 'Running' :: 'Stopped'}</button>")
webserver.content_send(f"<button type='submit' class='btn-small' {autorun ? '' :: dark_blue} name='{autorun ? 'a' :: 'A'}{ext_path_html}'>Auto-run: {autorun ? 'ON' :: 'OFF'}</button>")
webserver.content_send(f"<button type='submit' class='btn-small bred' name='d{ext_path_html}' onclick='return confirm(\"Confirm deletion of {webserver.html_escape(ext_path)}\")'>Uninstall</button>")
# webserver.content_send(f"<button type='submit' class='btn-small' style='background-color:var(--c_btnoff);border-color:var(--c_btnrst);border-width:3px;border-style:solid;' name='d{ext_path_html}' onclick='return confirm(\"Confirm deletion of {webserver.html_escape(ext_path)}\")'>Uninstall</button>")
webserver.content_send(f"<button type='submit' class='btn-small' {running ? back_green :: dark_blue} name='{running ? 's' :: 'r'}{tapp_name_html}'>{running ? 'Running' :: 'Stopped'}</button>")
webserver.content_send(f"<button type='submit' class='btn-small' {autorun ? '' :: dark_blue} name='{autorun ? 'a' :: 'A'}{tapp_name_html}'>Auto-run: {autorun ? 'ON' :: 'OFF'}</button>")
webserver.content_send(f"<button type='submit' class='btn-small bred' name='d{tapp_name_html}' onclick='return confirm(\"Confirm deletion of {tapp_name_html}.tapp\")'>Uninstall</button>")
webserver.content_send("</form></div></div>")
ext_nb += 1
@ -412,7 +494,6 @@ class Extension_manager
"<hr style='margin-bottom:0;'>"
"<span id='inet' style='font-size:small;font-weight:normal;''>&nbsp;(This feature requires an internet connection)</span>"
"</h3></div>")
# "<p><small>&nbsp;(This feature requires an internet connection)</small></p>")
webserver.content_send("<b id='store'>[ <span style='color:var(--c_btnsv);'>Loading from Store...</span> ]</b>")
@ -483,10 +564,8 @@ class Extension_manager
installed_tapp_name = self.tapp_name(entry['file'])
var installed_tapp_name_web = webserver.html_escape(installed_tapp_name)
installed = installed_ext.contains(installed_tapp_name)
var installed_path_web
if installed
var installed_path = installed_ext[installed_tapp_name]
installed_path_web = webserver.html_escape(installed_path)
var details = tasmota.read_extension_manifest(installed_path)
installed_version = int(details.find('version', 0))
end
@ -524,14 +603,14 @@ class Extension_manager
"<div style='width:30%'></div>")
if installed
if upgrade
webserver.content_send( f"<button type='submit' class='btn-action' name='u{installed_path_web}' onclick='return confirm(\"Confirm upgrade of {installed_path_web}\")'>Upgrade</button>")
webserver.content_send( f"<button type='submit' class='btn-action' name='u{installed_tapp_name_web}' onclick='return confirm(\"Confirm upgrade of {installed_tapp_name_web}\")'>Upgrade</button>")
else
webserver.content_send( "<button type='submit' class='btn-action' style='visibility:hidden;'></button>")
end
webserver.content_send( f"<button type='submit' class='btn-action bred' name='d{installed_path_web}' onclick='return confirm(\"Confirm deletion of {installed_path_web}\")'>Uninstall</button>")
webserver.content_send( f"<button type='submit' class='btn-action bred' name='d{installed_tapp_name_web}' onclick='return confirm(\"Confirm deletion of {installed_tapp_name_web}\")'>Uninstall</button>")
else
webserver.content_send( f"<button type='submit' class='btn-action' style='visibility:hidden;'></button>"
"<button type='submit' class='btn-action bgrn' name='i{installed_tapp_name_web}' onclick='return confirm(\"Confirm installation of {app_name_web}\")'>Install</button>")
webserver.content_send( f"<button type='submit' class='btn-action' name='i{installed_tapp_name_web}' onclick='return confirm(\"Confirm installation of {app_name_web}\")'>Install</button>"
"<button type='submit' class='btn-action bgrn' name='I{installed_tapp_name_web}' onclick='return confirm(\"Confirm installation of {app_name_web}\")'>Install+Run</button>")
end
webserver.content_send( "</form>"
"</div>"
@ -565,14 +644,14 @@ class Extension_manager
cl.begin(url)
var r = cl.GET()
if r != 200
tasmota.log(f"EXT: error fetching manifest {r}", 2)
log(f"EXT: error fetching manifest {r}", 2)
raise "webclient_error", f"Error fetching manifest code={r}"
end
var s = cl.get_string()
cl.close()
return s
except .. as e, m
tasmota.log(format("EXT: exception '%s' - '%s'", e, m), 2)
log(format("EXT: exception '%s' - '%s'", e, m), 2)
raise e, m
end
end
@ -591,73 +670,46 @@ class Extension_manager
try
# log(f">>> {webserver.arg_name(0)=} {webserver.arg(0)=} {webserver.arg_size()=}")
# var redirect_to_store = false # add suffix to redirect to store
var btn_name = webserver.arg_name(0)
var action = btn_name[0] # first character
var action_path = btn_name[1..] # remove first character
if (action == "r") # button "Run"
if (action_path != "")
# log(f"EXT: run '{action_path}'")
tasmota.load(action_path)
end
elif (action == "s") # button "Stop"
# log(f"EXT: stop '{action_path}'")
tasmota.unload_extension(action_path)
if (action == "r") || (action == "s") # button "Run" or "Stop"
self.run_stop_ext(action_path, action == "r")
elif (action == "a") || (action == "A") # button "Autorun", "A" enable, "a" disable
var new_name
if (action == "a") && string.endswith(action_path, ".tapp") # Autorun is enabled, disable it
new_name = action_path[0..-5] + "tapp_"
elif (action == "A") && string.endswith(action_path, ".tapp_")
new_name = action_path[0..-6] + "tapp"
end
if new_name
var success = path.rename(action_path, new_name)
# log(f"EXT: rename '{action_path}' to '{new_name} {success=}", 3)
if (success) # update any running extension with its new name
if tasmota._ext.contains(action_path)
tasmota._ext[new_name] = tasmota._ext[action_path]
tasmota._ext.remove(action_path)
self.enable_disable_ext(action_path, action == "A")
elif (action == 'd') # button "Delete"
self.delete_ext(action_path)
# Now try the store commands
elif (action == 'u') # Upgrade ext
# first stop the app if it's running
self.run_stop_ext(action_path, false) # stop the extension
var success = self.install_from_store(self.tapp_name(action_path))
elif (action == 'i') || (action == 'I') # Install ext ('I' for run as well)
var success = self.install_from_store(self.tapp_name(action_path))
if success
if (action == 'I') # run
self.run_stop_ext(action_path, true)
else # disable
self.enable_disable_ext(action_path, false)
end
end
else
log(f"EXT: wrong action '{btn_name}'", 3)
end
elif (action == 'd') # button "Delete"
if (action_path != "")
# first stop if it was running
tasmota.unload_extension(action_path)
# then delete file
var success = path.remove(action_path)
# log(f"EXT: delete '{action_path}' {success=}", 3)
end
# Now try the store commands
elif (action == 'u') # Upgrade ext
# log(f"EXT: upgrade '{action_path}'", 3)
# first stop the app if it's running
tasmota.unload_extension(action_path)
self.install_from_store(self.tapp_name(action_path))
# redirect_to_store = true
elif (action == 'i') # Install ext
# log(f"EXT: install '{action_path}'", 3)
self.install_from_store(self.tapp_name(action_path))
# redirect_to_store = true
end
# var redirect_suffix = redirect_to_store ? "store=" : ""
# webserver.redirect(f"/ext?{redirect_suffix}")
webserver.redirect(f"/ext")
except .. as e, m
log(f"CFG: Exception> '{e}' - {m}", 2)
log(f"EXT: Exception> '{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(f"<p style='width:340px;'><b>Exception:</b><br>'{webserver.html_escape(e)}'<br>{webserver.html_escape(m)}</p>")
webserver.content_button(webserver.BUTTON_CONFIGURATION) #- button back to management page -#
webserver.content_button(webserver.BUTTON_MANAGEMENT) #- button back to management page -#
webserver.content_stop() #- end of web page -#
end
end

View File

@ -948,7 +948,7 @@ class Tasmota
end
def unload_extension(name_or_instance)
if (self._ext == nil) return end
if (self._ext == nil) return false end
var d = name_or_instance # d = driver
if type(name_or_instance) == 'string'
@ -961,11 +961,14 @@ class Tasmota
d.unload()
end
self.remove_driver(d)
end
# force gc of instance
name_or_instance = nil
d = nil
tasmota.gc()
return true
else
return false
end
end
# cmd high-level function

View File

@ -1893,45 +1893,51 @@ be_local_closure(class_Tasmota_unload_extension, /* name */
&be_ktab_class_Tasmota, /* shared constants */
&be_const_str_unload_extension,
&be_const_str_solidified,
( &(const binstruction[38]) { /* code */
( &(const binstruction[44]) { /* code */
0x8808014B, // 0000 GETMBR R2 R0 K75
0x4C0C0000, // 0001 LDNIL R3
0x1C080403, // 0002 EQ R2 R2 R3
0x780A0000, // 0003 JMPF R2 #0005
0x80000400, // 0004 RET 0
0x5C080200, // 0005 MOVE R2 R1
0x600C0004, // 0006 GETGBL R3 G4
0x5C100200, // 0007 MOVE R4 R1
0x7C0C0200, // 0008 CALL R3 1
0x1C0C0701, // 0009 EQ R3 R3 K1
0x780E0004, // 000A JMPF R3 #0010
0x880C014B, // 000B GETMBR R3 R0 K75
0x8C0C070C, // 000C GETMET R3 R3 K12
0x5C140200, // 000D MOVE R5 R1
0x7C0C0400, // 000E CALL R3 2
0x5C080600, // 000F MOVE R2 R3
0x600C0004, // 0010 GETGBL R3 G4
0x5C100400, // 0011 MOVE R4 R2
0x7C0C0200, // 0012 CALL R3 1
0x1C0C0757, // 0013 EQ R3 R3 K87
0x780E000A, // 0014 JMPF R3 #0020
0xA40EE000, // 0015 IMPORT R3 K112
0x8C10075A, // 0016 GETMET R4 R3 K90
0x5C180400, // 0017 MOVE R6 R2
0x581C007A, // 0018 LDCONST R7 K122
0x7C100600, // 0019 CALL R4 3
0x78120001, // 001A JMPF R4 #001D
0x8C10057A, // 001B GETMET R4 R2 K122
0x7C100200, // 001C CALL R4 1
0x8C10017B, // 001D GETMET R4 R0 K123
0x5C180400, // 001E MOVE R6 R2
0x7C100400, // 001F CALL R4 2
0x4C040000, // 0020 LDNIL R1
0x4C080000, // 0021 LDNIL R2
0xB80E0400, // 0022 GETNGBL R3 K2
0x8C0C077C, // 0023 GETMET R3 R3 K124
0x7C0C0200, // 0024 CALL R3 1
0x80000000, // 0025 RET 0
0x780A0001, // 0003 JMPF R2 #0006
0x50080000, // 0004 LDBOOL R2 0 0
0x80040400, // 0005 RET 1 R2
0x5C080200, // 0006 MOVE R2 R1
0x600C0004, // 0007 GETGBL R3 G4
0x5C100200, // 0008 MOVE R4 R1
0x7C0C0200, // 0009 CALL R3 1
0x1C0C0701, // 000A EQ R3 R3 K1
0x780E0004, // 000B JMPF R3 #0011
0x880C014B, // 000C GETMBR R3 R0 K75
0x8C0C070C, // 000D GETMET R3 R3 K12
0x5C140200, // 000E MOVE R5 R1
0x7C0C0400, // 000F CALL R3 2
0x5C080600, // 0010 MOVE R2 R3
0x600C0004, // 0011 GETGBL R3 G4
0x5C100400, // 0012 MOVE R4 R2
0x7C0C0200, // 0013 CALL R3 1
0x1C0C0757, // 0014 EQ R3 R3 K87
0x780E0012, // 0015 JMPF R3 #0029
0xA40EE000, // 0016 IMPORT R3 K112
0x8C10075A, // 0017 GETMET R4 R3 K90
0x5C180400, // 0018 MOVE R6 R2
0x581C007A, // 0019 LDCONST R7 K122
0x7C100600, // 001A CALL R4 3
0x78120001, // 001B JMPF R4 #001E
0x8C10057A, // 001C GETMET R4 R2 K122
0x7C100200, // 001D CALL R4 1
0x8C10017B, // 001E GETMET R4 R0 K123
0x5C180400, // 001F MOVE R6 R2
0x7C100400, // 0020 CALL R4 2
0x4C040000, // 0021 LDNIL R1
0x4C080000, // 0022 LDNIL R2
0xB8120400, // 0023 GETNGBL R4 K2
0x8C10097C, // 0024 GETMET R4 R4 K124
0x7C100200, // 0025 CALL R4 1
0x50100200, // 0026 LDBOOL R4 1 0
0x80040800, // 0027 RET 1 R4
0x70020001, // 0028 JMP #002B
0x500C0000, // 0029 LDBOOL R3 0 0
0x80040600, // 002A RET 1 R3
0x80000000, // 002B RET 0
})
)
);

View File

@ -49,7 +49,7 @@ default_envs =
[tasmota]
; *** Global build / unbuild compile time flags for ALL Tasmota / Tasmota32 [env]
;build_unflags =
build_flags = -DUSE_BERRY_PARTITION_WIZARD
;build_flags =
[env]
;build_unflags = ${common.build_unflags}

View File

@ -1221,7 +1221,7 @@
//#define USE_WEBCAM // Add support for webcam
#define USE_AUTOCONF // Enable Esp32(x) autoconf feature, requires USE_BERRY and USE_WEBCLIENT_HTTPS (12KB Flash)
// #define USE_EXTENSION_MANAGER // Enable Esp32(x) extensions manager, requires USE_BERRY and USE_WEBCLIENT_HTTPS (9KB Flash)
#define USE_EXTENSION_MANAGER // Enable Esp32(x) extensions manager, requires USE_BERRY and USE_WEBCLIENT_HTTPS (11KB Flash)
#define USE_BERRY // Enable Berry scripting language
#define USE_BERRY_PYTHON_COMPAT // Enable by default `import python_compat`
#define USE_BERRY_TIMEOUT 4000 // Timeout in ms, will raise an exception if running time exceeds this timeout
@ -1235,9 +1235,9 @@
// Note that only two ciphers are enabled: ECDHE_RSA_WITH_AES_128_GCM_SHA256, ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
#define USE_BERRY_WEBCLIENT_USERAGENT "TasmotaClient" // default user-agent used, can be changed with `wc.set_useragent()`
#define USE_BERRY_WEBCLIENT_TIMEOUT 2000 // Default timeout in milliseconds
#define USE_BERRY_LEDS_PANEL // Add button to dynamically load the Leds Panel from a bec file online
// #define USE_BERRY_LEDS_PANEL // Add button to dynamically load the Leds Panel from a bec file online
#define USE_BERRY_LEDS_PANEL_URL "http://ota.tasmota.com/tapp/leds_panel.bec"
#define USE_BERRY_LVGL_PANEL // Add button to dynamically load the LVGL Panel from a bec file online
// #define USE_BERRY_LVGL_PANEL // Add button to dynamically load the LVGL Panel from a bec file online
#define USE_BERRY_LVGL_PANEL_URL "http://ota.tasmota.com/tapp/lvgl_panel.bec"
//#define USE_BERRY_PARTITION_WIZARD // Add a button to dynamically load the Partion Wizard from a bec file online (+1.3KB Flash)
#define USE_BERRY_PARTITION_WIZARD_URL "http://ota.tasmota.com/tapp/partition_wizard.bec"