#!/usr/bin/env python3 """ LVGL Header Preprocessor Extracts function signatures and enums from LVGL header files. Generates mapping files for Berry scripting integration. """ import re import sys import glob import argparse from pathlib import Path from typing import List, Set, Tuple, Optional import logging # Configure logging logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') logger = logging.getLogger(__name__) class LVGLPreprocessor: """Main preprocessor class for LVGL headers.""" def __init__(self, lv_src_prefix: str = "../../lvgl/src/"): self.lv_src_prefix = Path(lv_src_prefix) self.headers_exclude_suffix = { "_private.h", "lv_lottie.h", "lv_obj_property.h", "lv_obj_property_names.h", "lv_style_properties.h", "lv_3dtexture.h", } # Function exclusion patterns self.function_exclude_patterns = [ r"^_", # skip if function name starts with '_' r"^lv_debug", # all debug functions r"^lv_init", r"^lv_deinit", r"^lv_templ_", r"^lv_imagebutton_get_src_", # LV_IMGBTN_TILED == 0 r"^lv_imagebitton_set_src_tiled", # !LV_IMGBTN_TILED r"^lv_refr_get_fps_", # no LV_USE_PERF_MONITOR r"^lv_image_cache_", r"^lv_image_decoder_", r"^lv_image_cf_", r"^lv_image_buf_", r"^lv_indev_scroll_", r"^lv_pow", r"^lv_keyboard_def_event_cb", # need to fix conditional include r"^lv_refr_reset_fps_counter", r"^lv_refr_get_fps_avg", r"^lv_anim_path_", # callbacks for animation are moved to constants r"^lv_obj_set_property", # LV_USE_OBJ_PROPERTY 0 r"^lv_obj_set_properties", r"^lv_obj_get_property", r"^lv_win_", r"^lv_obj.*name", # we don't enable #if LV_USE_OBJ_NAME r".*_bind_.*", # 9.4.0 remove observer methods r".*_get_.*_by_name", # 9.4.0 r".*_translation_", # 9.4.0 ] # Enum exclusion patterns self.enum_exclude_prefixes = { "_", "LV_BIDI_DIR_", "LV_FONT_", "LV_SIGNAL_", "LV_TEMPL_", "LV_TASK_PRIO_", "LV_THEME_", "LV_LRU_", "LV_VECTOR_", "LV_KEYBOARD_MODE_TEXT_ARABIC", "LV_DRAW_TASK_TYPE_3D", "LV_DRAW_TASK_TYPE_VECTOR", "LV_EVENT_TRANSLATION_" } def comment_remover(self, text: str) -> str: """Remove C/C++ style comments from source code.""" def replacer(match): s = match.group(0) return " " if s.startswith('/') else s pattern = re.compile( r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE ) return re.sub(pattern, replacer, text) def list_files(self, prefix: Path, glob_patterns: List[str]) -> List[Path]: """Compute a sorted list of files from a prefix and glob patterns.""" files = [] for pattern in glob_patterns: files.extend(Path(prefix).glob(pattern)) return sorted(files) def clean_source(self, raw: str) -> str: """Clean source code by removing comments, preprocessor directives, etc.""" raw = self.comment_remover(raw) # Normalize line endings raw = re.sub(r'\r\n', '\n', raw) raw = re.sub(r'\r', '\n', raw) # Handle line continuations raw = re.sub(r'\\\n', ' ', raw) # Remove preprocessor directives raw = re.sub(r'\n[ \t]*#[^\n]*(?=\n)', '', raw) raw = re.sub(r'^[ \t]*#[^\n]*\n', '', raw) raw = re.sub(r'\n[ \t]*#[^\n]*$', '', raw) # Remove extern "C" blocks raw = re.sub(r'extern\s+"C"\s+{(.*)}', r'\1', raw, flags=re.DOTALL) # Remove empty lines raw = re.sub(r'\n[ \t]*(?=\n)', '', raw) raw = re.sub(r'^[ \t]*\n', '', raw) raw = re.sub(r'\n[ \t]*$', '', raw) return raw def extract_functions(self, source: str) -> List[str]: """Extract function signatures from cleaned source code.""" # Remove content within braces while True: source, repl_count = re.subn(r'\{[^{]*?\}', ';', source, flags=re.DOTALL) if repl_count == 0: break # Find function signatures pattern = r'(^|;|})\s*([^;{}]+\(.*?\))\s*(?=(;|{))' matches = re.findall(pattern, source, flags=re.DOTALL) functions = [] for match in matches: func_def = match[1] # Clean up whitespace func_def = re.sub(r'[ \t\r\n]+', ' ', func_def) # Remove LVGL-specific attributes func_def = re.sub(r'LV_ATTRIBUTE_FAST_MEM ', '', func_def) func_def = re.sub(r'LV_ATTRIBUTE_TIMER_HANDLER ', '', func_def) func_def = re.sub(r'extern ', '', func_def) # Skip excluded function types if any(func_def.startswith(prefix) for prefix in ["typedef", "_LV_", "LV_"]): continue # Extract function name name_match = re.search(r'\s(\w+)\([^\(]*$', func_def) if not name_match: continue func_name = name_match.group(1) # Check exclusion patterns if any(re.search(pattern, func_name) for pattern in self.function_exclude_patterns): continue functions.append(func_def) return functions def extract_enums(self, source: str) -> Set[str]: """Extract enum values from cleaned source code.""" enum_values = set() # Find enum definitions enum_matches = re.findall(r'enum\s+\w*\s*{(.*?)}', source, flags=re.DOTALL) for enum_content in enum_matches: # Skip LV_PROPERTY_ID enums (disabled feature) if 'LV_PROPERTY_ID' in enum_content: continue # Remove macro-defined enums enum_content = re.sub(r'\S+\((.*?),.*?\),', r'\1,', enum_content) # Split by commas and clean up for item in enum_content.split(','): item = re.sub(r'[ \t\n]', '', item) # Remove whitespace item = re.sub(r'=.*$', '', item) # Remove assignment if not item: # Skip empty items continue # Check exclusion patterns if any(item.startswith(prefix) for prefix in self.enum_exclude_prefixes): continue enum_values.add(item) # Extract LV_EXPORT_CONST_INT constants const_ints = re.findall(r'LV_EXPORT_CONST_INT\((\w+)\)', source, flags=re.DOTALL) enum_values.update(const_ints) return enum_values def get_function_headers(self) -> List[Path]: """Get list of header files for function extraction.""" patterns = [ "lv_api*.h", "widgets/*/*.h", "libs/qrcode/lv_qrcode.h", "core/*.h", "indev/lv_indev.h", "layouts/*/*.h", "themes/lv_theme.h", "draw/lv_draw_arc.h", "draw/lv_draw_label.h", "draw/lv_draw_line.h", "draw/lv_draw_mask.h", "draw/lv_draw_rect.h", "draw/lv_draw_triangle.h", "draw/lv_draw.h", "display/*.h", "misc/lv_anim.h", "misc/lv_area.h", "misc/lv_color.h", "misc/lv_color_op.h", "misc/lv_palette.h", "misc/lv_event.h", "misc/lv_style_gen.h", "misc/lv_style.h", "misc/lv_timer.h", "misc/lv_text.h", "font/lv_font.h", "../lvgl.h", ] headers = self.list_files(self.lv_src_prefix, patterns) # Add additional headers additional_paths = [ Path("../../LVGL_assets/src/lv_theme_haspmota.h"), Path("../src/lv_berry.h"), Path("../src/lv_colorwheel.h"), ] for path in additional_paths: if path.exists(): headers.append(path) # Filter out excluded files return [h for h in headers if not any(str(h).endswith(suffix) for suffix in self.headers_exclude_suffix)] def get_enum_headers(self) -> List[Path]: """Get list of header files for enum extraction.""" patterns = [ "core/*.h", "draw/*.h", "hal/*.h", "misc/*.h", "widgets/*/*.h", "display/lv_display.h", "layouts/**/*.h", ] headers = self.list_files(self.lv_src_prefix, patterns) return [h for h in headers if not any(str(h).endswith(suffix) for suffix in self.headers_exclude_suffix)] def generate_functions_header(self, output_path: Path): """Generate the functions header file.""" logger.info(f"Generating functions header: {output_path}") headers = self.get_function_headers() with open(output_path, 'w', encoding='utf-8') as f: f.write(""" // Automatically generated from LVGL source with `python3 preprocessor.py` // Extract function signatures from LVGL APIs in headers // Custom Tasmota functions lv_ts_calibration_t * lv_get_ts_calibration(void); // ====================================================================== // LV top level functions // ====================================================================== // resolution lv_coord_t lv_get_hor_res(void); lv_coord_t lv_get_ver_res(void); // ====================================================================== // Generated from headers // ====================================================================== """) for header_path in headers: try: with open(header_path, encoding='utf-8-sig') as header_file: f.write(f"// {header_path}\n") raw_content = self.clean_source(header_file.read()) functions = self.extract_functions(raw_content) for func in functions: f.write(f"{func}\n") f.write("\n") except Exception as e: logger.error(f"Error processing {header_path}: {e}") def generate_enums_header(self, output_path: Path): """Generate the enums header file.""" logger.info(f"Generating enums header: {output_path}") headers = self.get_enum_headers() with open(output_path, 'w', encoding='utf-8') as f: # Write the static content first f.write(self._get_static_enum_content()) # Process headers for dynamic enums for header_path in headers: try: with open(header_path, encoding='utf-8-sig') as header_file: f.write(f"// File: {header_path}\n") raw_content = self.clean_source(header_file.read()) enum_values = self.extract_enums(raw_content) for enum_value in sorted(enum_values): f.write(f"{enum_value}\n") f.write("\n") except Exception as e: logger.error(f"Error processing {header_path}: {e}") def _get_static_enum_content(self) -> str: """Get the static content for enum header.""" return """// ====================================================================== // Functions // ====================================================================== load_font=@lv0_load_font // lv_anim_path_functions anim_path_bounce=&lv_anim_path_bounce anim_path_ease_in=&lv_anim_path_ease_in anim_path_ease_in_out=&lv_anim_path_ease_in_out anim_path_ease_out=&lv_anim_path_ease_out anim_path_linear=&lv_anim_path_linear anim_path_overshoot=&lv_anim_path_overshoot anim_path_step=&lv_anim_path_step LV_LAYOUT_GRID=>be_LV_LAYOUT_GRID LV_LAYOUT_FLEX=>be_LV_LAYOUT_FLEX // ====================================================================== // Colors // ====================================================================== // LV Colors - we store in 24 bits format and will convert at runtime // This is specific treatment because we keep colors in 24 bits format COLOR_WHITE=0xFFFFFF COLOR_SILVER=0xC0C0C0 COLOR_GRAY=0x808080 COLOR_GREY=0x808080 // OpenHASP COLOR_BLACK=0x000000 COLOR_RED=0xFF0000 COLOR_MAROON=0x800000 COLOR_YELLOW=0xFFFF00 COLOR_OLIVE=0x808000 COLOR_LIME=0x00FF00 COLOR_GREEN=0x008000 COLOR_CYAN=0x00FFFF COLOR_AQUA=0x00FFFF COLOR_TEAL=0x008080 COLOR_BLUE=0x0000FF COLOR_NAVY=0x000080 COLOR_MAGENTA=0xFF00FF COLOR_FUCHSIA=0xFF00FF // OpenHASP COLOR_ORANGE=0xFFA500 // OpenHASP COLOR_PURPLE=0x800080 // Below are OpenHASP additions COLOR_PERU=0xCD853F COLOR_SIENNA=0xA0522D COLOR_BROWN=0xA52A2A COLOR_SNOW=0xFFFAFA COLOR_IVORY=0xFFFFF0 COLOR_LINEN=0xFAF0E6 COLOR_BEIGE=0xF5F5DC COLOR_AZURE=0xF0FFFF COLOR_PINK=0xFFC0CB COLOR_PLUM=0xDDA0DD COLOR_ORCHID=0xDA70D6 COLOR_VIOLET=0xEE82EE COLOR_INDIGO=0x4B0082 COLOR_BLUSH=0xB00000 COLOR_TOMATO=0xFF6347 COLOR_SALMON=0xFA8072 COLOR_CORAL=0xFF7F50 COLOR_GOLD=0xFFD700 COLOR_KHAKI=0xF0E68C COLOR_BISQUE=0xFFE4C4 COLOR_WHEAT=0xF5DEB3 COLOR_TAN=0xD2B48C // Freetype FT_FONT_STYLE_NORMAL=FT_FONT_STYLE_NORMAL FT_FONT_STYLE_ITALIC=FT_FONT_STYLE_ITALIC FT_FONT_STYLE_BOLD=FT_FONT_STYLE_BOLD // following are #define, not enum LV_GRID_FR=LV_GRID_FR(0) // ====================================================================== // Symbols // ====================================================================== SYMBOL_AUDIO="\\xef\\x80\\x81" SYMBOL_VIDEO="\\xef\\x80\\x88" SYMBOL_LIST="\\xef\\x80\\x8b" SYMBOL_OK="\\xef\\x80\\x8c" SYMBOL_CLOSE="\\xef\\x80\\x8d" SYMBOL_POWER="\\xef\\x80\\x91" SYMBOL_SETTINGS="\\xef\\x80\\x93" SYMBOL_HOME="\\xef\\x80\\x95" SYMBOL_DOWNLOAD="\\xef\\x80\\x99" SYMBOL_DRIVE="\\xef\\x80\\x9c" SYMBOL_REFRESH="\\xef\\x80\\xa1" SYMBOL_MUTE="\\xef\\x80\\xa6" SYMBOL_VOLUME_MID="\\xef\\x80\\xa7" SYMBOL_VOLUME_MAX="\\xef\\x80\\xa8" SYMBOL_IMAGE="\\xef\\x80\\xbe" SYMBOL_EDIT="\\xef\\x8C\\x84" SYMBOL_PREV="\\xef\\x81\\x88" SYMBOL_PLAY="\\xef\\x81\\x8b" SYMBOL_PAUSE="\\xef\\x81\\x8c" SYMBOL_STOP="\\xef\\x81\\x8d" SYMBOL_NEXT="\\xef\\x81\\x91" SYMBOL_EJECT="\\xef\\x81\\x92" SYMBOL_LEFT="\\xef\\x81\\x93" SYMBOL_RIGHT="\\xef\\x81\\x94" SYMBOL_PLUS="\\xef\\x81\\xa7" SYMBOL_MINUS="\\xef\\x81\\xa8" SYMBOL_EYE_OPEN="\\xef\\x81\\xae" SYMBOL_EYE_CLOSE="\\xef\\x81\\xb0" SYMBOL_WARNING="\\xef\\x81\\xb1" SYMBOL_SHUFFLE="\\xef\\x81\\xb4" SYMBOL_UP="\\xef\\x81\\xb7" SYMBOL_DOWN="\\xef\\x81\\xb8" SYMBOL_LOOP="\\xef\\x81\\xb9" SYMBOL_DIRECTORY="\\xef\\x81\\xbb" SYMBOL_UPLOAD="\\xef\\x82\\x93" SYMBOL_CALL="\\xef\\x82\\x95" SYMBOL_CUT="\\xef\\x83\\x84" SYMBOL_COPY="\\xef\\x83\\x85" SYMBOL_SAVE="\\xef\\x83\\x87" SYMBOL_CHARGE="\\xef\\x83\\xa7" SYMBOL_PASTE="\\xef\\x83\\xAA" SYMBOL_BELL="\\xef\\x83\\xb3" SYMBOL_KEYBOARD="\\xef\\x84\\x9c" SYMBOL_GPS="\\xef\\x84\\xa4" SYMBOL_FILE="\\xef\\x85\\x9b" SYMBOL_WIFI="\\xef\\x87\\xab" SYMBOL_BATTERY_FULL="\\xef\\x89\\x80" SYMBOL_BATTERY_3="\\xef\\x89\\x81" SYMBOL_BATTERY_2="\\xef\\x89\\x82" SYMBOL_BATTERY_1="\\xef\\x89\\x83" SYMBOL_BATTERY_EMPTY="\\xef\\x89\\x84" SYMBOL_USB="\\xef\\x8a\\x87" SYMBOL_BLUETOOTH="\\xef\\x8a\\x93" SYMBOL_TRASH="\\xef\\x8B\\xAD" SYMBOL_BACKSPACE="\\xef\\x95\\x9A" SYMBOL_SD_CARD="\\xef\\x9F\\x82" SYMBOL_NEW_LINE="\\xef\\xA2\\xA2" SYMBOL_DUMMY="\\xEF\\xA3\\xBF" SYMBOL_BULLET="\\xE2\\x80\\xA2" // LVGL 8 to 9 compatibility LV_DISP_ROTATION_0=LV_DISPLAY_ROTATION_0 LV_DISP_ROTATION_90=LV_DISPLAY_ROTATION_90 LV_DISP_ROTATION_180=LV_DISPLAY_ROTATION_180 LV_DISP_ROTATION_270=LV_DISPLAY_ROTATION_270 LV_DISP_RENDER_MODE_PARTIAL=LV_DISPLAY_RENDER_MODE_PARTIAL LV_DISP_RENDER_MODE_DIRECT=LV_DISPLAY_RENDER_MODE_DIRECT LV_DISP_RENDER_MODE_FULL=LV_DISPLAY_RENDER_MODE_FULL LV_BTNMATRIX_BTN_NONE=LV_BUTTONMATRIX_BUTTON_NONE LV_BTNMATRIX_CTRL_HIDDEN=LV_BUTTONMATRIX_CTRL_HIDDEN LV_BTNMATRIX_CTRL_NO_REPEAT=LV_BUTTONMATRIX_CTRL_NO_REPEAT LV_BTNMATRIX_CTRL_DISABLED=LV_BUTTONMATRIX_CTRL_DISABLED LV_BTNMATRIX_CTRL_CHECKABLE=LV_BUTTONMATRIX_CTRL_CHECKABLE LV_BTNMATRIX_CTRL_CHECKED=LV_BUTTONMATRIX_CTRL_CHECKED LV_BTNMATRIX_CTRL_CLICK_TRIG=LV_BUTTONMATRIX_CTRL_CLICK_TRIG LV_BTNMATRIX_CTRL_POPOVER=LV_BUTTONMATRIX_CTRL_POPOVER LV_BTNMATRIX_CTRL_CUSTOM_1=LV_BUTTONMATRIX_CTRL_CUSTOM_1 LV_BTNMATRIX_CTRL_CUSTOM_2=LV_BUTTONMATRIX_CTRL_CUSTOM_2 LV_RES_OK=LV_RESULT_OK LV_RES_INV=LV_RESULT_INVALID LV_INDEV_STATE_PR=LV_INDEV_STATE_PRESSED LV_INDEV_STATE_REL=LV_INDEV_STATE_RELEASED LV_STYLE_ANIM_TIME=LV_STYLE_ANIM_DURATION LV_STYLE_IMG_OPA=LV_STYLE_IMAGE_OPA LV_STYLE_IMG_RECOLOR=LV_STYLE_IMAGE_RECOLOR LV_STYLE_IMG_RECOLOR_OPA=LV_STYLE_IMAGE_RECOLOR_OPA LV_STYLE_SHADOW_OFS_X=LV_STYLE_SHADOW_OFFSET_X LV_STYLE_SHADOW_OFS_Y=LV_STYLE_SHADOW_OFFSET_Y LV_STYLE_TRANSFORM_ANGLE=LV_STYLE_TRANSFORM_ROTATION LV_ZOOM_NONE=LV_SCALE_NONE // LVGL 9.3 LV_LABEL_LONG_WRAP=LV_LABEL_LONG_MODE_WRAP LV_LABEL_LONG_DOT=LV_LABEL_LONG_MODE_DOTS LV_LABEL_LONG_SCROLL=LV_LABEL_LONG_MODE_SCROLL LV_LABEL_LONG_SCROLL_CIRCULAR=LV_LABEL_LONG_MODE_SCROLL_CIRCULAR LV_LABEL_LONG_CLIP=LV_LABEL_LONG_MODE_CLIP LV_ANIM_OFF=LV_ANIM_OFF LV_ANIM_ON=LV_ANIM_ON // ====================================================================== // Generated from headers // ====================================================================== """ def run(self, functions_output: str = "../mapping/lv_funcs.h", enums_output: str = "../mapping/lv_enum.h"): """Run the complete preprocessing pipeline.""" functions_path = Path(functions_output) enums_path = Path(enums_output) # Create output directories if they don't exist functions_path.parent.mkdir(parents=True, exist_ok=True) enums_path.parent.mkdir(parents=True, exist_ok=True) # Generate both files self.generate_functions_header(functions_path) self.generate_enums_header(enums_path) logger.info("Preprocessing complete!") def main(): """Main entry point with command line argument parsing.""" parser = argparse.ArgumentParser(description="LVGL Header Preprocessor") parser.add_argument("--lv-src", default="../../lvgl/src/", help="Path to LVGL source directory") parser.add_argument("--functions-output", default="../mapping/lv_funcs.h", help="Output path for functions header") parser.add_argument("--enums-output", default="../mapping/lv_enum.h", help="Output path for enums header") parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging") args = parser.parse_args() if args.verbose: logging.getLogger().setLevel(logging.DEBUG) try: preprocessor = LVGLPreprocessor(args.lv_src) preprocessor.run(args.functions_output, args.enums_output) except Exception as e: logger.error(f"Preprocessing failed: {e}") sys.exit(1) if __name__ == "__main__": main()