# ESP32 SvelteKit -- # # A simple, secure and extensible framework for IoT projects for ESP32 platforms # with responsive Sveltekit front-end built with TailwindCSS and DaisyUI. # https://github.com/theelims/ESP32-sveltekit # # Copyright (C) 2018 - 2023 rjwats # Copyright (C) 2023 theelims # Copyright (C) 2023 Maxtrium B.V. [ code available under dual license ] # Copyright (C) 2024 runeharlyk # # All Rights Reserved. This software may be modified and distributed under # the terms of the LGPL v3 license. See the LICENSE file for details. from pathlib import Path from shutil import copytree, rmtree, copyfileobj from os.path import exists, getmtime import os import gzip import mimetypes import glob from datetime import datetime Import("env") project_dir = env["PROJECT_DIR"] buildFlags = env.ParseFlags(env["BUILD_FLAGS"]) interface_dir = project_dir + "/app" output_file = project_dir + "/esp32/include/WWWData.h" source_www_dir = interface_dir + "/src" build_dir = interface_dir + "/build" filesystem_dir = project_dir + "/data/www" def get_files_to_exclude(): files_to_exclude = [] if (flag_exists("SPOTMICRO_ESP32") or flag_exists("SPOTMICRO_ESP32_MINI")) and not flag_exists("SPOTMICRO_YERTLE"): print("Excluding Yertle files for SPOTMICRO_ESP32 build") files_to_exclude.extend(["yertle.URDF", "URDF.zip", "URDF/"]) elif flag_exists("SPOTMICRO_YERTLE") and not flag_exists("SPOTMICRO_ESP32") and not flag_exists("SPOTMICRO_ESP32_MINI"): print("Excluding Spot Micro files for SPOTMICRO_YERTLE build") files_to_exclude.extend(["spot_micro.urdf.xacro", "stl.zip", "stl/"]) else: print("No specific variant flag set, including all files") return files_to_exclude def find_latest_timestamp_for_app(): return max((getmtime(f) for f in glob.glob(f"{source_www_dir}/**/*", recursive=True))) def should_regenerate_output_file(): if not flag_exists("EMBED_WWW") or not exists(output_file): return True last_source_change = find_latest_timestamp_for_app() last_build = getmtime(output_file) print( f"Newest file: {datetime.fromtimestamp(last_source_change)}, output file: {datetime.fromtimestamp(last_build)}" ) return last_build < last_source_change def gzip_file(file): with open(file, "rb") as f_in: with gzip.open(file + ".gz", "wb") as f_out: copyfileobj(f_in, f_out) os.remove(file) def flag_exists(flag): for define in buildFlags.get("CPPDEFINES"): if define == flag or (isinstance(define, list) and define[0] == flag): return True return False def get_package_manager(): if exists(os.path.join(interface_dir, "package-lock.json")): return "npm" if exists(os.path.join(interface_dir, "yarn.lock")): return "yarn" if exists(os.path.join(interface_dir, "pnpm-lock.yaml")): return "pnpm" def build_webapp(): if package_manager := get_package_manager(): print(f"Building interface with {package_manager}") os.chdir(interface_dir) env.Execute(f"{package_manager} install") env.Execute(f"{package_manager} run build:embedded") os.chdir("..") else: raise Exception("No lock-file found. Please install dependencies for interface (eg. npm install)") def embed_webapp(): if flag_exists("EMBED_WWW"): print("Converting interface to PROGMEM") build_progmem() return add_app_to_filesystem() def build_progmem(): mimetypes.init() with open(output_file, "w") as progmem: progmem.write("#include \n") progmem.write("#include \n") assetMap = {} files_to_exclude = get_files_to_exclude() for idx, path in enumerate(Path(build_dir).rglob("*.*")): asset_path = path.relative_to(build_dir).as_posix() should_exclude = False for exclude_pattern in files_to_exclude: if exclude_pattern.endswith("/"): if asset_path.startswith(exclude_pattern): should_exclude = True print(f"Skipping {asset_path}") break elif asset_path == exclude_pattern: should_exclude = True print(f"Skipping {asset_path}") break if should_exclude: continue asset_mime = mimetypes.guess_type(asset_path)[0] or "application/octet-stream" print(f"Converting {asset_path}") asset_var = f"ESP_SVELTEKIT_DATA_{idx}" progmem.write(f"// {asset_path}\n") progmem.write(f"const uint8_t {asset_var}[] PROGMEM = {{\n\t") file_data = gzip.compress(path.read_bytes()) for i, byte in enumerate(file_data): if i and not (i % 16): progmem.write("\n\t") progmem.write(f"0x{byte:02X},") progmem.write("\n};\n\n") assetMap[asset_path] = { "name": asset_var, "mime": asset_mime, "size": len(file_data), } progmem.write( "typedef std::function RouteRegistrationHandler;\n\n" ) progmem.write("class WWWData {\n") progmem.write("\tpublic:\n") progmem.write("\t\tstatic void registerRoutes(RouteRegistrationHandler handler) {\n") for asset_path, asset in assetMap.items(): progmem.write(f'\t\t\thandler("/{asset_path}", "{asset["mime"]}", {asset["name"]}, {asset["size"]});\n') progmem.write("\t\t}\n") progmem.write("};\n\n") def add_app_to_filesystem(): build_path = Path(build_dir) www_path = Path(filesystem_dir) if www_path.exists() and www_path.is_dir(): rmtree(www_path) print("Copying and compress app to data directory") files_to_exclude = get_files_to_exclude() def ignore_files(dir, files): ignored = [] for file in files: file_path = Path(dir) / file relative_path = file_path.relative_to(build_path) if str(relative_path) in files_to_exclude: ignored.append(file) print(f"Excluding: {relative_path}") return ignored copytree(build_path, www_path, ignore=ignore_files if files_to_exclude else None) for current_path, _, files in os.walk(www_path): for file in files: gzip_file(os.path.join(current_path, file)) print("Build LittleFS file system image and upload to ESP32") env.Execute("pio run --target uploadfs") print("running: build_app.py") if should_regenerate_output_file(): build_webapp() embed_webapp()