#!/usr/bin/env python3 """ Bell Melody to C Header Converter Converts human-readable bell notation to C header file with PROGMEM arrays Input Format: MELODY_NAME: 1,2,3+4,0,5+6+7 Output: melodies.h (C header with const uint16_t PROGMEM arrays) """ import sys import re from pathlib import Path from typing import List, Tuple from datetime import datetime def parse_bell_notation(notation: str) -> int: """ Convert human-readable bell notation to bit flag value Examples: "2+8" → bells 2,8 → bits 1,7 → 0x0082 (130) "4" → bell 4 → bit 3 → 0x0008 (8) "1+2+3" → bells 1,2,3 → bits 0,1,2 → 0x0007 (7) "0" → no bells → 0x0000 (0) Formula: Bell #N → Bit (N-1) → Value = 1 << (N-1) """ notation = notation.strip() # Handle zero/silence if notation == '0' or not notation: return 0 # Split by + to get individual bell numbers bell_numbers = notation.split('+') value = 0 for bell_str in bell_numbers: try: bell_num = int(bell_str.strip()) if bell_num == 0: continue # Bell 0 means silence, contributes nothing if bell_num < 1 or bell_num > 16: print(f"Warning: Bell number {bell_num} out of range (1-16), skipping") continue # Convert bell number to bit position (1-indexed to 0-indexed) bit_position = bell_num - 1 bit_value = 1 << bit_position value |= bit_value except ValueError: print(f"Warning: Invalid bell number '{bell_str}', skipping") continue return value def parse_melody_line(line: str) -> Tuple[str, List[int]]: """ Parse a melody line in format: MELODY_NAME: step,step,step Returns: (melody_name, list of uint16_t values) """ line = line.strip() if not line or line.startswith('#'): return None, [] # Split by colon if ':' not in line: return None, [] parts = line.split(':', 1) melody_name = parts[0].strip() steps_str = parts[1].strip() if not melody_name: return None, [] # Parse steps (comma-separated) step_strings = steps_str.split(',') values = [] for step_str in step_strings: value = parse_bell_notation(step_str) values.append(value) return melody_name, values def format_melody_array(melody_name: str, values: List[int], values_per_line: int = 8) -> str: """ Format melody values as C PROGMEM array Args: melody_name: Name of the melody (will be prefixed with "builtin_") values: List of uint16_t values values_per_line: Number of hex values per line Returns: Formatted C array declaration """ array_name = f"melody_builtin_{melody_name.lower()}" lines = [] lines.append(f"const uint16_t PROGMEM {array_name}[] = {{") # Format values in rows for i in range(0, len(values), values_per_line): chunk = values[i:i + values_per_line] hex_values = [f"0x{val:04X}" for val in chunk] # Add comma after each value except the last one overall if i + len(chunk) < len(values): line = " " + ", ".join(hex_values) + "," else: line = " " + ", ".join(hex_values) lines.append(line) lines.append("};") return "\n".join(lines) def format_melody_info_entry(melody_name: str, display_name: str, array_size: int) -> str: """ Format a single MelodyInfo struct entry Args: melody_name: Technical name (will be prefixed with "builtin_") display_name: Human-readable name array_size: Number of elements in the melody array Returns: Formatted struct entry """ array_name = f"melody_builtin_{melody_name.lower()}" id_name = f"builtin_{melody_name.lower()}" return f""" {{ "{display_name}", "{id_name}", {array_name}, sizeof({array_name}) / sizeof(uint16_t) }}""" def convert_to_header(input_path: str, output_path: str = "melodies.h"): """ Convert multi-melody file to C header file Args: input_path: Path to input text file output_path: Path to output .h file """ melodies = [] # List of (name, display_name, values) try: with open(input_path, 'r', encoding='utf-8') as f: lines = f.readlines() print(f"Reading from: {input_path}") print(f"Output file: {output_path}\n") # Parse all melodies for line_num, line in enumerate(lines, 1): melody_name, values = parse_melody_line(line) if melody_name and values: # Create display name (convert underscores to spaces, title case) display_name = melody_name.replace('_', ' ').title() melodies.append((melody_name, display_name, values)) print(f"✓ Parsed: {display_name} ({len(values)} steps)") if not melodies: print("Error: No valid melodies found in input file") return False # Generate header file print(f"\n{'='*50}") print(f"Generating C header file...\n") with open(output_path, 'w', encoding='utf-8') as f: # Header guard and comments f.write("/*\n") f.write(" * Bell Melodies - Auto-generated\n") f.write(f" * Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f" * Source: {input_path}\n") f.write(" * \n") f.write(" * This file contains built-in melody definitions for the ESP32 bell controller\n") f.write(" */\n\n") f.write("#ifndef MELODIES_H\n") f.write("#define MELODIES_H\n\n") f.write("#include \n\n") # Write melody arrays f.write("// ========================================\n") f.write("// Melody Data Arrays\n") f.write("// ========================================\n\n") for melody_name, display_name, values in melodies: f.write(f"// {display_name}\n") f.write(format_melody_array(melody_name, values)) f.write("\n\n") # Write MelodyInfo structure definition f.write("// ========================================\n") f.write("// Melody Information Structure\n") f.write("// ========================================\n\n") f.write("struct MelodyInfo {\n") f.write(" const char* display_name;\n") f.write(" const char* id;\n") f.write(" const uint16_t* data;\n") f.write(" size_t length;\n") f.write("};\n\n") # Write melody library array f.write("// ========================================\n") f.write("// Melody Library\n") f.write("// ========================================\n\n") f.write("const MelodyInfo MELODY_LIBRARY[] = {\n") for i, (melody_name, display_name, values) in enumerate(melodies): entry = format_melody_info_entry(melody_name, display_name, len(values)) # Add comma except for last entry if i < len(melodies) - 1: f.write(entry + ",\n") else: f.write(entry + "\n") f.write("};\n\n") # Add library size constant f.write(f"const size_t MELODY_LIBRARY_SIZE = {len(melodies)};\n\n") # Close header guard f.write("#endif // MELODIES_H\n") # Summary print(f"✓ Successfully created {output_path}") print(f" Melodies: {len(melodies)}") total_steps = sum(len(values) for _, _, values in melodies) print(f" Total steps: {total_steps}") print(f" Estimated PROGMEM usage: {total_steps * 2} bytes") print(f"\n{'='*50}") print("Done! Include this file in your ESP32 project.") return True except FileNotFoundError: print(f"Error: Input file '{input_path}' not found") return False except Exception as e: print(f"Error: {e}") import traceback traceback.print_exc() return False def main(): """Main entry point""" print("=== Bell Melody to C Header Converter ===") print("Creates melodies.h for ESP32 firmware\n") # Default input file input_file = "builtin_melodies.txt" output_file = "melodies.h" # Check if file exists if not Path(input_file).exists(): print(f"Error: '{input_file}' not found in current directory!") print("\nPlease create 'builtin_melodies.txt' with format:") print(" MELODY_NAME: step,step,step,...") print("\nStep notation:") print(" 0 - Silence") print(" 4 - Bell #4 only") print(" 2+8 - Bells #2 and #8 together") print(" 1+2+3 - Bells #1, #2, and #3 together") print("\nExample:") print(" JINGLE_BELLS: 4,4,4,0,4,4,4,0,4,8,1,2,4") print(" ALARM: 2+8,0,2+8,0,2+8,0") print(" HAPPY_BIRTHDAY: 1,1,2,1,4,3,0,1,1,2,1,8,4") sys.exit(1) success = convert_to_header(input_file, output_file) sys.exit(0 if success else 1) if __name__ == "__main__": main()