300 lines
9.6 KiB
Python
300 lines
9.6 KiB
Python
#!/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 <Arduino.h>\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() |