diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6626e06..4b65d07 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: Build: strategy: matrix: - app_name: [Calculator, Diceware, GPIO, GraphicsDemo, HelloWorld, SerialConsole] + app_name: [Calculator, Diceware, GPIO, GraphicsDemo, HelloWorld, SerialConsole, TwoEleven] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/Apps/Calculator/tactility.py b/Apps/Calculator/tactility.py index 959146f..8861a0e 100644 --- a/Apps/Calculator/tactility.py +++ b/Apps/Calculator/tactility.py @@ -10,8 +10,6 @@ import zipfile import requests import tarfile -import shutil -import configparser ttbuild_path = ".tactility" ttbuild_version = "2.4.0" @@ -23,12 +21,12 @@ local_base_path = None if sys.platform == "win32": - shell_color_red = "" - shell_color_orange = "" - shell_color_green = "" - shell_color_purple = "" - shell_color_cyan = "" - shell_color_reset = "" + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" else: shell_color_red = "\033[91m" shell_color_orange = "\033[93m" @@ -196,7 +194,10 @@ def fetch_sdkconfig_files(platform_targets): def validate_environment(): if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if sys.platform == "win32": + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via %IDF_PATH%\\export.ps1") + else: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") if not os.path.exists("manifest.properties"): exit_with_error("manifest.properties not found") if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: @@ -336,15 +337,24 @@ def build_all(version, platforms, skip_build): def wait_for_process(process): buffer = [] - os.set_blocking(process.stdout.fileno(), False) + if sys.platform != "win32": + os.set_blocking(process.stdout.fileno(), False) while process.poll() is None: while True: line = process.stdout.readline() - decoded_line = line.decode("UTF-8") - if decoded_line != "": - buffer.append(decoded_line) + if line: + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break else: break + # Read any remaining output + for line in process.stdout: + decoded_line = line.decode("UTF-8") + if decoded_line: + buffer.append(decoded_line) return buffer # The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. @@ -356,7 +366,7 @@ def build_first(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") elf_path = find_elf_file(platform) # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, # as the actual build job will always fail due to technical issues with the elf cmake script @@ -367,7 +377,8 @@ def build_first(version, platform, skip_build): print(f"Building first {platform} build") cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -389,12 +400,13 @@ def build_consecutively(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") if skip_build: return True cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) if process.returncode == 0: print_status_success(f"Building {platform} ELF") @@ -457,7 +469,7 @@ def package_all(platforms): print_status_success(status) return True except Exception as e: - print_status_error(f"Building package failed: {e.message}") + print_status_error(f"Building package failed: {e}") return False #endregion Packaging @@ -531,7 +543,7 @@ def get_device_info(ip): print_status_success(f"Received device info:") print(response.json()) except requests.RequestException as e: - print_status_error(f"Device info request failed: {e.message}") + print_status_error(f"Device info request failed: {e}") def run_action(manifest, ip): app_id = manifest["app"]["id"] @@ -545,7 +557,7 @@ def run_action(manifest, ip): else: print_status_success("Running") except requests.RequestException as e: - print_status_error(f"Running request failed: {e.message}") + print_status_error(f"Running request failed: {e}") def install_action(ip, platforms): print_status_busy("Installing") @@ -566,15 +578,15 @@ def install_action(ip, platforms): response = requests.put(url, files=files) if response.status_code != 200: print_status_error("Install failed") - return True + return False else: print_status_success("Installing") return True except requests.RequestException as e: - print_status_error(f"Install request failed: {e.message}") + print_status_error(f"Install request failed: {e}") return False except IOError as e: - print_status_error(f"Install file error: {e.message}") + print_status_error(f"Install file error: {e}") return False def uninstall_action(manifest, ip): @@ -589,7 +601,7 @@ def uninstall_action(manifest, ip): else: print_status_success("Uninstalled") except requests.RequestException as e: - print_status_success(f"Uninstall request failed: {e.message}") + print_status_success(f"Uninstall request failed: {e}") #region Main diff --git a/Apps/Diceware/tactility.py b/Apps/Diceware/tactility.py index 959146f..8861a0e 100644 --- a/Apps/Diceware/tactility.py +++ b/Apps/Diceware/tactility.py @@ -10,8 +10,6 @@ import zipfile import requests import tarfile -import shutil -import configparser ttbuild_path = ".tactility" ttbuild_version = "2.4.0" @@ -23,12 +21,12 @@ local_base_path = None if sys.platform == "win32": - shell_color_red = "" - shell_color_orange = "" - shell_color_green = "" - shell_color_purple = "" - shell_color_cyan = "" - shell_color_reset = "" + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" else: shell_color_red = "\033[91m" shell_color_orange = "\033[93m" @@ -196,7 +194,10 @@ def fetch_sdkconfig_files(platform_targets): def validate_environment(): if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if sys.platform == "win32": + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via %IDF_PATH%\\export.ps1") + else: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") if not os.path.exists("manifest.properties"): exit_with_error("manifest.properties not found") if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: @@ -336,15 +337,24 @@ def build_all(version, platforms, skip_build): def wait_for_process(process): buffer = [] - os.set_blocking(process.stdout.fileno(), False) + if sys.platform != "win32": + os.set_blocking(process.stdout.fileno(), False) while process.poll() is None: while True: line = process.stdout.readline() - decoded_line = line.decode("UTF-8") - if decoded_line != "": - buffer.append(decoded_line) + if line: + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break else: break + # Read any remaining output + for line in process.stdout: + decoded_line = line.decode("UTF-8") + if decoded_line: + buffer.append(decoded_line) return buffer # The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. @@ -356,7 +366,7 @@ def build_first(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") elf_path = find_elf_file(platform) # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, # as the actual build job will always fail due to technical issues with the elf cmake script @@ -367,7 +377,8 @@ def build_first(version, platform, skip_build): print(f"Building first {platform} build") cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -389,12 +400,13 @@ def build_consecutively(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") if skip_build: return True cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) if process.returncode == 0: print_status_success(f"Building {platform} ELF") @@ -457,7 +469,7 @@ def package_all(platforms): print_status_success(status) return True except Exception as e: - print_status_error(f"Building package failed: {e.message}") + print_status_error(f"Building package failed: {e}") return False #endregion Packaging @@ -531,7 +543,7 @@ def get_device_info(ip): print_status_success(f"Received device info:") print(response.json()) except requests.RequestException as e: - print_status_error(f"Device info request failed: {e.message}") + print_status_error(f"Device info request failed: {e}") def run_action(manifest, ip): app_id = manifest["app"]["id"] @@ -545,7 +557,7 @@ def run_action(manifest, ip): else: print_status_success("Running") except requests.RequestException as e: - print_status_error(f"Running request failed: {e.message}") + print_status_error(f"Running request failed: {e}") def install_action(ip, platforms): print_status_busy("Installing") @@ -566,15 +578,15 @@ def install_action(ip, platforms): response = requests.put(url, files=files) if response.status_code != 200: print_status_error("Install failed") - return True + return False else: print_status_success("Installing") return True except requests.RequestException as e: - print_status_error(f"Install request failed: {e.message}") + print_status_error(f"Install request failed: {e}") return False except IOError as e: - print_status_error(f"Install file error: {e.message}") + print_status_error(f"Install file error: {e}") return False def uninstall_action(manifest, ip): @@ -589,7 +601,7 @@ def uninstall_action(manifest, ip): else: print_status_success("Uninstalled") except requests.RequestException as e: - print_status_success(f"Uninstall request failed: {e.message}") + print_status_success(f"Uninstall request failed: {e}") #region Main diff --git a/Apps/GPIO/tactility.py b/Apps/GPIO/tactility.py index 959146f..8861a0e 100644 --- a/Apps/GPIO/tactility.py +++ b/Apps/GPIO/tactility.py @@ -10,8 +10,6 @@ import zipfile import requests import tarfile -import shutil -import configparser ttbuild_path = ".tactility" ttbuild_version = "2.4.0" @@ -23,12 +21,12 @@ local_base_path = None if sys.platform == "win32": - shell_color_red = "" - shell_color_orange = "" - shell_color_green = "" - shell_color_purple = "" - shell_color_cyan = "" - shell_color_reset = "" + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" else: shell_color_red = "\033[91m" shell_color_orange = "\033[93m" @@ -196,7 +194,10 @@ def fetch_sdkconfig_files(platform_targets): def validate_environment(): if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if sys.platform == "win32": + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via %IDF_PATH%\\export.ps1") + else: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") if not os.path.exists("manifest.properties"): exit_with_error("manifest.properties not found") if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: @@ -336,15 +337,24 @@ def build_all(version, platforms, skip_build): def wait_for_process(process): buffer = [] - os.set_blocking(process.stdout.fileno(), False) + if sys.platform != "win32": + os.set_blocking(process.stdout.fileno(), False) while process.poll() is None: while True: line = process.stdout.readline() - decoded_line = line.decode("UTF-8") - if decoded_line != "": - buffer.append(decoded_line) + if line: + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break else: break + # Read any remaining output + for line in process.stdout: + decoded_line = line.decode("UTF-8") + if decoded_line: + buffer.append(decoded_line) return buffer # The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. @@ -356,7 +366,7 @@ def build_first(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") elf_path = find_elf_file(platform) # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, # as the actual build job will always fail due to technical issues with the elf cmake script @@ -367,7 +377,8 @@ def build_first(version, platform, skip_build): print(f"Building first {platform} build") cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -389,12 +400,13 @@ def build_consecutively(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") if skip_build: return True cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) if process.returncode == 0: print_status_success(f"Building {platform} ELF") @@ -457,7 +469,7 @@ def package_all(platforms): print_status_success(status) return True except Exception as e: - print_status_error(f"Building package failed: {e.message}") + print_status_error(f"Building package failed: {e}") return False #endregion Packaging @@ -531,7 +543,7 @@ def get_device_info(ip): print_status_success(f"Received device info:") print(response.json()) except requests.RequestException as e: - print_status_error(f"Device info request failed: {e.message}") + print_status_error(f"Device info request failed: {e}") def run_action(manifest, ip): app_id = manifest["app"]["id"] @@ -545,7 +557,7 @@ def run_action(manifest, ip): else: print_status_success("Running") except requests.RequestException as e: - print_status_error(f"Running request failed: {e.message}") + print_status_error(f"Running request failed: {e}") def install_action(ip, platforms): print_status_busy("Installing") @@ -566,15 +578,15 @@ def install_action(ip, platforms): response = requests.put(url, files=files) if response.status_code != 200: print_status_error("Install failed") - return True + return False else: print_status_success("Installing") return True except requests.RequestException as e: - print_status_error(f"Install request failed: {e.message}") + print_status_error(f"Install request failed: {e}") return False except IOError as e: - print_status_error(f"Install file error: {e.message}") + print_status_error(f"Install file error: {e}") return False def uninstall_action(manifest, ip): @@ -589,7 +601,7 @@ def uninstall_action(manifest, ip): else: print_status_success("Uninstalled") except requests.RequestException as e: - print_status_success(f"Uninstall request failed: {e.message}") + print_status_success(f"Uninstall request failed: {e}") #region Main diff --git a/Apps/GraphicsDemo/tactility.py b/Apps/GraphicsDemo/tactility.py index 959146f..8861a0e 100644 --- a/Apps/GraphicsDemo/tactility.py +++ b/Apps/GraphicsDemo/tactility.py @@ -10,8 +10,6 @@ import zipfile import requests import tarfile -import shutil -import configparser ttbuild_path = ".tactility" ttbuild_version = "2.4.0" @@ -23,12 +21,12 @@ local_base_path = None if sys.platform == "win32": - shell_color_red = "" - shell_color_orange = "" - shell_color_green = "" - shell_color_purple = "" - shell_color_cyan = "" - shell_color_reset = "" + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" else: shell_color_red = "\033[91m" shell_color_orange = "\033[93m" @@ -196,7 +194,10 @@ def fetch_sdkconfig_files(platform_targets): def validate_environment(): if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if sys.platform == "win32": + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via %IDF_PATH%\\export.ps1") + else: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") if not os.path.exists("manifest.properties"): exit_with_error("manifest.properties not found") if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: @@ -336,15 +337,24 @@ def build_all(version, platforms, skip_build): def wait_for_process(process): buffer = [] - os.set_blocking(process.stdout.fileno(), False) + if sys.platform != "win32": + os.set_blocking(process.stdout.fileno(), False) while process.poll() is None: while True: line = process.stdout.readline() - decoded_line = line.decode("UTF-8") - if decoded_line != "": - buffer.append(decoded_line) + if line: + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break else: break + # Read any remaining output + for line in process.stdout: + decoded_line = line.decode("UTF-8") + if decoded_line: + buffer.append(decoded_line) return buffer # The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. @@ -356,7 +366,7 @@ def build_first(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") elf_path = find_elf_file(platform) # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, # as the actual build job will always fail due to technical issues with the elf cmake script @@ -367,7 +377,8 @@ def build_first(version, platform, skip_build): print(f"Building first {platform} build") cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -389,12 +400,13 @@ def build_consecutively(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") if skip_build: return True cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) if process.returncode == 0: print_status_success(f"Building {platform} ELF") @@ -457,7 +469,7 @@ def package_all(platforms): print_status_success(status) return True except Exception as e: - print_status_error(f"Building package failed: {e.message}") + print_status_error(f"Building package failed: {e}") return False #endregion Packaging @@ -531,7 +543,7 @@ def get_device_info(ip): print_status_success(f"Received device info:") print(response.json()) except requests.RequestException as e: - print_status_error(f"Device info request failed: {e.message}") + print_status_error(f"Device info request failed: {e}") def run_action(manifest, ip): app_id = manifest["app"]["id"] @@ -545,7 +557,7 @@ def run_action(manifest, ip): else: print_status_success("Running") except requests.RequestException as e: - print_status_error(f"Running request failed: {e.message}") + print_status_error(f"Running request failed: {e}") def install_action(ip, platforms): print_status_busy("Installing") @@ -566,15 +578,15 @@ def install_action(ip, platforms): response = requests.put(url, files=files) if response.status_code != 200: print_status_error("Install failed") - return True + return False else: print_status_success("Installing") return True except requests.RequestException as e: - print_status_error(f"Install request failed: {e.message}") + print_status_error(f"Install request failed: {e}") return False except IOError as e: - print_status_error(f"Install file error: {e.message}") + print_status_error(f"Install file error: {e}") return False def uninstall_action(manifest, ip): @@ -589,7 +601,7 @@ def uninstall_action(manifest, ip): else: print_status_success("Uninstalled") except requests.RequestException as e: - print_status_success(f"Uninstall request failed: {e.message}") + print_status_success(f"Uninstall request failed: {e}") #region Main diff --git a/Apps/HelloWorld/tactility.py b/Apps/HelloWorld/tactility.py index 959146f..8861a0e 100644 --- a/Apps/HelloWorld/tactility.py +++ b/Apps/HelloWorld/tactility.py @@ -10,8 +10,6 @@ import zipfile import requests import tarfile -import shutil -import configparser ttbuild_path = ".tactility" ttbuild_version = "2.4.0" @@ -23,12 +21,12 @@ local_base_path = None if sys.platform == "win32": - shell_color_red = "" - shell_color_orange = "" - shell_color_green = "" - shell_color_purple = "" - shell_color_cyan = "" - shell_color_reset = "" + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" else: shell_color_red = "\033[91m" shell_color_orange = "\033[93m" @@ -196,7 +194,10 @@ def fetch_sdkconfig_files(platform_targets): def validate_environment(): if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if sys.platform == "win32": + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via %IDF_PATH%\\export.ps1") + else: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") if not os.path.exists("manifest.properties"): exit_with_error("manifest.properties not found") if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: @@ -336,15 +337,24 @@ def build_all(version, platforms, skip_build): def wait_for_process(process): buffer = [] - os.set_blocking(process.stdout.fileno(), False) + if sys.platform != "win32": + os.set_blocking(process.stdout.fileno(), False) while process.poll() is None: while True: line = process.stdout.readline() - decoded_line = line.decode("UTF-8") - if decoded_line != "": - buffer.append(decoded_line) + if line: + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break else: break + # Read any remaining output + for line in process.stdout: + decoded_line = line.decode("UTF-8") + if decoded_line: + buffer.append(decoded_line) return buffer # The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. @@ -356,7 +366,7 @@ def build_first(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") elf_path = find_elf_file(platform) # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, # as the actual build job will always fail due to technical issues with the elf cmake script @@ -367,7 +377,8 @@ def build_first(version, platform, skip_build): print(f"Building first {platform} build") cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -389,12 +400,13 @@ def build_consecutively(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") if skip_build: return True cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) if process.returncode == 0: print_status_success(f"Building {platform} ELF") @@ -457,7 +469,7 @@ def package_all(platforms): print_status_success(status) return True except Exception as e: - print_status_error(f"Building package failed: {e.message}") + print_status_error(f"Building package failed: {e}") return False #endregion Packaging @@ -531,7 +543,7 @@ def get_device_info(ip): print_status_success(f"Received device info:") print(response.json()) except requests.RequestException as e: - print_status_error(f"Device info request failed: {e.message}") + print_status_error(f"Device info request failed: {e}") def run_action(manifest, ip): app_id = manifest["app"]["id"] @@ -545,7 +557,7 @@ def run_action(manifest, ip): else: print_status_success("Running") except requests.RequestException as e: - print_status_error(f"Running request failed: {e.message}") + print_status_error(f"Running request failed: {e}") def install_action(ip, platforms): print_status_busy("Installing") @@ -566,15 +578,15 @@ def install_action(ip, platforms): response = requests.put(url, files=files) if response.status_code != 200: print_status_error("Install failed") - return True + return False else: print_status_success("Installing") return True except requests.RequestException as e: - print_status_error(f"Install request failed: {e.message}") + print_status_error(f"Install request failed: {e}") return False except IOError as e: - print_status_error(f"Install file error: {e.message}") + print_status_error(f"Install file error: {e}") return False def uninstall_action(manifest, ip): @@ -589,7 +601,7 @@ def uninstall_action(manifest, ip): else: print_status_success("Uninstalled") except requests.RequestException as e: - print_status_success(f"Uninstall request failed: {e.message}") + print_status_success(f"Uninstall request failed: {e}") #region Main diff --git a/Apps/SerialConsole/tactility.py b/Apps/SerialConsole/tactility.py index 959146f..8861a0e 100644 --- a/Apps/SerialConsole/tactility.py +++ b/Apps/SerialConsole/tactility.py @@ -10,8 +10,6 @@ import zipfile import requests import tarfile -import shutil -import configparser ttbuild_path = ".tactility" ttbuild_version = "2.4.0" @@ -23,12 +21,12 @@ local_base_path = None if sys.platform == "win32": - shell_color_red = "" - shell_color_orange = "" - shell_color_green = "" - shell_color_purple = "" - shell_color_cyan = "" - shell_color_reset = "" + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" else: shell_color_red = "\033[91m" shell_color_orange = "\033[93m" @@ -196,7 +194,10 @@ def fetch_sdkconfig_files(platform_targets): def validate_environment(): if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if sys.platform == "win32": + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via %IDF_PATH%\\export.ps1") + else: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") if not os.path.exists("manifest.properties"): exit_with_error("manifest.properties not found") if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: @@ -336,15 +337,24 @@ def build_all(version, platforms, skip_build): def wait_for_process(process): buffer = [] - os.set_blocking(process.stdout.fileno(), False) + if sys.platform != "win32": + os.set_blocking(process.stdout.fileno(), False) while process.poll() is None: while True: line = process.stdout.readline() - decoded_line = line.decode("UTF-8") - if decoded_line != "": - buffer.append(decoded_line) + if line: + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break else: break + # Read any remaining output + for line in process.stdout: + decoded_line = line.decode("UTF-8") + if decoded_line: + buffer.append(decoded_line) return buffer # The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. @@ -356,7 +366,7 @@ def build_first(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") elf_path = find_elf_file(platform) # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, # as the actual build job will always fail due to technical issues with the elf cmake script @@ -367,7 +377,8 @@ def build_first(version, platform, skip_build): print(f"Building first {platform} build") cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -389,12 +400,13 @@ def build_consecutively(version, platform, skip_build): print(f"Using SDK at {sdk_dir}") os.environ["TACTILITY_SDK_PATH"] = sdk_dir sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") - os.system(f"cp {sdkconfig_path} sdkconfig") + shutil.copy(sdkconfig_path, "sdkconfig") if skip_build: return True cmake_path = get_cmake_path(platform) print_status_busy(f"Building {platform} ELF") - with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: build_output = wait_for_process(process) if process.returncode == 0: print_status_success(f"Building {platform} ELF") @@ -457,7 +469,7 @@ def package_all(platforms): print_status_success(status) return True except Exception as e: - print_status_error(f"Building package failed: {e.message}") + print_status_error(f"Building package failed: {e}") return False #endregion Packaging @@ -531,7 +543,7 @@ def get_device_info(ip): print_status_success(f"Received device info:") print(response.json()) except requests.RequestException as e: - print_status_error(f"Device info request failed: {e.message}") + print_status_error(f"Device info request failed: {e}") def run_action(manifest, ip): app_id = manifest["app"]["id"] @@ -545,7 +557,7 @@ def run_action(manifest, ip): else: print_status_success("Running") except requests.RequestException as e: - print_status_error(f"Running request failed: {e.message}") + print_status_error(f"Running request failed: {e}") def install_action(ip, platforms): print_status_busy("Installing") @@ -566,15 +578,15 @@ def install_action(ip, platforms): response = requests.put(url, files=files) if response.status_code != 200: print_status_error("Install failed") - return True + return False else: print_status_success("Installing") return True except requests.RequestException as e: - print_status_error(f"Install request failed: {e.message}") + print_status_error(f"Install request failed: {e}") return False except IOError as e: - print_status_error(f"Install file error: {e.message}") + print_status_error(f"Install file error: {e}") return False def uninstall_action(manifest, ip): @@ -589,7 +601,7 @@ def uninstall_action(manifest, ip): else: print_status_success("Uninstalled") except requests.RequestException as e: - print_status_success(f"Uninstall request failed: {e.message}") + print_status_success(f"Uninstall request failed: {e}") #region Main diff --git a/Apps/TwoEleven/CMakeLists.txt b/Apps/TwoEleven/CMakeLists.txt new file mode 100644 index 0000000..7dda003 --- /dev/null +++ b/Apps/TwoEleven/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() + +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) + +project(TwoEleven) +tactility_project(TwoEleven) diff --git a/Apps/TwoEleven/README.md b/Apps/TwoEleven/README.md new file mode 100644 index 0000000..ebc2980 --- /dev/null +++ b/Apps/TwoEleven/README.md @@ -0,0 +1,57 @@ +# Two Eleven (2048 Game) + +A customizable 2048 sliding tile puzzle game for Tactility. +Named Two Eleven because numbers can be a pain in the butt with file names and 2^11=2048. + +## Overview + +TwoEleven is a faithful implementation of the popular 2048 game, where you slide numbered tiles to combine them and reach the 2048 tile. Unlike the standard 4x4 grid, this version allows you to choose grid sizes from 3x3 to 6x6, providing varying levels of challenge. + +## Features + +- **Configurable Grid Sizes**: Choose from 3x3 (easy), 4x4 (classic), 5x5 (moderate), or 6x6 (expert) grids. +- **Intuitive Controls**: Swipe gestures on touchscreens or use arrow keys for keyboard input. +- **Visual Feedback**: Color-coded tiles with smooth animations. +- **Score Tracking**: Real-time score display with win/lose detection. +- **Responsive UI**: Optimized for small screens with clean, modern design. +- **Thread-Safe**: Proper handling of UI updates to prevent crashes. + +## Screenshots + +Screenshots taken directly from my Lilygo T-Deck Plus. +Which is also the only device it has been tested on so far. + +![alt text](images/3x3.png) ![alt text](images/4x4.png) ![alt text](images/5x5.png) +![alt text](images/6x6.png) ![alt text](images/selection.png) + +## Requirements + +- Tactility - obviously... +- Touchscreen or keyboard input + +## Usage + +1. Launch the TwoEleven app. +2. Select your preferred grid size (3x3 to 6x6). +3. Swipe tiles in any direction to move and combine them. +4. Reach the 2048 tile to win, or get stuck to lose. +5. Press the refresh button to restart with the same grid size. + +## Controls + +- **Touchscreen**: Swipe up, down, left, or right to move tiles. +- **Keyboard**: Use arrow keys (↑, ↓, ←, →) for movement. +- **New Game**: Press the "New" button to reset the board. + +## Game Rules + +- Start with random tiles (2 or 4). +- Slide tiles in four directions. +- Tiles with the same number merge when they collide. +- New tiles appear after each move. +- Game ends when you reach 2048 (win) or no moves are possible (lose). + +## TODO + +- Maybe trackball one day? +- Maybe other keys rather being limited to arrow directions. (why so limited lvgl?) diff --git a/Apps/TwoEleven/images/3x3.png b/Apps/TwoEleven/images/3x3.png new file mode 100644 index 0000000..eeb5de7 Binary files /dev/null and b/Apps/TwoEleven/images/3x3.png differ diff --git a/Apps/TwoEleven/images/4x4.png b/Apps/TwoEleven/images/4x4.png new file mode 100644 index 0000000..5d592c2 Binary files /dev/null and b/Apps/TwoEleven/images/4x4.png differ diff --git a/Apps/TwoEleven/images/5x5.png b/Apps/TwoEleven/images/5x5.png new file mode 100644 index 0000000..9e506f4 Binary files /dev/null and b/Apps/TwoEleven/images/5x5.png differ diff --git a/Apps/TwoEleven/images/6x6.png b/Apps/TwoEleven/images/6x6.png new file mode 100644 index 0000000..6c5aab4 Binary files /dev/null and b/Apps/TwoEleven/images/6x6.png differ diff --git a/Apps/TwoEleven/images/selection.png b/Apps/TwoEleven/images/selection.png new file mode 100644 index 0000000..26881bb Binary files /dev/null and b/Apps/TwoEleven/images/selection.png differ diff --git a/Apps/TwoEleven/main/CMakeLists.txt b/Apps/TwoEleven/main/CMakeLists.txt new file mode 100644 index 0000000..08cb55b --- /dev/null +++ b/Apps/TwoEleven/main/CMakeLists.txt @@ -0,0 +1,15 @@ +file(GLOB_RECURSE SOURCE_FILES + Source/*.c* + # Library source files must be included directly, + # because all regular dependencies get stripped by elf_loader's cmake script + ../../../Libraries/Str/Source/*.c** +) + +idf_component_register( + SRCS ${SOURCE_FILES} + # Library headers must be included directly, + # because all regular dependencies get stripped by elf_loader's cmake script + INCLUDE_DIRS ../../../Libraries/Str/Include + INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include + REQUIRES TactilitySDK +) \ No newline at end of file diff --git a/Apps/TwoEleven/main/Source/TwoEleven.cpp b/Apps/TwoEleven/main/Source/TwoEleven.cpp new file mode 100644 index 0000000..4472a9e --- /dev/null +++ b/Apps/TwoEleven/main/Source/TwoEleven.cpp @@ -0,0 +1,202 @@ +#include "TwoEleven.h" + +#include +#include +#include + +constexpr auto* TAG = "TwoEleven"; + +static lv_obj_t* scoreLabel = nullptr; +static lv_obj_t* scoreWrapper = nullptr; +static lv_obj_t* toolbar = nullptr; +static lv_obj_t* mainWrapper = nullptr; +static lv_obj_t* newGameWrapper = nullptr; +static lv_obj_t* gameObject = nullptr; + +static uint16_t selectedSize = 4; + +void TwoEleven::twoElevenEventCb(lv_event_t* e) { + lv_event_code_t code = lv_event_get_code(e); + lv_obj_t* obj_2048 = lv_event_get_target_obj(e); + lv_obj_t* scoreLabel = (lv_obj_t *)lv_event_get_user_data(e); + + const char* alertDialogLabels[] = { "OK" }; + + if (code == LV_EVENT_VALUE_CHANGED) { + if (twoeleven_get_best_tile(obj_2048) >= 2048) { + char message[64]; + sprintf(message, "YOU WIN!\n\nSCORE: %d", twoeleven_get_score(obj_2048)); + tt_app_alertdialog_start("YOU WIN!", message, alertDialogLabels, 1); + } else if (twoeleven_get_status(obj_2048)) { + char message[64]; + sprintf(message, "GAME OVER!\n\nSCORE: %d", twoeleven_get_score(obj_2048)); + tt_app_alertdialog_start("GAME OVER!", message, alertDialogLabels, 1); + } else { + lv_label_set_text_fmt(scoreLabel, "SCORE: %d", twoeleven_get_score(obj_2048)); + } + } +} + +void TwoEleven::newGameBtnEvent(lv_event_t* e) { + lv_obj_t* obj_2048 = (lv_obj_t *)lv_event_get_user_data(e); + twoeleven_set_new_game(obj_2048); +} + +void TwoEleven::create_game(lv_obj_t* parent, uint16_t size, lv_obj_t* toolbar) { + lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + //game... + gameObject = twoeleven_create(parent, size); + lv_obj_set_style_text_font(gameObject, lv_font_get_default(), 0); + lv_obj_set_size(gameObject, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_grow(gameObject, 1); + + scoreWrapper = lv_obj_create(toolbar); + lv_obj_set_size(scoreWrapper, LV_SIZE_CONTENT, LV_PCT(100)); + lv_obj_set_style_pad_top(scoreWrapper, 4, LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(scoreWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_left(scoreWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(scoreWrapper, 10, LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(scoreWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(scoreWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(scoreWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(scoreWrapper, 0, LV_STATE_DEFAULT); + lv_obj_remove_flag(scoreWrapper, LV_OBJ_FLAG_SCROLLABLE); + + //toolbar new score + scoreLabel = lv_label_create(scoreWrapper); + lv_label_set_text_fmt(scoreLabel, "SCORE: %d", twoeleven_get_score(gameObject)); + lv_obj_set_style_text_align(scoreLabel, LV_TEXT_ALIGN_LEFT, LV_STATE_DEFAULT); + lv_obj_align(scoreLabel, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_size(scoreLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_text_font(scoreLabel, lv_font_get_default(), 0); + lv_obj_set_style_text_color(scoreLabel, lv_palette_main(LV_PALETTE_AMBER), LV_PART_MAIN); + lv_obj_add_event_cb(gameObject, twoElevenEventCb, LV_EVENT_ALL, scoreLabel); + + newGameWrapper = lv_obj_create(toolbar); + lv_obj_set_width(newGameWrapper, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(newGameWrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_all(newGameWrapper, 2, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(newGameWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(newGameWrapper, 0, LV_STATE_DEFAULT); + + //toolbar reset + lv_obj_t* newGameBtn = lv_btn_create(newGameWrapper); + lv_obj_set_size(newGameBtn, 34, 34); + lv_obj_set_style_pad_all(newGameBtn, 0, LV_STATE_DEFAULT); + lv_obj_align(newGameBtn, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_event_cb(newGameBtn, newGameBtnEvent, LV_EVENT_CLICKED, gameObject); + + lv_obj_t* btnLabel = lv_image_create(newGameBtn); + lv_image_set_src(btnLabel, LV_SYMBOL_REFRESH); + lv_obj_align(btnLabel, LV_ALIGN_CENTER, 0, 0); +} + +void TwoEleven::create_selection(lv_obj_t* parent, lv_obj_t* toolbar) { + lv_obj_t* selection = lv_obj_create(parent); + lv_obj_set_size(selection, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(selection, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_grow(selection, 1); + lv_obj_remove_flag(selection, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(selection, 0, LV_PART_MAIN); + lv_obj_set_style_border_width(selection, 0, LV_STATE_DEFAULT); + + lv_obj_t* titleWrapper = lv_obj_create(selection); + lv_obj_set_size(titleWrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(titleWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(titleWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(titleWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(titleWrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(titleWrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(titleWrapper, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* titleLabel = lv_label_create(titleWrapper); + lv_label_set_text(titleLabel, "Select Matrix Size"); + lv_obj_align(titleLabel, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_size(titleLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + + lv_obj_t* controlsWrapper = lv_obj_create(titleWrapper); + lv_obj_set_size(controlsWrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(controlsWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(controlsWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(controlsWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(controlsWrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(controlsWrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(controlsWrapper, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* touchControlsLabel = lv_label_create(controlsWrapper); + lv_label_set_text(touchControlsLabel, "Touchscreen:\nSwipe up, down, left, right to move tiles."); + lv_obj_set_style_text_font(touchControlsLabel, lv_font_get_default(), 0); + lv_obj_set_style_text_align(touchControlsLabel, LV_TEXT_ALIGN_CENTER, 0); + + lv_obj_t* keyControlsLabel = lv_label_create(controlsWrapper); + lv_label_set_text_fmt(keyControlsLabel, "Keyboard:\nUse arrow keys (%s, %s, %s, %s) to move tiles.", LV_SYMBOL_UP, LV_SYMBOL_DOWN, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT); + lv_obj_set_style_text_font(keyControlsLabel, lv_font_get_default(), 0); + lv_obj_set_style_text_align(keyControlsLabel, LV_TEXT_ALIGN_CENTER, 0); + + lv_obj_t* buttonContainer = lv_obj_create(selection); + lv_obj_set_flex_flow(buttonContainer, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(buttonContainer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(buttonContainer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(buttonContainer, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(buttonContainer, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(buttonContainer, 0, LV_STATE_DEFAULT); + + for(int s = 3; s <= 6; s++) { + lv_obj_t* btn = lv_btn_create(buttonContainer); + lv_obj_set_size(btn, 60, 40); + lv_obj_t* lbl = lv_label_create(btn); + char txt[10]; + sprintf(txt, "%dx%d", s, s); + lv_label_set_text(lbl, txt); + lv_obj_center(lbl); + lv_obj_add_event_cb(btn, size_select_cb, LV_EVENT_CLICKED, (void*)s); + } +} + +void TwoEleven::size_select_cb(lv_event_t* e) { + selectedSize = (uint16_t)(uintptr_t)lv_event_get_user_data(e); + lv_obj_t* selection = lv_obj_get_parent(lv_event_get_target_obj(e)); + lv_obj_t* selectionWrapper = lv_obj_get_parent(selection); + lv_obj_clean(selectionWrapper); + scoreLabel = nullptr; + scoreWrapper = nullptr; + newGameWrapper = nullptr; + gameObject = nullptr; + create_game(selectionWrapper, selectedSize, toolbar); +} + +void TwoEleven::onShow(AppHandle appHandle, lv_obj_t* parent) { + lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + mainWrapper = lv_obj_create(parent); + lv_obj_set_width(mainWrapper, LV_PCT(100)); + lv_obj_set_height(mainWrapper, LV_PCT(100)); + lv_obj_set_flex_grow(mainWrapper, 1); + lv_obj_set_style_pad_all(mainWrapper, 2, LV_PART_MAIN); + lv_obj_set_style_pad_row(mainWrapper, 2, LV_PART_MAIN); + lv_obj_set_style_pad_column(mainWrapper, 2, LV_PART_MAIN); + lv_obj_set_style_border_width(mainWrapper, 0, LV_PART_MAIN); + lv_obj_remove_flag(mainWrapper, LV_OBJ_FLAG_SCROLLABLE); + + create_selection(mainWrapper, toolbar); +} + +void TwoEleven::onResult(AppHandle appHandle, void* _Nullable data, AppLaunchId launchId, AppResult result, BundleHandle resultData) { + if (result == APP_RESULT_OK && resultData != nullptr) { + // Dialog closed with OK, go back to selection + tt_lvgl_lock(TT_LVGL_DEFAULT_LOCK_TIME); + lv_obj_clean(mainWrapper); + scoreLabel = nullptr; + scoreWrapper = nullptr; + newGameWrapper = nullptr; + gameObject = nullptr; + create_selection(mainWrapper, toolbar); + tt_lvgl_unlock(); + } +} diff --git a/Apps/TwoEleven/main/Source/TwoEleven.h b/Apps/TwoEleven/main/Source/TwoEleven.h new file mode 100644 index 0000000..c9a0494 --- /dev/null +++ b/Apps/TwoEleven/main/Source/TwoEleven.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include +#include + +#include "TwoElevenUi.h" +#include "TwoElevenLogic.h" +#include "TwoElevenHelpers.h" + +class TwoEleven final : public App { + + static void twoElevenEventCb(lv_event_t* e); + static void newGameBtnEvent(lv_event_t* e); + static void create_game(lv_obj_t* parent, uint16_t size, lv_obj_t* toolbar); + static void create_selection(lv_obj_t* parent, lv_obj_t* toolbar); + static void size_select_cb(lv_event_t* e); + +public: + + void onShow(AppHandle context, lv_obj_t* parent) override; + void onResult(AppHandle appHandle, void* _Nullable data, AppLaunchId launchId, AppResult result, BundleHandle resultData) override; +}; \ No newline at end of file diff --git a/Apps/TwoEleven/main/Source/TwoElevenHelpers.c b/Apps/TwoEleven/main/Source/TwoElevenHelpers.c new file mode 100644 index 0000000..7c3c58c --- /dev/null +++ b/Apps/TwoEleven/main/Source/TwoElevenHelpers.c @@ -0,0 +1,130 @@ +#include "TwoElevenHelpers.h" +#include +#include + +/** + * @brief Get the color for a given tile value + */ +lv_color_t get_num_color(uint16_t num) +{ + switch (num) { + case 0: + return ELEVENTWO_NUMBER_EMPTY_COLOR; + case 1: + return ELEVENTWO_NUMBER_EMPTY_COLOR; + case 2: + return ELEVENTWO_NUMBER_2_COLOR; + case 4: + return ELEVENTWO_NUMBER_4_COLOR; + case 8: + return ELEVENTWO_NUMBER_8_COLOR; + case 16: + return ELEVENTWO_NUMBER_16_COLOR; + case 32: + return ELEVENTWO_NUMBER_32_COLOR; + case 64: + return ELEVENTWO_NUMBER_64_COLOR; + case 128: + return ELEVENTWO_NUMBER_128_COLOR; + case 256: + return ELEVENTWO_NUMBER_256_COLOR; + case 512: + return ELEVENTWO_NUMBER_512_COLOR; + case 1024: + return ELEVENTWO_NUMBER_1024_COLOR; + case 2048: + return ELEVENTWO_NUMBER_2048_COLOR; + default: + return ELEVENTWO_NUMBER_2048_COLOR; + } +} + +/** + * @brief Count empty cells in the matrix + */ +uint8_t count_empty(uint16_t matrix_size, const uint16_t **matrix) { + uint8_t count = 0; + for (uint8_t x = 0; x < matrix_size; x++) { + for (uint8_t y = 0; y < matrix_size; y++) { + if (matrix[x][y] == 0) count++; + } + } + return count; +} + +/** + * @brief Find if any adjacent pairs exist (for move possibility) + */ +bool find_pair_down(uint16_t matrix_size, const uint16_t **matrix) { + for (uint8_t x = 0; x < matrix_size; x++) { + for (uint8_t y = 0; y < matrix_size - 1; y++) { + if (matrix[x][y] == matrix[x][y+1]) return true; + } + } + return false; +} + +/** + * @brief Rotate the matrix 90 degrees clockwise (in-place) + * Used for move logic + */ +void rotate_matrix(uint16_t matrix_size, uint16_t **matrix) { + uint8_t n = matrix_size; + for (uint8_t i = 0; i < n / 2; i++) { + for (uint8_t j = i; j < n - i - 1; j++) { + uint16_t tmp = matrix[i][j]; + matrix[i][j] = matrix[n - j - 1][i]; + matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1]; + matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1]; + matrix[j][n - i - 1] = tmp; + } + } +} + +/** + * @brief Slide and merge a single row/column + */ +bool slide_array(uint16_t * score, uint16_t matrix_size, uint16_t *array) { + bool success = false; + uint8_t stop = 0; + for (uint8_t x = 0; x < matrix_size; x++) { + if (array[x] != 0) { + uint8_t t = x; + while (t > stop && array[t-1] == 0) { + array[t-1] = array[t]; + array[t] = 0; + t--; + success = true; + } + if (t > stop && array[t-1] == array[t]) { + array[t-1]++; + *score += (1U << array[t-1]); + array[t] = 0; + stop = t; + success = true; + } + } + } + return success; +} + +/** + * @brief Update the button map with current matrix values + */ +void update_btnm_map(uint16_t matrix_size, char ** btnm_map, const uint16_t **matrix) +{ + uint8_t index = 0; + for (uint8_t x = 0; x < matrix_size; x++) { + for (uint8_t y = 0; y < matrix_size; y++) { + if (((index + 1) % (matrix_size + 1)) == 0) { + index++; + } + if (matrix[x][y] != 0) { + snprintf(btnm_map[index], 16, "%d", (1 << matrix[x][y])); + } else { + strcpy(btnm_map[index], " "); + } + index++; + } + } +} diff --git a/Apps/TwoEleven/main/Source/TwoElevenHelpers.h b/Apps/TwoEleven/main/Source/TwoElevenHelpers.h new file mode 100644 index 0000000..f36a2a8 --- /dev/null +++ b/Apps/TwoEleven/main/Source/TwoElevenHelpers.h @@ -0,0 +1,89 @@ +#ifndef TWOELEVEN_HELPERS_H +#define TWOELEVEN_HELPERS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "lvgl.h" + +/********************* + * DEFINES + *********************/ + +#define ELEVENTWO_BG_COLOR lv_color_hex(0xb3a397) +#define ELEVENTWO_TEXT_BLACK_COLOR lv_color_hex(0x6c635b) +#define ELEVENTWO_TEXT_WHITE_COLOR lv_color_hex(0xf8f5f0) +#define ELEVENTWO_NUMBER_EMPTY_COLOR lv_color_hex(0xc7b9ac) +#define ELEVENTWO_NUMBER_2_COLOR lv_color_hex(0xeee4da) +#define ELEVENTWO_NUMBER_4_COLOR lv_color_hex(0xede0c8) +#define ELEVENTWO_NUMBER_8_COLOR lv_color_hex(0xf2b179) +#define ELEVENTWO_NUMBER_16_COLOR lv_color_hex(0xf59563) +#define ELEVENTWO_NUMBER_32_COLOR lv_color_hex(0xf67c5f) +#define ELEVENTWO_NUMBER_64_COLOR lv_color_hex(0xf75f3b) +#define ELEVENTWO_NUMBER_128_COLOR lv_color_hex(0xedcf72) +#define ELEVENTWO_NUMBER_256_COLOR lv_color_hex(0xedcc61) +#define ELEVENTWO_NUMBER_512_COLOR lv_color_hex(0xedc850) +#define ELEVENTWO_NUMBER_1024_COLOR lv_color_hex(0xedc53f) +#define ELEVENTWO_NUMBER_2048_COLOR lv_color_hex(0xedc22e) + +/********************** + * TYPEDEFS + **********************/ +/** + * @brief Struct for 2048 game state + */ +struct twoeleven_t { + lv_obj_t * btnm; + uint16_t score; + uint16_t map_count; + uint16_t matrix_size; + uint16_t **matrix; + char ** btnm_map; + bool game_over; +}; + +typedef struct twoeleven_t twoeleven_t; + +/*********************** + * FUNCTION PROTOTYPES + **********************/ +/** + * @brief Get the color for a given tile value + */ +lv_color_t get_num_color(uint16_t num); + +/** + * @brief Count empty cells in the matrix + */ +uint8_t count_empty(uint16_t matrix_size, const uint16_t **matrix); + +/** + * @brief Find if any adjacent pairs exist (for move possibility) + */ +bool find_pair_down(uint16_t matrix_size, const uint16_t **matrix); + +/** + * @brief Rotate the matrix 90 degrees clockwise (in-place) + * Used for move logic + */ +void rotate_matrix(uint16_t matrix_size, uint16_t **matrix); + +/** + * @brief Slide and merge a single row/column + */ +bool slide_array(uint16_t * score, uint16_t matrix_size, uint16_t *array); + +/** + * @brief Update the button map with current matrix values + */ +void update_btnm_map(uint16_t matrix_size, char ** btnm_map, const uint16_t **matrix); + +#ifdef __cplusplus +} /*extern "C"*/ +#endif + +#endif /*TWOELEVEN_HELPERS_H*/ \ No newline at end of file diff --git a/Apps/TwoEleven/main/Source/TwoElevenLogic.c b/Apps/TwoEleven/main/Source/TwoElevenLogic.c new file mode 100644 index 0000000..a3001be --- /dev/null +++ b/Apps/TwoEleven/main/Source/TwoElevenLogic.c @@ -0,0 +1,162 @@ +#include "TwoElevenLogic.h" +#include "TwoElevenHelpers.h" +#include +#include +#include + +/** + * @brief Initialize the matrix with two random tiles + */ +void init_matrix_num(uint16_t matrix_size, uint16_t **matrix) +{ + for (uint8_t x = 0; x < matrix_size; x++) { + for (uint8_t y = 0; y < matrix_size; y++) { + matrix[x][y] = 0; + } + } + // Add two random tiles + add_random(matrix_size, matrix); + add_random(matrix_size, matrix); +} + +/** + * @brief Add a random tile (2 or 4) to the matrix + */ +void add_random(uint16_t matrix_size, uint16_t **matrix) +{ + static bool initialized = false; + if (!initialized) { + srand((unsigned int)time(NULL)); + initialized = true; + } + uint16_t empty[matrix_size * matrix_size][2]; + uint16_t len = 0; + for (uint8_t x = 0; x < matrix_size; x++) { + for (uint8_t y = 0; y < matrix_size; y++) { + if (matrix[x][y] == 0) { + empty[len][0] = x; + empty[len][1] = y; + len++; + } + } + } + if (len > 0) { + uint16_t r = rand() % len; + uint8_t x = empty[r][0]; + uint8_t y = empty[r][1]; + uint16_t n = (rand() % 10 == 0) ? 2 : 1; // 10% chance for 4, else 2 + matrix[x][y] = n; + } +} + +/** + * @brief Move up (or left/right/down via rotation) + */ +bool move_up(uint16_t * score, uint16_t matrix_size, uint16_t **matrix) { + bool success = false; + for (uint8_t x = 0; x < matrix_size; x++) { + success |= slide_array(score, matrix_size, matrix[x]); + } + return success; +} + +bool move_down(uint16_t * score, uint16_t matrix_size, uint16_t **matrix) { + rotate_matrix(matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + bool success = move_up(score, matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + return success; +} + +bool move_left(uint16_t * score, uint16_t matrix_size, uint16_t **matrix) { + rotate_matrix(matrix_size, matrix); + bool success = move_up(score, matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + return success; +} + +bool move_right(uint16_t * score, uint16_t matrix_size, uint16_t **matrix) { + rotate_matrix(matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + bool success = move_up(score, matrix_size, matrix); + rotate_matrix(matrix_size, matrix); + return success; +} + +/** + * @brief Check if the game is over (no moves left) + * Does not mutate the original matrix + */ +bool game_over(uint16_t matrix_size, const uint16_t **matrix) { + if (count_empty(matrix_size, matrix) > 0) return false; + if (find_pair_down(matrix_size, matrix)) return false; + // Check for possible moves in rotated matrices + uint16_t **temp = lv_malloc(matrix_size * sizeof(uint16_t*)); + for (uint16_t i = 0; i < matrix_size; i++) { + temp[i] = lv_malloc(matrix_size * sizeof(uint16_t)); + memcpy(temp[i], matrix[i], matrix_size * sizeof(uint16_t)); + } + rotate_matrix(matrix_size, temp); + if (find_pair_down(matrix_size, (const uint16_t **)temp)) { + for (uint16_t i = 0; i < matrix_size; i++) lv_free(temp[i]); + lv_free(temp); + return false; + } + rotate_matrix(matrix_size, temp); + if (find_pair_down(matrix_size, (const uint16_t **)temp)) { + for (uint16_t i = 0; i < matrix_size; i++) lv_free(temp[i]); + lv_free(temp); + return false; + } + rotate_matrix(matrix_size, temp); + if (find_pair_down(matrix_size, (const uint16_t **)temp)) { + for (uint16_t i = 0; i < matrix_size; i++) lv_free(temp[i]); + lv_free(temp); + return false; + } + for (uint16_t i = 0; i < matrix_size; i++) lv_free(temp[i]); + lv_free(temp); + return true; +} + +/** + * @brief Get the current score + */ +uint16_t twoeleven_get_score(lv_obj_t * obj) +{ + const twoeleven_t * game_2048 = (const twoeleven_t *)lv_obj_get_user_data(obj); + if (!game_2048) return 0; + return game_2048->score; +} + +/** + * @brief Get the game over status + */ +bool twoeleven_get_status(lv_obj_t * obj) +{ + const twoeleven_t * game_2048 = (const twoeleven_t *)lv_obj_get_user_data(obj); + if (!game_2048) return true; + return game_2048->game_over; +} + +/** + * @brief Get the best tile value + */ +uint16_t twoeleven_get_best_tile(lv_obj_t * obj) +{ + const twoeleven_t * game_2048 = (const twoeleven_t *)lv_obj_get_user_data(obj); + if (!game_2048) return 0; + uint16_t best_tile = 0; + for (uint8_t x = 0; x < game_2048->matrix_size; x++) { + for (uint8_t y = 0; y < game_2048->matrix_size; y++) { + if (best_tile < game_2048->matrix[x][y]) { + best_tile = game_2048->matrix[x][y]; + } + } + } + return (uint16_t)(1 << best_tile); +} diff --git a/Apps/TwoEleven/main/Source/TwoElevenLogic.h b/Apps/TwoEleven/main/Source/TwoElevenLogic.h new file mode 100644 index 0000000..1c01873 --- /dev/null +++ b/Apps/TwoEleven/main/Source/TwoElevenLogic.h @@ -0,0 +1,71 @@ +#ifndef TWOELEVEN_LOGIC_H +#define TWOELEVEN_LOGIC_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "TwoElevenHelpers.h" + +/*********************** + * FUNCTION PROTOTYPES + **********************/ +/** + * @brief Initialize the matrix with two random tiles + */ +void init_matrix_num(uint16_t matrix_size, uint16_t **matrix); + +/** + * @brief Add a random tile (2 or 4) to the matrix + */ +void add_random(uint16_t matrix_size, uint16_t **matrix); + +/** + * @brief Move up + */ +bool move_up(uint16_t * score, uint16_t matrix_size, uint16_t **matrix); + +/** + * @brief Move down + */ +bool move_down(uint16_t * score, uint16_t matrix_size, uint16_t **matrix); + +/** + * @brief Move left + */ +bool move_left(uint16_t * score, uint16_t matrix_size, uint16_t **matrix); + +/** + * @brief Move right + */ +bool move_right(uint16_t * score, uint16_t matrix_size, uint16_t **matrix); + +/** + * @brief Check if the game is over (no moves left) + * Does not mutate the original matrix + */ +bool game_over(uint16_t matrix_size, const uint16_t **matrix); + +/** + * @brief Get the current score + */ +uint16_t twoeleven_get_score(lv_obj_t * obj); + +/** + * @brief Get the game over status + */ +bool twoeleven_get_status(lv_obj_t * obj); + +/** + * @brief Get the best tile value + */ +uint16_t twoeleven_get_best_tile(lv_obj_t * obj); + +#ifdef __cplusplus +} /*extern "C"*/ +#endif + +#endif /*TWOELEVEN_LOGIC_H*/ \ No newline at end of file diff --git a/Apps/TwoEleven/main/Source/TwoElevenUi.c b/Apps/TwoEleven/main/Source/TwoElevenUi.c new file mode 100644 index 0000000..cd15459 --- /dev/null +++ b/Apps/TwoEleven/main/Source/TwoElevenUi.c @@ -0,0 +1,242 @@ +#include "TwoElevenUi.h" +#include "TwoElevenLogic.h" +#include "TwoElevenHelpers.h" +#include +#include + +static void game_play_event(lv_event_t * e); +static void btnm_event_cb(lv_event_t * e); + +/** + * @brief Free all resources for the 2048 game object + */ +static void delete_event(lv_event_t * e) +{ + lv_obj_t * obj = lv_event_get_target_obj(e); + twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj); + if (game_2048) { + for (uint16_t index = 0; index < game_2048->map_count; index++) { + if (game_2048->btnm_map[index]) { + lv_free(game_2048->btnm_map[index]); + game_2048->btnm_map[index] = NULL; + } + } + lv_free(game_2048->btnm_map); + // Free matrix + for (uint16_t i = 0; i < game_2048->matrix_size; i++) { + lv_free(game_2048->matrix[i]); + } + lv_free(game_2048->matrix); + lv_free(game_2048); + lv_obj_set_user_data(obj, NULL); + } +} + +/** + * @brief Create a new 2048 game object + */ +lv_obj_t * twoeleven_create(lv_obj_t * parent, uint16_t matrix_size) +{ + lv_obj_t * obj = lv_obj_create(parent); + if (!obj) return NULL; + twoeleven_t * game_2048 = (twoeleven_t *)lv_malloc(sizeof(twoeleven_t)); + if (!game_2048) { + lv_obj_delete(obj); + return NULL; + } + lv_obj_set_user_data(obj, game_2048); + + game_2048->score = 0; + game_2048->game_over = false; + // Limit matrix size to 3x3 to 6x6, default to 4x4 if out of range + if (matrix_size < 3 || matrix_size > 6) { + matrix_size = 4; + } + game_2048->matrix_size = matrix_size; + game_2048->map_count = game_2048->matrix_size * game_2048->matrix_size + game_2048->matrix_size; + + // Allocate matrix + game_2048->matrix = lv_malloc(game_2048->matrix_size * sizeof(uint16_t*)); + if (!game_2048->matrix) { + lv_free(game_2048); + lv_obj_delete(obj); + return NULL; + } + for (uint16_t i = 0; i < game_2048->matrix_size; i++) { + game_2048->matrix[i] = lv_malloc(game_2048->matrix_size * sizeof(uint16_t)); + if (!game_2048->matrix[i]) { + for (uint16_t j = 0; j < i; j++) lv_free(game_2048->matrix[j]); + lv_free(game_2048->matrix); + lv_free(game_2048); + lv_obj_delete(obj); + return NULL; + } + } + + // Allocate button map + game_2048->btnm_map = lv_malloc(game_2048->map_count * sizeof(char*)); + for (uint16_t index = 0; index < game_2048->map_count; index++) { + if (((index + 1) % (game_2048->matrix_size + 1)) == 0) { + game_2048->btnm_map[index] = (char *)lv_malloc(2); + if (!game_2048->btnm_map[index]) continue; + if ((index + 1) == game_2048->map_count) + strcpy(game_2048->btnm_map[index], ""); + else + strcpy(game_2048->btnm_map[index], "\n"); + } else { + game_2048->btnm_map[index] = (char *)lv_malloc(16); + if (!game_2048->btnm_map[index]) continue; + strcpy(game_2048->btnm_map[index], " "); + } + } + + init_matrix_num(game_2048->matrix_size, game_2048->matrix); + update_btnm_map(game_2048->matrix_size, game_2048->btnm_map, (const uint16_t **)game_2048->matrix); + + // Style initialization (unchanged) + lv_obj_set_style_outline_color(obj, lv_theme_get_color_primary(obj), LV_STATE_FOCUS_KEY); + lv_obj_set_style_outline_width(obj, lv_display_dpx(lv_obj_get_disp(obj), 2), LV_STATE_FOCUS_KEY); + lv_obj_set_style_outline_pad(obj, lv_display_dpx(lv_obj_get_disp(obj), 2), LV_STATE_FOCUS_KEY); + lv_obj_set_style_outline_opa(obj, LV_OPA_50, LV_STATE_FOCUS_KEY); + + // Create button matrix + game_2048->btnm = lv_btnmatrix_create(obj); + lv_obj_set_size(game_2048->btnm, LV_PCT(100), LV_PCT(100)); + lv_obj_center(game_2048->btnm); + lv_obj_set_style_pad_all(game_2048->btnm, 10, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(game_2048->btnm, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(game_2048->btnm, ELEVENTWO_BG_COLOR, LV_PART_MAIN); + lv_group_remove_obj(game_2048->btnm); + lv_obj_add_flag(game_2048->btnm, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS); + lv_obj_remove_flag(game_2048->btnm, LV_OBJ_FLAG_GESTURE_BUBBLE); + + lv_btnmatrix_set_map(game_2048->btnm, (const char **)game_2048->btnm_map); + lv_btnmatrix_set_btn_ctrl_all(game_2048->btnm, LV_BTNMATRIX_CTRL_DISABLED); + + lv_obj_add_event_cb(game_2048->btnm, game_play_event, LV_EVENT_ALL, obj); + lv_obj_add_event_cb(game_2048->btnm, btnm_event_cb, LV_EVENT_DRAW_TASK_ADDED, NULL); + lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL); + + return obj; +} + +/** + * @brief Start a new game (reset state) + */ +void twoeleven_set_new_game(lv_obj_t * obj) +{ + twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj); + if (!game_2048) return; + game_2048->score = 0; + game_2048->game_over = false; + game_2048->map_count = game_2048->matrix_size * game_2048->matrix_size + game_2048->matrix_size; + init_matrix_num(game_2048->matrix_size, game_2048->matrix); + update_btnm_map(game_2048->matrix_size, game_2048->btnm_map, (const uint16_t **)game_2048->matrix); + lv_btnmatrix_set_map(game_2048->btnm, (const char **)game_2048->btnm_map); + lv_obj_invalidate(game_2048->btnm); + lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); +} + +/** + * @brief Event callback for game play (gesture/key) + */ +static void game_play_event(lv_event_t * e) +{ + lv_event_code_t code = lv_event_get_code(e); + lv_obj_t * obj = lv_event_get_user_data(e); + twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj); + bool success = false; + if (!game_2048) return; + + if (code == LV_EVENT_GESTURE) { + game_2048->game_over = game_over(game_2048->matrix_size, (const uint16_t **)game_2048->matrix); + if (!game_2048->game_over) { + lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_active()); + switch (dir) { + case LV_DIR_TOP: + success = move_left(&(game_2048->score), game_2048->matrix_size, game_2048->matrix); + break; + case LV_DIR_BOTTOM: + success = move_right(&(game_2048->score), game_2048->matrix_size, game_2048->matrix); + break; + case LV_DIR_LEFT: + success = move_up(&(game_2048->score), game_2048->matrix_size, game_2048->matrix); + break; + case LV_DIR_RIGHT: + success = move_down(&(game_2048->score), game_2048->matrix_size, game_2048->matrix); + break; + default: + break; + } + } else { + lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); + return; + } + } else if (code == LV_EVENT_KEY) { + game_2048->game_over = game_over(game_2048->matrix_size, (const uint16_t **)game_2048->matrix); + if (!game_2048->game_over) { + switch (*((const uint8_t *) lv_event_get_param(e))) { + case LV_KEY_UP: + success = move_left(&(game_2048->score), game_2048->matrix_size, game_2048->matrix); + break; + case LV_KEY_DOWN: + success = move_right(&(game_2048->score), game_2048->matrix_size, game_2048->matrix); + break; + case LV_KEY_LEFT: + success = move_up(&(game_2048->score), game_2048->matrix_size, game_2048->matrix); + break; + case LV_KEY_RIGHT: + success = move_down(&(game_2048->score), game_2048->matrix_size, game_2048->matrix); + break; + default: + break; + } + } else { + lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); + return; + } + } + + if (success) { + add_random(game_2048->matrix_size, game_2048->matrix); + update_btnm_map(game_2048->matrix_size, game_2048->btnm_map, (const uint16_t **)game_2048->matrix); + lv_btnmatrix_set_map(game_2048->btnm, (const char **)game_2048->btnm_map); + lv_obj_invalidate(game_2048->btnm); + lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); + } +} + +/** + * @brief Event callback for button matrix drawing (coloring tiles) + */ +static void btnm_event_cb(lv_event_t * e) +{ + lv_obj_t * btnm = lv_event_get_target_obj(e); + lv_obj_t * parent = lv_obj_get_parent(btnm); + twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(parent); + if (!game_2048) return; + + lv_draw_task_t * draw_task = lv_event_get_draw_task(e); + lv_draw_dsc_base_t * base_dsc = lv_draw_task_get_draw_dsc(draw_task); + + if (base_dsc->part == LV_PART_ITEMS) { + lv_draw_label_dsc_t * label_draw_dsc = lv_draw_task_get_label_dsc(draw_task); + lv_draw_fill_dsc_t * fill_draw_dsc = lv_draw_task_get_fill_dsc(draw_task); + + if ((int32_t)base_dsc->id1 >= 0) { + uint16_t x = (uint16_t)((base_dsc->id1) / game_2048->matrix_size); + uint16_t y = (uint16_t)((base_dsc->id1) % game_2048->matrix_size); + uint16_t num = (uint16_t)(1 << (game_2048->matrix[x][y])); + + if (fill_draw_dsc) { + fill_draw_dsc->radius = 3; + fill_draw_dsc->color = get_num_color(num); + } + if (label_draw_dsc) { + label_draw_dsc->color = (num < 8) ? ELEVENTWO_TEXT_BLACK_COLOR : ELEVENTWO_TEXT_WHITE_COLOR; + } + } else { + if (fill_draw_dsc) fill_draw_dsc->radius = 5; + } + } +} diff --git a/Apps/TwoEleven/main/Source/TwoElevenUi.h b/Apps/TwoEleven/main/Source/TwoElevenUi.h new file mode 100644 index 0000000..959d97f --- /dev/null +++ b/Apps/TwoEleven/main/Source/TwoElevenUi.h @@ -0,0 +1,35 @@ +#ifndef TWOELEVEN_UI_H +#define TWOELEVEN_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "TwoElevenHelpers.h" +#include "lvgl.h" + +/*********************** + * FUNCTION PROTOTYPES + **********************/ +/** + * @brief Create a new 2048 game object + * @param parent Parent LVGL object + * @param matrix_size Size of the game matrix (3 to 6, defaults to 4 if out of range) + * @return Pointer to the created LVGL object + */ +lv_obj_t * twoeleven_create(lv_obj_t * parent, uint16_t matrix_size); + +/** + * @brief Start a new game (reset state) + * @param obj 2048 game LVGL object + */ +void twoeleven_set_new_game(lv_obj_t * obj); + +#ifdef __cplusplus +} /*extern "C"*/ +#endif + +#endif /*TWOELEVEN_UI_H*/ \ No newline at end of file diff --git a/Apps/TwoEleven/main/Source/main.cpp b/Apps/TwoEleven/main/Source/main.cpp new file mode 100644 index 0000000..12d95aa --- /dev/null +++ b/Apps/TwoEleven/main/Source/main.cpp @@ -0,0 +1,11 @@ +#include "TwoEleven.h" +#include + +extern "C" { + +int main(int argc, char* argv[]) { + registerApp(); + return 0; +} + +} diff --git a/Apps/TwoEleven/manifest.properties b/Apps/TwoEleven/manifest.properties new file mode 100644 index 0000000..1ce7e24 --- /dev/null +++ b/Apps/TwoEleven/manifest.properties @@ -0,0 +1,11 @@ +[manifest] +version=0.1 +[target] +sdk=0.7.0-SNAPSHOT2 +platforms=esp32,esp32s3 +[app] +id=one.tactility.twoeleven +versionName=0.2.0 +versionCode=2 +name=2048 +description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert). \ No newline at end of file diff --git a/Apps/TwoEleven/tactility.py b/Apps/TwoEleven/tactility.py new file mode 100644 index 0000000..8861a0e --- /dev/null +++ b/Apps/TwoEleven/tactility.py @@ -0,0 +1,690 @@ +import configparser +import json +import os +import re +import shutil +import sys +import subprocess +import time +import urllib.request +import zipfile +import requests +import tarfile + +ttbuild_path = ".tactility" +ttbuild_version = "2.4.0" +ttbuild_cdn = "https://cdn.tactility.one" +ttbuild_sdk_json_validity = 3600 # seconds +ttport = 6666 +verbose = False +use_local_sdk = False +local_base_path = None + +if sys.platform == "win32": + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" +else: + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" + +def print_help(): + print("Usage: python tactility.py [action] [options]") + print("") + print("Actions:") + print(" build [esp32,esp32s3] Build the app. Optionally specify a platform.") + print(" esp32: ESP32") + print(" esp32s3: ESP32 S3") + print(" clean Clean the build folders") + print(" clearcache Clear the SDK cache") + print(" updateself Update this tool") + print(" run [ip] Run the application") + print(" install [ip] Install the application") + print(" uninstall [ip] Uninstall the application") + print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.") + print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.") + print("") + print("Options:") + print(" --help Show this commandline info") + print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH with platform subfolders matching target platforms.") + print(" --skip-build Run everything except the idf.py/CMake commands") + print(" --verbose Show extra console output") + +# region Core + +def download_file(url, filepath): + global verbose + if verbose: + print(f"Downloading from {url} to {filepath}") + request = urllib.request.Request( + url, + data=None, + headers={ + "User-Agent": f"Tactility Build Tool {ttbuild_version}" + } + ) + try: + response = urllib.request.urlopen(request) + file = open(filepath, mode="wb") + file.write(response.read()) + file.close() + return True + except OSError as error: + if verbose: + print_error(f"Failed to fetch URL {url}\n{error}") + return False + +def print_warning(message): + print(f"{shell_color_orange}WARNING: {message}{shell_color_reset}") + +def print_error(message): + print(f"{shell_color_red}ERROR: {message}{shell_color_reset}") + +def print_status_busy(status): + sys.stdout.write(f"⌛ {status}\r") + +def print_status_success(status): + # Trailing spaces are to overwrite previously written characters by a potentially shorter print_status_busy() text + print(f"✅ {shell_color_green}{status}{shell_color_reset} ") + +def print_status_error(status): + # Trailing spaces are to overwrite previously written characters by a potentially shorter print_status_busy() text + print(f"❌ {shell_color_red}{status}{shell_color_reset} ") + +def exit_with_error(message): + print_error(message) + sys.exit(1) + +def get_url(ip, path): + return f"http://{ip}:{ttport}{path}" + +def read_properties_file(path): + config = configparser.RawConfigParser() + config.read(path) + return config + +#endregion Core + +#region SDK helpers + +def read_sdk_json(): + json_file_path = os.path.join(ttbuild_path, "sdk.json") + json_file = open(json_file_path) + return json.load(json_file) + +def get_sdk_dir(version, platform): + global use_local_sdk, local_base_path + if use_local_sdk: + base_path = local_base_path + if base_path is None: + exit_with_error("TACTILITY_SDK_PATH environment variable is not set") + sdk_parent_dir = os.path.join(base_path, f"{version}-{platform}") + sdk_dir = os.path.join(sdk_parent_dir, "TactilitySDK") + if not os.path.isdir(sdk_dir): + exit_with_error(f"Local SDK folder not found for platform {platform}: {sdk_dir}") + return sdk_dir + else: + return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") + +def validate_local_sdks(platforms, version): + if not use_local_sdk: + return + global local_base_path + base_path = local_base_path + for platform in platforms: + sdk_parent_dir = os.path.join(base_path, f"{version}-{platform}") + sdk_dir = os.path.join(sdk_parent_dir, "TactilitySDK") + if not os.path.isdir(sdk_dir): + exit_with_error(f"Local SDK folder missing for {platform}: {sdk_dir}") + +def get_sdk_root_dir(version, platform): + global ttbuild_cdn + return os.path.join(ttbuild_path, f"{version}-{platform}") + +def get_sdk_url(version, platform): + global ttbuild_cdn + return f"{ttbuild_cdn}/TactilitySDK-{version}-{platform}.zip" + +def sdk_exists(version, platform): + sdk_dir = get_sdk_dir(version, platform) + return os.path.isdir(sdk_dir) + +def should_update_sdk_json(): + global ttbuild_cdn + json_filepath = os.path.join(ttbuild_path, "sdk.json") + if os.path.exists(json_filepath): + json_modification_time = os.path.getmtime(json_filepath) + now = time.time() + global ttbuild_sdk_json_validity + minimum_seconds_difference = ttbuild_sdk_json_validity + return (now - json_modification_time) > minimum_seconds_difference + else: + return True + +def update_sdk_json(): + global ttbuild_cdn, ttbuild_path + json_url = f"{ttbuild_cdn}/sdk.json" + json_filepath = os.path.join(ttbuild_path, "sdk.json") + return download_file(json_url, json_filepath) + +def should_fetch_sdkconfig_files(platform_targets): + for platform in platform_targets: + sdkconfig_filename = f"sdkconfig.app.{platform}" + if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)): + return True + return False + +def fetch_sdkconfig_files(platform_targets): + for platform in platform_targets: + sdkconfig_filename = f"sdkconfig.app.{platform}" + target_path = os.path.join(ttbuild_path, sdkconfig_filename) + if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path): + exit_with_error(f"Failed to download sdkconfig file for {platform}") + +#endregion SDK helpers + +#region Validation + +def validate_environment(): + if os.environ.get("IDF_PATH") is None: + if sys.platform == "win32": + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via %IDF_PATH%\\export.ps1") + else: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if not os.path.exists("manifest.properties"): + exit_with_error("manifest.properties not found") + if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: + print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") + print_warning("If you want to use it, use the '--local-sdk' parameter") + elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: + exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") + +def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build): + version_map = sdk_json["versions"] + if not sdk_version in version_map: + exit_with_error(f"Version not found: {sdk_version}") + version_data = version_map[sdk_version] + available_platforms = version_data["platforms"] + for desired_platform in platforms_to_build: + if not desired_platform in available_platforms: + exit_with_error(f"Platform {desired_platform} is not available. Available ones: {available_platforms}") + +def validate_self(sdk_json): + if not "toolVersion" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolVersion not found)") + if not "toolCompatibility" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolCompatibility not found)") + if not "toolDownloadUrl" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolDownloadUrl not found)") + tool_version = sdk_json["toolVersion"] + tool_compatibility = sdk_json["toolCompatibility"] + if tool_version != ttbuild_version: + print_warning(f"New version available: {tool_version} (currently using {ttbuild_version})") + print_warning(f"Run 'tactility.py updateself' to update.") + if re.search(tool_compatibility, ttbuild_version) is None: + print_error("The tool is not compatible anymore.") + print_error("Run 'tactility.py updateself' to update.") + sys.exit(1) + +#endregion Validation + +#region Manifest + +def read_manifest(): + return read_properties_file("manifest.properties") + +def validate_manifest(manifest): + # [manifest] + if not "manifest" in manifest: + exit_with_error("Invalid manifest format: [manifest] not found") + if not "version" in manifest["manifest"]: + exit_with_error("Invalid manifest format: [manifest] version not found") + # [target] + if not "target" in manifest: + exit_with_error("Invalid manifest format: [target] not found") + if not "sdk" in manifest["target"]: + exit_with_error("Invalid manifest format: [target] sdk not found") + if not "platforms" in manifest["target"]: + exit_with_error("Invalid manifest format: [target] platforms not found") + # [app] + if not "app" in manifest: + exit_with_error("Invalid manifest format: [app] not found") + if not "id" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] id not found") + if not "versionName" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] versionName not found") + if not "versionCode" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] versionCode not found") + if not "name" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] name not found") + +def is_valid_manifest_platform(manifest, platform): + manifest_platforms = manifest["target"]["platforms"].split(",") + return platform in manifest_platforms + +def validate_manifest_platform(manifest, platform): + if not is_valid_manifest_platform(manifest, platform): + exit_with_error(f"Platform {platform} is not available in the manifest.") + +def get_manifest_target_platforms(manifest, requested_platform): + if requested_platform == "" or requested_platform is None: + return manifest["target"]["platforms"].split(",") + else: + validate_manifest_platform(manifest, requested_platform) + return [requested_platform] + +#endregion Manifest + +#region SDK download + +def sdk_download(version, platform): + sdk_root_dir = get_sdk_root_dir(version, platform) + os.makedirs(sdk_root_dir, exist_ok=True) + sdk_url = get_sdk_url(version, platform) + filepath = os.path.join(sdk_root_dir, f"{version}-{platform}.zip") + print(f"Downloading SDK version {version} for {platform}") + if download_file(sdk_url, filepath): + with zipfile.ZipFile(filepath, "r") as zip_ref: + zip_ref.extractall(os.path.join(sdk_root_dir, "TactilitySDK")) + return True + else: + return False + +def sdk_download_all(version, platforms): + for platform in platforms: + if not sdk_exists(version, platform): + if not sdk_download(version, platform): + return False + else: + if verbose: + print(f"Using cached download for SDK version {version} and platform {platform}") + return True + +#endregion SDK download + +#region Building + +def get_cmake_path(platform): + return os.path.join("build", f"cmake-build-{platform}") + +def find_elf_file(platform): + cmake_dir = get_cmake_path(platform) + if os.path.exists(cmake_dir): + for file in os.listdir(cmake_dir): + if file.endswith(".app.elf"): + return os.path.join(cmake_dir, file) + return None + +def build_all(version, platforms, skip_build): + for platform in platforms: + # First build command must be "idf.py build", otherwise it fails to execute "idf.py elf" + # We check if the ELF file exists and run the correct command + # This can lead to code caching issues, so sometimes a clean build is required + if find_elf_file(platform) is None: + if not build_first(version, platform, skip_build): + return False + else: + if not build_consecutively(version, platform, skip_build): + return False + return True + +def wait_for_process(process): + buffer = [] + if sys.platform != "win32": + os.set_blocking(process.stdout.fileno(), False) + while process.poll() is None: + while True: + line = process.stdout.readline() + if line: + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break + else: + break + # Read any remaining output + for line in process.stdout: + decoded_line = line.decode("UTF-8") + if decoded_line: + buffer.append(decoded_line) + return buffer + +# The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. +# The problem is that the "idf.py build" always results in an error, even though the elf file is created. +# The solution is to suppress the error if we find that the elf file was created. +def build_first(version, platform, skip_build): + sdk_dir = get_sdk_dir(version, platform) + if verbose: + print(f"Using SDK at {sdk_dir}") + os.environ["TACTILITY_SDK_PATH"] = sdk_dir + sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") + shutil.copy(sdkconfig_path, "sdkconfig") + elf_path = find_elf_file(platform) + # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, + # as the actual build job will always fail due to technical issues with the elf cmake script + if elf_path is not None: + os.remove(elf_path) + if skip_build: + return True + print(f"Building first {platform} build") + cmake_path = get_cmake_path(platform) + print_status_busy(f"Building {platform} ELF") + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: + build_output = wait_for_process(process) + # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case + if process.returncode == 0: + print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") + return True + else: + if find_elf_file(platform) is None: + for line in build_output: + print(line, end="") + print_status_error(f"Building {platform} ELF") + return False + else: + print_status_success(f"Building {platform} ELF") + return True + +def build_consecutively(version, platform, skip_build): + sdk_dir = get_sdk_dir(version, platform) + if verbose: + print(f"Using SDK at {sdk_dir}") + os.environ["TACTILITY_SDK_PATH"] = sdk_dir + sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") + shutil.copy(sdkconfig_path, "sdkconfig") + if skip_build: + return True + cmake_path = get_cmake_path(platform) + print_status_busy(f"Building {platform} ELF") + shell_needed = sys.platform == "win32" + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell_needed) as process: + build_output = wait_for_process(process) + if process.returncode == 0: + print_status_success(f"Building {platform} ELF") + return True + else: + for line in build_output: + print(line, end="") + print_status_error(f"Building {platform} ELF") + return False + +#endregion Building + +#region Packaging + +def package_intermediate_manifest(target_path): + if not os.path.isfile("manifest.properties"): + print_error("manifest.properties not found") + return + shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties")) + +def package_intermediate_binaries(target_path, platforms): + elf_dir = os.path.join(target_path, "elf") + os.makedirs(elf_dir, exist_ok=True) + for platform in platforms: + elf_path = find_elf_file(platform) + if elf_path is None: + print_error(f"ELF file not found at {elf_path}") + return + shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf")) + +def package_intermediate_assets(target_path): + if os.path.isdir("assets"): + shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True) + +def package_intermediate(platforms): + target_path = os.path.join("build", "package-intermediate") + if os.path.isdir(target_path): + shutil.rmtree(target_path) + os.makedirs(target_path, exist_ok=True) + package_intermediate_manifest(target_path) + package_intermediate_binaries(target_path, platforms) + package_intermediate_assets(target_path) + +def package_name(platforms): + elf_path = find_elf_file(platforms[0]) + elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf") + return os.path.join("build", f"{elf_base_name}.app") + + +def package_all(platforms): + status = f"Building package with {platforms}" + print_status_busy(status) + package_intermediate(platforms) + # Create build/something.app + try: + tar_path = package_name(platforms) + tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT) + tar.add(os.path.join("build", "package-intermediate"), arcname="") + tar.close() + print_status_success(status) + return True + except Exception as e: + print_status_error(f"Building package failed: {e}") + return False + +#endregion Packaging + +def setup_environment(): + global ttbuild_path + os.makedirs(ttbuild_path, exist_ok=True) + +def build_action(manifest, platform_arg): + # Environment validation + validate_environment() + platforms_to_build = get_manifest_target_platforms(manifest, platform_arg) + + if use_local_sdk: + global local_base_path + local_base_path = os.environ.get("TACTILITY_SDK_PATH") + validate_local_sdks(platforms_to_build, manifest["target"]["sdk"]) + + if should_fetch_sdkconfig_files(platforms_to_build): + fetch_sdkconfig_files(platforms_to_build) + + if not use_local_sdk: + sdk_json = read_sdk_json() + validate_self(sdk_json) + if not "versions" in sdk_json: + exit_with_error("Version data not found in sdk.json") + # Build + sdk_version = manifest["target"]["sdk"] + if not use_local_sdk: + validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build) + if not sdk_download_all(sdk_version, platforms_to_build): + exit_with_error("Failed to download one or more SDKs") + if not build_all(sdk_version, platforms_to_build, skip_build): # Environment validation + return False + if not skip_build: + package_all(platforms_to_build) + return True + +def clean_action(): + if os.path.exists("build"): + print_status_busy("Removing build/") + shutil.rmtree("build") + print_status_success("Removed build/") + else: + print("Nothing to clean") + +def clear_cache_action(): + if os.path.exists(ttbuild_path): + print_status_busy(f"Removing {ttbuild_path}/") + shutil.rmtree(ttbuild_path) + print_status_success(f"Removed {ttbuild_path}/") + else: + print("Nothing to clear") + +def update_self_action(): + sdk_json = read_sdk_json() + tool_download_url = sdk_json["toolDownloadUrl"] + if download_file(tool_download_url, "tactility.py"): + print("Updated") + else: + exit_with_error("Update failed") + +def get_device_info(ip): + print_status_busy(f"Requesting device info") + url = get_url(ip, "/info") + try: + response = requests.get(url) + if response.status_code != 200: + print_error("Run failed") + else: + print_status_success(f"Received device info:") + print(response.json()) + except requests.RequestException as e: + print_status_error(f"Device info request failed: {e}") + +def run_action(manifest, ip): + app_id = manifest["app"]["id"] + print_status_busy("Running") + url = get_url(ip, "/app/run") + params = {'id': app_id} + try: + response = requests.post(url, params=params) + if response.status_code != 200: + print_error("Run failed") + else: + print_status_success("Running") + except requests.RequestException as e: + print_status_error(f"Running request failed: {e}") + +def install_action(ip, platforms): + print_status_busy("Installing") + for platform in platforms: + elf_path = find_elf_file(platform) + if elf_path is None: + print_status_error(f"ELF file not built for {platform}") + return False + package_path = package_name(platforms) + # print(f"Installing {package_path} to {ip}") + url = get_url(ip, "/app/install") + try: + # Prepare multipart form data + with open(package_path, 'rb') as file: + files = { + 'elf': file + } + response = requests.put(url, files=files) + if response.status_code != 200: + print_status_error("Install failed") + return False + else: + print_status_success("Installing") + return True + except requests.RequestException as e: + print_status_error(f"Install request failed: {e}") + return False + except IOError as e: + print_status_error(f"Install file error: {e}") + return False + +def uninstall_action(manifest, ip): + app_id = manifest["app"]["id"] + print_status_busy("Uninstalling") + url = get_url(ip, "/app/uninstall") + params = {'id': app_id} + try: + response = requests.put(url, params=params) + if response.status_code != 200: + print_status_error("Server responded that uninstall failed") + else: + print_status_success("Uninstalled") + except requests.RequestException as e: + print_status_success(f"Uninstall request failed: {e}") + +#region Main + +if __name__ == "__main__": + print(f"Tactility Build System v{ttbuild_version}") + if "--help" in sys.argv: + print_help() + sys.exit() + # Argument validation + if len(sys.argv) == 1: + print_help() + sys.exit() + if "--verbose" in sys.argv: + verbose = True + sys.argv.remove("--verbose") + skip_build = False + if "--skip-build" in sys.argv: + skip_build = True + sys.argv.remove("--skip-build") + if "--local-sdk" in sys.argv: + use_local_sdk = True + sys.argv.remove("--local-sdk") + action_arg = sys.argv[1] + + # Environment setup + setup_environment() + if not os.path.isfile("manifest.properties"): + exit_with_error("manifest.properties not found") + manifest = read_manifest() + validate_manifest(manifest) + all_platform_targets = manifest["target"]["platforms"].split(",") + # Update SDK cache (sdk.json) + if should_update_sdk_json() and not update_sdk_json(): + exit_with_error("Failed to retrieve SDK info") + # Actions + if action_arg == "build": + if len(sys.argv) < 2: + print_help() + exit_with_error("Commandline parameter missing") + platform = None + if len(sys.argv) > 2: + platform = sys.argv[2] + build_action(manifest, platform) + elif action_arg == "clean": + clean_action() + elif action_arg == "clearcache": + clear_cache_action() + elif action_arg == "updateself": + update_self_action() + elif action_arg == "run": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + run_action(manifest, sys.argv[2]) + elif action_arg == "install": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + platform = None + platforms_to_install = all_platform_targets + if len(sys.argv) >= 4: + platform = sys.argv[3] + platforms_to_install = [platform] + install_action(sys.argv[2], platforms_to_install) + elif action_arg == "uninstall": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + uninstall_action(manifest, sys.argv[2]) + elif action_arg == "bir" or action_arg == "brrr": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + platform = None + platforms_to_install = all_platform_targets + if len(sys.argv) >= 4: + platform = sys.argv[3] + platforms_to_install = [platform] + if build_action(manifest, platform): + if install_action(sys.argv[2], platforms_to_install): + run_action(manifest, sys.argv[2]) + else: + print_help() + exit_with_error("Unknown commandline parameter") + +#endregion Main