From 54a152f44059d68dcb2957b7a20054dbf5af2ff3 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:06:28 +0100 Subject: [PATCH] Platform 2025.12.31 --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- pio-tools/custom_target.py | 87 +++++++++++++++++++++++--------- pio-tools/post_esp32.py | 57 +++++++++++++++++++-- platformio.ini | 6 +-- platformio_tasmota32.ini | 2 +- 5 files changed, 118 insertions(+), 36 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e38c916c1..8960abec6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ - [ ] Only relevant files were touched - [ ] Only one feature/fix was added per PR and the code change compiles without warnings - [ ] The code change is tested and works with Tasmota core ESP8266 V.2.7.8 - - [ ] The code change is tested and works with Tasmota core ESP32 V.3.1.7 + - [ ] The code change is tested and works with Tasmota core ESP32 V.3.1.8 - [ ] I accept the [CLA](https://github.com/arendst/Tasmota/blob/development/CONTRIBUTING.md#contributor-license-agreement-cla). _NOTE: The code change must pass CI tests. **Your PR cannot be merged unless tests pass**_ diff --git a/pio-tools/custom_target.py b/pio-tools/custom_target.py index ce039644c..f9e050418 100644 --- a/pio-tools/custom_target.py +++ b/pio-tools/custom_target.py @@ -1,18 +1,17 @@ # Written by Maximilian Gerhardt # 29th December 2020 # and Christian Baars, Johann Obermeier -# 2023 / 2024 +# 2023 - 2025 # License: Apache # Expanded from functionality provided by PlatformIO's espressif32 and espressif8266 platforms, credited below. # This script provides functions to download the filesystem (LittleFS) from a running ESP32 / ESP8266 -# over the serial bootloader using esptool.py, and mklittlefs for extracting. +# over the serial bootloader using esptool.py, and littlefs-python for extracting. # run by either using the VSCode task "Custom" -> "Download Filesystem" # or by doing 'pio run -t downloadfs' (with optional '-e ') from the commandline. # output will be saved, by default, in the "unpacked_fs" of the project. # this folder can be changed by writing 'custom_unpack_dir = some_other_dir' in the corresponding platformio.ini # environment. import re -import sys from os.path import isfile, join from enum import Enum import os @@ -20,7 +19,9 @@ import tasmotapiolib import subprocess import shutil import json +from pathlib import Path from colorama import Fore, Back, Style +from littlefs import LittleFS from platformio.compat import IS_WINDOWS from platformio.project.config import ProjectConfig @@ -42,19 +43,12 @@ class FSInfo: self.block_size = block_size def __repr__(self): return f"FS type {self.fs_type} Start {hex(self.start)} Len {self.length} Page size {self.page_size} Block size {self.block_size}" - # extract command supposed to be implemented by subclasses - def get_extract_cmd(self, input_file, output_dir): - raise NotImplementedError() class FS_Info(FSInfo): def __init__(self, start, length, page_size, block_size): - self.tool = env["MKFSTOOL"] - self.tool = os.path.join(ProjectConfig.get_instance().get("platformio", "packages_dir"), "tool-mklittlefs", self.tool) super().__init__(FSType.LITTLEFS, start, length, page_size, block_size) def __repr__(self): return f"{self.fs_type} Start {hex(self.start)} Len {hex(self.length)} Page size {hex(self.page_size)} Block size {hex(self.block_size)}" - def get_extract_cmd(self, input_file, output_dir): - return f'"{self.tool}" -b {self.block_size} -s {self.length} -p {self.page_size} --unpack "{output_dir}" "{input_file}"' def _parse_size(value): if isinstance(value, int): @@ -150,15 +144,11 @@ switch_off_ldf() ## Script interface functions def parse_partition_table(content): entries = [e for e in content.split(b'\xaaP') if len(e) > 0] - #print("Partition data:") for entry in entries: type = entry[1] if type in [0x82,0x83]: # SPIFFS or LITTLEFS - offset = int.from_bytes(entry[2:5], byteorder='little', signed=False) - size = int.from_bytes(entry[6:9], byteorder='little', signed=False) - #print("type:",hex(type)) - #print("address:",hex(offset)) - #print("size:",hex(size)) + offset = int.from_bytes(entry[2:6], byteorder='little', signed=False) + size = int.from_bytes(entry[6:10], byteorder='little', signed=False) env["FS_START"] = offset env["FS_SIZE"] = size env["FS_PAGE"] = int("0x100", 16) @@ -266,20 +256,67 @@ def unpack_fs(fs_info: FSInfo, downloaded_file: str): if not os.path.exists(unpack_dir): os.makedirs(unpack_dir) - cmd = fs_info.get_extract_cmd(downloaded_file, unpack_dir) - print("Unpack files from filesystem image") + print() try: - returncode = subprocess.call(cmd, shell=True) + # Read the downloaded filesystem image + with open(downloaded_file, 'rb') as f: + fs_data = f.read() + + # Calculate block count + block_count = fs_info.length // fs_info.block_size + + # Create LittleFS instance and mount the image + fs = LittleFS( + block_size=fs_info.block_size, + block_count=block_count, + mount=False + ) + fs.context.buffer = bytearray(fs_data) + fs.mount() + + # Extract all files + unpack_path = Path(unpack_dir) + for root, dirs, files in fs.walk("/"): + if not root.endswith("/"): + root += "/" + # Create directories + for dir_name in dirs: + src_path = root + dir_name + dst_path = unpack_path / src_path[1:] # Remove leading '/' + dst_path.mkdir(parents=True, exist_ok=True) + # Extract files + for file_name in files: + src_path = root + file_name + dst_path = unpack_path / src_path[1:] # Remove leading '/' + dst_path.parent.mkdir(parents=True, exist_ok=True) + with fs.open(src_path, "rb") as src: + dst_path.write_bytes(src.read()) + + fs.unmount() return (True, unpack_dir) - except subprocess.CalledProcessError as exc: - print("Unpacking filesystem failed with " + str(exc)) + except Exception as exc: + print("Unpacking filesystem with littlefs-python failed with " + str(exc)) return (False, "") def display_fs(extracted_dir): - # extract command already nicely lists all extracted files. - # no need to display that ourselves. just display a summary - file_count = sum([len(files) for r, d, files in os.walk(extracted_dir)]) - print("Extracted " + str(file_count) + " file(s) from filesystem.") + # List all extracted files + file_count = 0 + print(Fore.GREEN + "Extracted files from filesystem image:") + print() + for root, dirs, files in os.walk(extracted_dir): + # Display directories + for dir_name in dirs: + dir_path = os.path.join(root, dir_name) + rel_path = os.path.relpath(dir_path, extracted_dir) + print(f" [DIR] {rel_path}/") + # Display files + for file_name in files: + file_path = os.path.join(root, file_name) + rel_path = os.path.relpath(file_path, extracted_dir) + file_size = os.path.getsize(file_path) + print(f" [FILE] {rel_path} ({file_size} bytes)") + file_count += 1 + print(f"\nExtracted {file_count} file(s) from filesystem.") def command_download_fs(*args, **kwargs): info = get_fs_type_start_and_length() diff --git a/pio-tools/post_esp32.py b/pio-tools/post_esp32.py index 3f96cfaa2..73f3511b2 100644 --- a/pio-tools/post_esp32.py +++ b/pio-tools/post_esp32.py @@ -24,10 +24,12 @@ from genericpath import exists import os from os.path import join, getsize import csv +from littlefs import LittleFS import requests import shutil import subprocess import codecs +from pathlib import Path from colorama import Fore from SCons.Script import COMMAND_LINE_TARGETS @@ -134,7 +136,7 @@ def esp32_build_filesystem(fs_size): os.makedirs(filesystem_dir) if num_entries > 1: print() - print(Fore.GREEN + "Will create filesystem with the following files:") + print(Fore.GREEN + "Will create filesystem with the following file(s):") print() for file in files: if "no_files" in file: @@ -146,21 +148,66 @@ def esp32_build_filesystem(fs_size): if len(file.split(" ")) > 1: target = os.path.normpath(join(filesystem_dir, file.split(" ")[1])) print("Renaming",(file.split(os.path.sep)[-1]).split(" ")[0],"to",file.split(" ")[1]) + else: + print(file.split(os.path.sep)[-1]) open(target, "wb").write(response.content) else: print(Fore.RED + "Failed to download: ",file) continue if os.path.isdir(file): + print(f"{file}/ (directory)") shutil.copytree(file, filesystem_dir, dirs_exist_ok=True) else: + print(file) shutil.copy(file, filesystem_dir) if not os.listdir(filesystem_dir): #print("No files added -> will NOT create littlefs.bin and NOT overwrite fs partition!") return False - tool = env.subst(env["MKFSTOOL"]) - cmd = (tool,"-c",filesystem_dir,"-s",fs_size,join(env.subst("$BUILD_DIR"),"littlefs.bin")) - returncode = subprocess.call(cmd, shell=False) - # print(returncode) + + # Use littlefs-python + output_file = join(env.subst("$BUILD_DIR"), "littlefs.bin") + + # Parse fs_size (can be hex string like "0x2f0000") + if isinstance(fs_size, str): + if fs_size.startswith("0x"): + fs_size_bytes = int(fs_size, 16) + else: + fs_size_bytes = int(fs_size) + else: + fs_size_bytes = int(fs_size) + + # LittleFS parameters for ESP32 + block_size = 4096 + block_count = fs_size_bytes // block_size + + # Create LittleFS instance with disk version 2.0 for Tasmota + fs = LittleFS( + block_size=block_size, + block_count=block_count, + disk_version=0x00020000, + mount=True + ) + + # Add all files from filesystem_dir + source_path = Path(filesystem_dir) + for item in source_path.rglob("*"): + rel_path = item.relative_to(source_path) + if item.is_dir(): + fs.makedirs(rel_path.as_posix(), exist_ok=True) + else: + # Ensure parent directories exist + if rel_path.parent != Path("."): + fs.makedirs(rel_path.parent.as_posix(), exist_ok=True) + # Copy file + with fs.open(rel_path.as_posix(), "wb") as dest: + dest.write(item.read_bytes()) + + # Write filesystem image + with open(output_file, "wb") as f: + f.write(fs.context.buffer) + + print() + print(Fore.GREEN + f"LittleFS image created: {output_file}") return True def esp32_fetch_safeboot_bin(tasmota_platform): diff --git a/platformio.ini b/platformio.ini index cb741869c..b0c1c95d9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -31,6 +31,7 @@ platform_packages = ${core.platform_packages} framework = arduino board = esp8266_1M board_build.filesystem = littlefs +board_build.littlefs_version = 2.0 board_build.variants_dir = variants/tasmota custom_unpack_dir = unpacked_littlefs build_unflags = ${core.build_unflags} @@ -137,13 +138,10 @@ lib_ignore = ESP8266Audio [core] ; *** Esp8266 Tasmota modified Arduino core based on core 2.7.4. Added Backport for PWM selection -platform = https://github.com/tasmota/platform-espressif8266/releases/download/2025.10.00/platform-espressif8266.zip +platform = https://github.com/tasmota/platform-espressif8266/releases/download/2025.12.00/platform-espressif8266.zip platform_packages = build_unflags = ${esp_defaults.build_unflags} build_flags = ${esp82xx_defaults.build_flags} ; *** Use ONE of the two PWM variants. Tasmota default is Locked PWM ;-DWAVEFORM_LOCKED_PHASE -DWAVEFORM_LOCKED_PWM - - - diff --git a/platformio_tasmota32.ini b/platformio_tasmota32.ini index 009cc65fc..5e30d1f69 100644 --- a/platformio_tasmota32.ini +++ b/platformio_tasmota32.ini @@ -97,7 +97,7 @@ custom_component_remove = espressif/cmake_utilities [core32] -platform = https://github.com/tasmota/platform-espressif32/releases/download/2025.12.30/platform-espressif32.zip +platform = https://github.com/tasmota/platform-espressif32/releases/download/2025.12.31/platform-espressif32.zip platform_packages = build_unflags = ${esp32_defaults.build_unflags} build_flags = ${esp32_defaults.build_flags}