First Production Push

This commit is contained in:
2026-02-25 21:29:56 +02:00
parent e62cffc10c
commit 8cb639c1bd
32 changed files with 3714 additions and 2719 deletions

View File

@@ -17,3 +17,7 @@ MOSQUITTO_PASSWORD_FILE=/etc/mosquitto/passwd
# App
BACKEND_CORS_ORIGINS=["http://localhost:5173"]
DEBUG=true
# Local file storage (override if you want to store data elsewhere)
SQLITE_DB_PATH=./mqtt_data.db
BUILT_MELODIES_STORAGE_PATH=./storage/built_melodies

8
.gitignore vendored
View File

@@ -2,6 +2,11 @@
.env
firebase-service-account.json
# Persistent runtime data (lives outside docker, not in git)
/data/*
!/data/.gitkeep
!/data/built_melodies/.gitkeep
# Python
__pycache__/
*.pyc
@@ -20,5 +25,4 @@ dist/
.DS_Store
Thumbs.db
MAIN-APP-REFERENCE/
SecondaryApps/
.MAIN-APP-REFERENCE/

View File

@@ -1 +0,0 @@
Esperinos-Adamn-1k: 1,0,1,0,1,0,0,0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0

View File

@@ -1,26 +0,0 @@
Doxology_Traditional: 1,2,1,2,1,3,0,0,1,2,1,2,1,4,0,0,1,2,1,2,1,2,1,2,1,2,1,3,1,4,0,0
Doxology_Alternative: 1,0,2,3,0,4+5,0,1,0,2,3,0,4+5,0,1,0,2,3,0,4+5,0,1,2,1,2,3,0,4+5,0
Doxology_Festive: 2,3,4+1,3,2,3,5+1,3,2,3,6+1,3,2,3,5+1,3
Vesper_Traditional: 1,2,3,0,1,2,3,0,1,2,1,2,1,2,3,0
Vesper_Alternative: 1,2,0,0,1,2,0,0,1,3,0,0,0,0,0,0,1,2,0,0,1,2,0,0,1,4,0,0,0,0,0,0,1,2,0,0,1,2,0,0,1,2,0,0,1,2,0,0,1,2,0,0,1,3,0,0,1,4,0,0,0,0,0,0
Catehetical: 1,2,3,4,5
Orthros_Traditional: 1,0,2,0,3,4,0,5,0,6,0,7,8,0
Orthros_Alternative: 1,0,2,1,0,2,0,1,0,1,2,1,0,3,0
Mournfull_Toll: 1,0,0,0,0,0,0,1,0,0,0,0,0,0,3,0,0,0,0,0,0,3,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,4,0,0,0,0,0,0,4,0,0,0,0,0,0
Mournfull_Toll_Alternative: 1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,3,0,0,0,0,0,0,3,3,0,0,0,0,0,0,2,0,0,0,0,0,0,2,2,0,0,0,0,0,0,4,0,0,0,0,0,0,4,4,0,0,0,0,0,0
Mournfull_Toll_Meg_Par: 1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,3,0,0,0,0,0,0,3,0,0,0,0,0,0,3,3,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,2,2,0,0,0,0,0,0,4,0,0,0,0,0,0,4,0,0,0,0,0,0,4,4,0,0,0,0,0,0
Sematron: 1,1,1,2,1,1,1,4,1,1,1,2,1,3,1,4
Sematron_Alternative: 1,1,1,2,1,1,1,4,1,2,1,3,1,1,1,4
Athonite_1_2_Voices: 1,2,1,1,2,1,1,2,1,1,2,1,2
Athonite_3_Voices: 2,1,0,0,2,1,0,0,2,1,2,1,2,1,0,1,2,1,0,1,2,1,0,1,2,1,2,1,2,1,0,1,2,1,0,1,2,1,0,1,2,1,2,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,2,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,2,1,2,1,3
Athonite_3_4_Voices: 2,1,0,0,2,1,0,0,2,1,2,1,2,1,0,1,2,1,0,1,2,1,0,1,2,1,2,1,2,1,0,1,2,1,0,1,2,1,0,1,2,1,2,1,2,1,3,1,2,1,0,3+1,2,1,0,3+1,2,1,0,3+1,2,1,2,3+1,2,1,4,3+1,2,1,0,3+1,2,1,0,3+1,2,1,2,3+1,2,1,4+1,2,1,3+1,2,1,4+2,2,1,3+2,2,1,4+1,2,1,3+1,2,1,4+2,2,1,3+2,2,1,4+1
Athonite_4_8_Voices: 2,1,0,0,2,1,0,0,2,1,2,1,2,1,0,1,2,1,0,1,2,1,0,1,2,1,2,1,2,1,0,1,2,1,0,1,2,1,0,1,2,1,2,1,2,1,3,1,2,1,0,3+1,2,1,0,3+1,2,1,0,3+1,2,1,2,3+1,2,1,4,3+1,2,1,0,3+1,2,1,0,3+1,2,1,2,3+1,2,1,4+1,2,1,5+1,2,1,6+2,2,1,8+1,2,1,4+2,2,1,7+1,2,1,5+2,2,1,6+1,2,1,8+2,2,1,4+1,2,1,7+2,2,1,5+1,2,1,6+2,2,1,8+1,2,1,4+2,2,1,7+1,2,1,0,3+1,2,1,0,3+1,2,1,2,3+1,2,1,0,0,0
OneByOne_2_3_Voices: 1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2
OneByOne_4_8_Voices: 1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,2,3,4,3,2,5+1,2,3,4,3,2,6+1,2,3,4,3,2,7+1,2,3,4,3,2,8+1,2,3,4,3,2,7+1,2,3,4,3,2,6+1,2,3,4,3,2,7+1,2,3,4,3,2,8+1,2,3,4,3,2,7+1,2,3,4,3,2,6+1,2,3,4,3,2,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,3,1,2,1,2,1,2,1,2,1,2,1,2,1,0
Festive_1Voice: 1,1,1,0,1,1,1,1,0,1,0,1,1,1,0,1,1,1,1,1,1,0,1,0
Festive_4Voices: 1,2,3,4+1,2,1,3,4+1
Festive_5Voices: 1,2,3,2,1,2,1,2,4,2,1,2,1,2,3,2,1,2,1,2,5,2,1,2
Festive_5Voice_Alternative: 3,2,4,1,3,3,2,4,1,5,3,2,4,1,3,3,2,4,1,5+1,3,2,4,1,3,3,2,4,1,5+1,3,2,4,1,3+1,3,2,4,1,5+1,3,2,4,1,3+1,3,2,4,1,5+1,3,2,4,1,3,3,2,4,1,5,3,2,4,1,3,3,2,4,1,5
Festive_6Voices: 1,2,3,2,1,2,1,2,4,2,1,2,1,2,3,2,1,2,1,2,5,2,1,2,1,2,4+1,2,1,2,5+1,2,1,2,3+1,2,1,2,6+1,2,1,2,4+1,2,1,2,5+1,2,1,2,3+1,2,1,2,6+1,2,1,2,4+1,2,1,2,5+1,2,1,2,3+1,2,1,2,6+1,2,1,2
Festive_8Voices: 1,2,3,4,5,6,7,8
Ormilia: 2,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,2,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,2,1,0,1,2,1,0,1,2,1,0,1,0,1,0,1,2,1,2,1,2,1,2,1,0,1,0,1,0,1,0,1,2,4+1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,2,3+1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,2,4+1,0,1,2,3+1,0,1,2,4+1,0,1,2,3+1,0,1,2,5+1,2,1,2,6+1,2,1,2,5+1,2,1,2,6+1,2,7+1,2,8+1,2,4+1,2,7+1,2,8+1,2,4+1,2,7+1,2,8+1,2,3+1,2,1,0

View File

@@ -1,215 +0,0 @@
#!/usr/bin/env python3
"""
Bell Melody Converter
Converts human-readable bell notation to binary .bsm files for ESP32
Format: MELODY_NAME: 1,2,3+4,0,5+6+7
Output: MELODY_NAME.bsm (binary file, uint16_t big-endian)
"""
import sys
import re
from pathlib import Path
from typing import List, Tuple
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:
print(f"Warning: Invalid format (missing ':'): {line}")
return None, []
parts = line.split(':', 1)
melody_name = parts[0].strip()
steps_str = parts[1].strip()
if not melody_name:
print(f"Warning: Empty melody name in line: {line}")
return None, []
# Parse steps (comma-separated)
step_strings = steps_str.split(',')
values = []
for i, step_str in enumerate(step_strings):
value = parse_bell_notation(step_str)
values.append(value)
return melody_name, values
def write_binary_melody(filepath: str, values: List[int]):
"""
Write melody values as binary file (uint16_t, big-endian)
Args:
filepath: Output file path
values: List of uint16_t values (0-65535)
"""
with open(filepath, 'wb') as f:
for value in values:
# Ensure value fits in uint16_t
if value > 0xFFFF:
print(f"Warning: Value {value} exceeds uint16_t range, truncating")
value = value & 0xFFFF
# Write as 2 bytes, big-endian (MSB first)
f.write(value.to_bytes(2, byteorder='big'))
def convert_melodies_file(input_path: str, output_dir: str = '.'):
"""
Convert multi-melody file to individual .bsm binary files
Args:
input_path: Path to input text file
output_dir: Directory for output .bsm files
"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
melodies_created = 0
total_steps = 0
try:
with open(input_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
print(f"Reading from: {input_path}")
print(f"Output directory: {output_path.absolute()}\n")
for line_num, line in enumerate(lines, 1):
melody_name, values = parse_melody_line(line)
if melody_name and values:
# Create output filename
output_file = output_path / f"{melody_name}.bsm"
# Write binary file
write_binary_melody(str(output_file), values)
# Calculate file size
file_size = len(values) * 2 # 2 bytes per uint16_t
# Show what bells are used
all_bells = set()
for value in values:
for bit in range(16):
if value & (1 << bit):
all_bells.add(bit + 1) # Convert back to 1-indexed
bells_str = ','.join(map(str, sorted(all_bells))) if all_bells else 'none'
print(f"{melody_name}.bsm")
print(f" Steps: {len(values)}")
print(f" Size: {file_size} bytes")
print(f" Bells used: {bells_str}")
print()
melodies_created += 1
total_steps += len(values)
print(f"{'='*50}")
print(f"✓ Successfully created {melodies_created} melody files")
print(f" Total steps: {total_steps}")
print(f" Total size: {total_steps * 2} bytes")
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 Converter ===")
print("Creates binary .bsm files for ESP32\n")
# Default input file
input_file = "all_melodies.txt"
output_dir = "."
# Check if file exists
if not Path(input_file).exists():
print(f"Error: '{input_file}' not found in current directory!")
print("\nPlease create 'all_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_melodies_file(input_file, output_dir)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View File

@@ -1,300 +0,0 @@
#!/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()

View File

@@ -1,475 +0,0 @@
/*
* Bell Melodies - Auto-generated
* Generated: 2026-01-20 09:19:43
* Source: builtin_melodies.txt
*
* This file contains built-in melody definitions for the ESP32 bell controller
*/
#ifndef MELODIES_H
#define MELODIES_H
#include <Arduino.h>
// ========================================
// Melody Data Arrays
// ========================================
// Doxology Traditional
const uint16_t PROGMEM melody_builtin_doxology_traditional[] = {
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0000, 0x0000,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0008, 0x0000, 0x0000,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0008, 0x0000, 0x0000
};
// Doxology Alternative
const uint16_t PROGMEM melody_builtin_doxology_alternative[] = {
0x0001, 0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001,
0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001, 0x0000,
0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001, 0x0002, 0x0001,
0x0002, 0x0004, 0x0000, 0x0018, 0x0000
};
// Doxology Festive
const uint16_t PROGMEM melody_builtin_doxology_festive[] = {
0x0002, 0x0004, 0x0009, 0x0004, 0x0002, 0x0004, 0x0011, 0x0004,
0x0002, 0x0004, 0x0021, 0x0004, 0x0002, 0x0004, 0x0011, 0x0004
};
// Vesper Traditional
const uint16_t PROGMEM melody_builtin_vesper_traditional[] = {
0x0001, 0x0002, 0x0004, 0x0000, 0x0001, 0x0002, 0x0004, 0x0000,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0000
};
// Vesper Alternative
const uint16_t PROGMEM melody_builtin_vesper_alternative[] = {
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
0x0001, 0x0004, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
0x0001, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0004, 0x0000, 0x0000,
0x0001, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
// Catehetical
const uint16_t PROGMEM melody_builtin_catehetical[] = {
0x0001, 0x0002, 0x0004, 0x0008, 0x0010
};
// Orthros Traditional
const uint16_t PROGMEM melody_builtin_orthros_traditional[] = {
0x0001, 0x0000, 0x0002, 0x0000, 0x0004, 0x0008, 0x0000, 0x0010,
0x0000, 0x0020, 0x0000, 0x0040, 0x0080, 0x0000
};
// Orthros Alternative
const uint16_t PROGMEM melody_builtin_orthros_alternative[] = {
0x0001, 0x0000, 0x0002, 0x0001, 0x0000, 0x0002, 0x0000, 0x0001,
0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0004, 0x0000
};
// Mournfull Toll
const uint16_t PROGMEM melody_builtin_mournfull_toll[] = {
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
// Mournfull Toll Alternative
const uint16_t PROGMEM melody_builtin_mournfull_toll_alternative[] = {
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0004,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0002, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0008, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0008, 0x0008, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000
};
// Mournfull Toll Meg Par
const uint16_t PROGMEM melody_builtin_mournfull_toll_meg_par[] = {
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001, 0x0001,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0004, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0002, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0008, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
// Sematron
const uint16_t PROGMEM melody_builtin_sematron[] = {
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0001, 0x0008,
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0008
};
// Sematron Alternative
const uint16_t PROGMEM melody_builtin_sematron_alternative[] = {
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0001, 0x0008,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0001, 0x0001, 0x0008
};
// Athonite 1 2 Voices
const uint16_t PROGMEM melody_builtin_athonite_1_2_voices[] = {
0x0001, 0x0002, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0002,
0x0001, 0x0001, 0x0002, 0x0001, 0x0002
};
// Athonite 3 Voices
const uint16_t PROGMEM melody_builtin_athonite_3_voices[] = {
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004
};
// Athonite 3 4 Voices
const uint16_t PROGMEM melody_builtin_athonite_3_4_voices[] = {
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
0x0002, 0x0001, 0x0008, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
0x0002, 0x0001, 0x0009, 0x0002, 0x0001, 0x0005, 0x0002, 0x0001,
0x000A, 0x0002, 0x0001, 0x0006, 0x0002, 0x0001, 0x0009, 0x0002,
0x0001, 0x0005, 0x0002, 0x0001, 0x000A, 0x0002, 0x0001, 0x0006,
0x0002, 0x0001, 0x0009
};
// Athonite 4 8 Voices
const uint16_t PROGMEM melody_builtin_athonite_4_8_voices[] = {
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
0x0002, 0x0001, 0x0008, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
0x0002, 0x0001, 0x0009, 0x0002, 0x0001, 0x0011, 0x0002, 0x0001,
0x0022, 0x0002, 0x0001, 0x0081, 0x0002, 0x0001, 0x000A, 0x0002,
0x0001, 0x0041, 0x0002, 0x0001, 0x0012, 0x0002, 0x0001, 0x0021,
0x0002, 0x0001, 0x0082, 0x0002, 0x0001, 0x0009, 0x0002, 0x0001,
0x0042, 0x0002, 0x0001, 0x0011, 0x0002, 0x0001, 0x0022, 0x0002,
0x0001, 0x0081, 0x0002, 0x0001, 0x000A, 0x0002, 0x0001, 0x0041,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0000, 0x0000,
0x0000
};
// Onebyone 2 3 Voices
const uint16_t PROGMEM melody_builtin_onebyone_2_3_voices[] = {
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002
};
// Onebyone 4 8 Voices
const uint16_t PROGMEM melody_builtin_onebyone_4_8_voices[] = {
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0011, 0x0002, 0x0004,
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
0x0002, 0x0041, 0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0081,
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0041, 0x0002, 0x0004,
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
0x0002, 0x0041, 0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0081,
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0041, 0x0002, 0x0004,
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001,
0x0002, 0x0001, 0x0000
};
// Festive 1Voice
const uint16_t PROGMEM melody_builtin_festive_1voice[] = {
0x0001, 0x0001, 0x0001, 0x0000, 0x0001, 0x0001, 0x0001, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0001, 0x0001, 0x0000, 0x0001,
0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0000, 0x0001, 0x0000
};
// Festive 4Voices
const uint16_t PROGMEM melody_builtin_festive_4voices[] = {
0x0001, 0x0002, 0x0004, 0x0009, 0x0002, 0x0001, 0x0004, 0x0009
};
// Festive 5Voices
const uint16_t PROGMEM melody_builtin_festive_5voices[] = {
0x0001, 0x0002, 0x0004, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0008, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0010, 0x0002, 0x0001, 0x0002
};
// Festive 5Voice Alternative
const uint16_t PROGMEM melody_builtin_festive_5voice_alternative[] = {
0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004, 0x0002, 0x0008,
0x0001, 0x0010, 0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004,
0x0002, 0x0008, 0x0001, 0x0011, 0x0004, 0x0002, 0x0008, 0x0001,
0x0004, 0x0004, 0x0002, 0x0008, 0x0001, 0x0011, 0x0004, 0x0002,
0x0008, 0x0001, 0x0005, 0x0004, 0x0002, 0x0008, 0x0001, 0x0011,
0x0004, 0x0002, 0x0008, 0x0001, 0x0005, 0x0004, 0x0002, 0x0008,
0x0001, 0x0011, 0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004,
0x0002, 0x0008, 0x0001, 0x0010, 0x0004, 0x0002, 0x0008, 0x0001,
0x0004, 0x0004, 0x0002, 0x0008, 0x0001, 0x0010
};
// Festive 6Voices
const uint16_t PROGMEM melody_builtin_festive_6voices[] = {
0x0001, 0x0002, 0x0004, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0008, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0010, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
0x0001, 0x0002
};
// Festive 8Voices
const uint16_t PROGMEM melody_builtin_festive_8voices[] = {
0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080
};
// Ormilia
const uint16_t PROGMEM melody_builtin_ormilia[] = {
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0009, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0005, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0009, 0x0000, 0x0001, 0x0002, 0x0005, 0x0000, 0x0001,
0x0002, 0x0009, 0x0000, 0x0001, 0x0002, 0x0005, 0x0000, 0x0001,
0x0002, 0x0011, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002, 0x0001,
0x0002, 0x0011, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002, 0x0041,
0x0002, 0x0081, 0x0002, 0x0009, 0x0002, 0x0041, 0x0002, 0x0081,
0x0002, 0x0009, 0x0002, 0x0041, 0x0002, 0x0081, 0x0002, 0x0005,
0x0002, 0x0001, 0x0000
};
// ========================================
// Melody Information Structure
// ========================================
struct MelodyInfo {
const char* display_name;
const char* id;
const uint16_t* data;
size_t length;
};
// ========================================
// Melody Library
// ========================================
const MelodyInfo MELODY_LIBRARY[] = {
{
"Doxology Traditional",
"builtin_doxology_traditional",
melody_builtin_doxology_traditional,
sizeof(melody_builtin_doxology_traditional) / sizeof(uint16_t)
},
{
"Doxology Alternative",
"builtin_doxology_alternative",
melody_builtin_doxology_alternative,
sizeof(melody_builtin_doxology_alternative) / sizeof(uint16_t)
},
{
"Doxology Festive",
"builtin_doxology_festive",
melody_builtin_doxology_festive,
sizeof(melody_builtin_doxology_festive) / sizeof(uint16_t)
},
{
"Vesper Traditional",
"builtin_vesper_traditional",
melody_builtin_vesper_traditional,
sizeof(melody_builtin_vesper_traditional) / sizeof(uint16_t)
},
{
"Vesper Alternative",
"builtin_vesper_alternative",
melody_builtin_vesper_alternative,
sizeof(melody_builtin_vesper_alternative) / sizeof(uint16_t)
},
{
"Catehetical",
"builtin_catehetical",
melody_builtin_catehetical,
sizeof(melody_builtin_catehetical) / sizeof(uint16_t)
},
{
"Orthros Traditional",
"builtin_orthros_traditional",
melody_builtin_orthros_traditional,
sizeof(melody_builtin_orthros_traditional) / sizeof(uint16_t)
},
{
"Orthros Alternative",
"builtin_orthros_alternative",
melody_builtin_orthros_alternative,
sizeof(melody_builtin_orthros_alternative) / sizeof(uint16_t)
},
{
"Mournfull Toll",
"builtin_mournfull_toll",
melody_builtin_mournfull_toll,
sizeof(melody_builtin_mournfull_toll) / sizeof(uint16_t)
},
{
"Mournfull Toll Alternative",
"builtin_mournfull_toll_alternative",
melody_builtin_mournfull_toll_alternative,
sizeof(melody_builtin_mournfull_toll_alternative) / sizeof(uint16_t)
},
{
"Mournfull Toll Meg Par",
"builtin_mournfull_toll_meg_par",
melody_builtin_mournfull_toll_meg_par,
sizeof(melody_builtin_mournfull_toll_meg_par) / sizeof(uint16_t)
},
{
"Sematron",
"builtin_sematron",
melody_builtin_sematron,
sizeof(melody_builtin_sematron) / sizeof(uint16_t)
},
{
"Sematron Alternative",
"builtin_sematron_alternative",
melody_builtin_sematron_alternative,
sizeof(melody_builtin_sematron_alternative) / sizeof(uint16_t)
},
{
"Athonite 1 2 Voices",
"builtin_athonite_1_2_voices",
melody_builtin_athonite_1_2_voices,
sizeof(melody_builtin_athonite_1_2_voices) / sizeof(uint16_t)
},
{
"Athonite 3 Voices",
"builtin_athonite_3_voices",
melody_builtin_athonite_3_voices,
sizeof(melody_builtin_athonite_3_voices) / sizeof(uint16_t)
},
{
"Athonite 3 4 Voices",
"builtin_athonite_3_4_voices",
melody_builtin_athonite_3_4_voices,
sizeof(melody_builtin_athonite_3_4_voices) / sizeof(uint16_t)
},
{
"Athonite 4 8 Voices",
"builtin_athonite_4_8_voices",
melody_builtin_athonite_4_8_voices,
sizeof(melody_builtin_athonite_4_8_voices) / sizeof(uint16_t)
},
{
"Onebyone 2 3 Voices",
"builtin_onebyone_2_3_voices",
melody_builtin_onebyone_2_3_voices,
sizeof(melody_builtin_onebyone_2_3_voices) / sizeof(uint16_t)
},
{
"Onebyone 4 8 Voices",
"builtin_onebyone_4_8_voices",
melody_builtin_onebyone_4_8_voices,
sizeof(melody_builtin_onebyone_4_8_voices) / sizeof(uint16_t)
},
{
"Festive 1Voice",
"builtin_festive_1voice",
melody_builtin_festive_1voice,
sizeof(melody_builtin_festive_1voice) / sizeof(uint16_t)
},
{
"Festive 4Voices",
"builtin_festive_4voices",
melody_builtin_festive_4voices,
sizeof(melody_builtin_festive_4voices) / sizeof(uint16_t)
},
{
"Festive 5Voices",
"builtin_festive_5voices",
melody_builtin_festive_5voices,
sizeof(melody_builtin_festive_5voices) / sizeof(uint16_t)
},
{
"Festive 5Voice Alternative",
"builtin_festive_5voice_alternative",
melody_builtin_festive_5voice_alternative,
sizeof(melody_builtin_festive_5voice_alternative) / sizeof(uint16_t)
},
{
"Festive 6Voices",
"builtin_festive_6voices",
melody_builtin_festive_6voices,
sizeof(melody_builtin_festive_6voices) / sizeof(uint16_t)
},
{
"Festive 8Voices",
"builtin_festive_8voices",
melody_builtin_festive_8voices,
sizeof(melody_builtin_festive_8voices) / sizeof(uint16_t)
},
{
"Ormilia",
"builtin_ormilia",
melody_builtin_ormilia,
sizeof(melody_builtin_ormilia) / sizeof(uint16_t)
}
};
const size_t MELODY_LIBRARY_SIZE = 26;
#endif // MELODIES_H

View File

@@ -1,3 +0,0 @@
Esperinos-Adamn-1k [min 1426 / mid 572 / max 194]: 1,0,1,0,1,0,0,0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0
Esperinos-Eortastikos-1k: 1,1,0,0,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,0,0,0,0
Orthros-1k [min 552 / mid 1402 / max 2229]: 1,0,1,0,1,1,0

View File

@@ -1,3 +0,0 @@
PyQt6>=6.4.0
numpy>=1.21.0
sounddevice>=0.4.6

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,12 @@ from typing import List, Optional
from builder import database as db
from builder.models import BuiltMelodyCreate, BuiltMelodyUpdate, BuiltMelodyInDB
from fastapi import HTTPException
from config import settings
logger = logging.getLogger("builder.service")
# Storage directory for built .bsm files
STORAGE_DIR = Path(__file__).parent.parent / "storage" / "built_melodies"
# Storage directory for built .bsm files — configurable via BUILT_MELODIES_STORAGE_PATH env var
STORAGE_DIR = Path(settings.built_melodies_storage_path)
def _ensure_storage_dir():

View File

@@ -24,6 +24,9 @@ class Settings(BaseSettings):
sqlite_db_path: str = "./mqtt_data.db"
mqtt_data_retention_days: int = 90
# Local file storage
built_melodies_storage_path: str = "./storage/built_melodies"
# App
backend_cors_origins: str = '["http://localhost:5173"]'
debug: bool = True

View File

@@ -1,5 +1,5 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from typing import Any, Dict, List, Optional
from enum import Enum
@@ -133,9 +133,10 @@ class DeviceUpdate(BaseModel):
device_photo: Optional[str] = None
device_location: Optional[str] = None
is_Online: Optional[bool] = None
device_attributes: Optional[DeviceAttributes] = None
device_subscription: Optional[DeviceSubInformation] = None
device_stats: Optional[DeviceStatistics] = None
# Use raw dicts so only the fields actually sent are present — no Pydantic defaults
device_attributes: Optional[Dict[str, Any]] = None
device_subscription: Optional[Dict[str, Any]] = None
device_stats: Optional[Dict[str, Any]] = None
events_on: Optional[bool] = None
device_location_coordinates: Optional[str] = None
device_melodies_all: Optional[List[MelodyMainItem]] = None

View File

@@ -15,6 +15,33 @@ COLLECTION = "devices"
SN_CHARS = string.ascii_uppercase + string.digits
SN_SEGMENT_LEN = 4
# Clock/silence/backlight fields stored as Firestore Timestamps (written as datetime)
_TIMESTAMP_FIELD_NAMES = {
"daySilenceFrom", "daySilenceTo",
"nightSilenceFrom", "nightSilenceTo",
"backlightTurnOnTime", "backlightTurnOffTime",
}
def _restore_timestamps(d: dict) -> dict:
"""Recursively convert ISO 8601 strings for known timestamp fields to datetime objects.
Firestore stores Python datetime objects as native Timestamps, which Flutter
reads as DateTime. Plain strings would break the Flutter app.
"""
result = {}
for k, v in d.items():
if isinstance(v, dict):
result[k] = _restore_timestamps(v)
elif isinstance(v, str) and k in _TIMESTAMP_FIELD_NAMES:
try:
result[k] = datetime.fromisoformat(v.replace("Z", "+00:00"))
except ValueError:
result[k] = v
else:
result[k] = v
return result
def _generate_serial_number() -> str:
"""Generate a unique serial number in the format BS-XXXX-XXXX."""
@@ -139,6 +166,17 @@ def create_device(data: DeviceCreate) -> DeviceInDB:
return DeviceInDB(id=doc_ref.id, **doc_data)
def _deep_merge(base: dict, overrides: dict) -> dict:
"""Recursively merge overrides into base, preserving unmentioned nested keys."""
result = dict(base)
for k, v in overrides.items():
if isinstance(v, dict) and isinstance(result.get(k), dict):
result[k] = _deep_merge(result[k], v)
else:
result[k] = v
return result
def update_device(device_doc_id: str, data: DeviceUpdate) -> DeviceInDB:
"""Update an existing device document. Only provided fields are updated."""
db = get_db()
@@ -149,16 +187,16 @@ def update_device(device_doc_id: str, data: DeviceUpdate) -> DeviceInDB:
update_data = data.model_dump(exclude_none=True)
# For nested structs, merge with existing data rather than replacing
# Deep-merge nested structs so unmentioned sub-fields are preserved
existing = doc.to_dict()
nested_keys = (
"device_attributes", "device_subscription", "device_stats",
)
for key in nested_keys:
if key in update_data and key in existing:
merged = {**existing[key], **update_data[key]}
update_data[key] = merged
if key in update_data and isinstance(existing.get(key), dict):
update_data[key] = _deep_merge(existing[key], update_data[key])
update_data = _restore_timestamps(update_data)
doc_ref.update(update_data)
updated_doc = doc_ref.get()

Binary file not shown.

0
data/.gitkeep Normal file
View File

View File

@@ -5,6 +5,10 @@ services:
env_file: .env
volumes:
- ./backend:/app
# Persistent data - lives outside the container
- ./data/mqtt_data.db:/app/mqtt_data.db
- ./data/built_melodies:/app/storage/built_melodies
- ./data/firebase-service-account.json:/app/firebase-service-account.json:ro
ports:
- "8000:8000"
depends_on: []

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -176,11 +176,15 @@ export default function DeviceForm() {
device_stats: stats,
events_on: eventsOn,
device_location_coordinates: locationCoordinates,
websocket_url: websocketUrl,
churchAssistantURL,
// device_melodies_all, device_melodies_favorites, user_list are managed
// elsewhere and must NOT be sent here to avoid overwriting them on edit
...(!isEdit && {
device_melodies_all: [],
device_melodies_favorites: [],
user_list: [],
websocket_url: websocketUrl,
churchAssistantURL,
}),
};
let deviceId = id;

View File

@@ -468,13 +468,6 @@ export default function DeviceList() {
className="flex gap-2"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => navigate(`/devices/${device.id}/edit`)}
className="hover:opacity-80 text-xs cursor-pointer"
style={{ color: "var(--text-link)" }}
>
Edit
</button>
<button
onClick={() => setDeleteTarget(device)}
className="hover:opacity-80 text-xs cursor-pointer"

View File

@@ -171,6 +171,940 @@ input[type="range"]::-moz-range-thumb {
gap: 1.5rem;
}
.device-tabs-wrap {
border-bottom: 1px solid var(--border-primary);
padding: 0 0 0.6rem;
}
.device-tabs-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
}
.device-tab-item {
display: inline-flex;
align-items: center;
}
.device-tab-divider {
width: 1px;
height: 16px;
background-color: var(--border-primary);
margin: 0 0.3rem;
}
.device-tab-btn {
border: 0;
background: transparent;
color: var(--text-secondary);
border-radius: 0.45rem;
padding: 0.4rem 0.7rem;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.02em;
transition: all 0.2s ease;
position: relative;
--tab-tone: rgba(116, 184, 22, 0.24);
--tab-glow: rgba(116, 184, 22, 0.24);
}
.device-tab-btn:hover {
color: var(--text-primary);
background: color-mix(in srgb, var(--tab-tone) 42%, transparent);
}
.device-tab-btn--active {
background-color: color-mix(in srgb, var(--tab-tone) 70%, transparent);
color: var(--tab-text);
box-shadow: 0 0 18px var(--tab-glow);
}
.device-tab-btn--dashboard {
--tab-tone: rgba(136, 201, 94, 0.34);
--tab-glow: rgba(136, 201, 94, 0.32);
--tab-text: #baff53;
}
.device-tab-btn--general {
--tab-tone: rgba(92, 158, 189, 0.34);
--tab-glow: rgba(92, 161, 189, 0.32);
--tab-text: #7dd6ff;
}
.device-tab-btn--bells {
--tab-tone: rgba(210, 218, 226, 0.34);
--tab-glow: rgba(230, 236, 243, 0.28);
--tab-text: #f3f9ff;
}
.device-tab-btn--clock {
--tab-tone: rgba(233, 199, 99, 0.34);
--tab-glow: rgba(233, 199, 99, 0.3);
--tab-text: #ffde4d;
}
.device-tab-btn--warranty {
--tab-tone: rgba(177, 94, 194, 0.33);
--tab-glow: rgba(182, 98, 185, 0.29);
--tab-text: rgba(214, 131, 253, 0.99);
}
.device-tab-btn--control {
--tab-tone: rgba(216, 106, 106, 0.34);
--tab-glow: rgba(215, 105, 105, 0.28);
--tab-text: #ff5353;
}
.device-tab-stack {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.device-tab-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 1rem;
}
.device-tab-grid--wide > * {
grid-column: 1 / -1;
}
.device-dashboard-hero {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 1rem;
}
.device-dashboard-main-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 1.1rem 1.4rem;
}
.device-dashboard-subsection {
padding-bottom: 0.2rem;
border-bottom: 1px solid var(--border-secondary);
}
/* ── Dashboard Hero Card ──────────────────────────────────────────── */
/*
* Layout strategy (desktop):
* Card is a flex row. Each section is a flex-column with
* justify-content: space-between and padding: 10px 0, so the 3 rows
* are evenly distributed top-to-bottom regardless of content height.
* The card itself has a fixed min-height so all columns are the same.
* Row alignment across columns is achieved by keeping the same
* flex rhythm — each section always has exactly 3 flex children.
* Sections with 2 logical items use a "grow" wrapper for the last one.
*/
/* ── Outer row: main card + notes card, same height ── */
.db-hero-row {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
@media (min-width: 1100px) {
.db-hero-row {
flex-direction: row;
align-items: stretch;
gap: 1.25rem;
}
}
/* ── Main info card ── */
.db-hero-card {
display: flex;
flex-direction: column;
background-color: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 10px;
padding: 1.75rem 1.5rem;
gap: 0;
flex: 1;
min-width: 0;
}
@media (min-width: 1100px) {
.db-hero-card {
flex-direction: row;
flex-wrap: wrap;
align-items: stretch;
padding: 1.3rem 0;
min-height: 320px;
row-gap: 0.5rem;
}
}
/* ── Notes sidebar card ── */
.db-notes-card {
display: flex;
flex-direction: column;
background-color: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 10px;
padding: 3.30rem 2.5rem 2.5rem;
flex-shrink: 0;
width: 100%;
gap: 1rem;
overflow: hidden;
}
@media (min-width: 1100px) {
.db-notes-card {
width: 22%;
min-width: 200px;
max-width: 300px;
}
}
/* Vertical/horizontal divider between columns */
.db-hero-divider {
width: 100%;
height: 1px;
background: var(--border-primary);
opacity: 0.6;
margin: 1.25rem 0;
flex-shrink: 0;
}
@media (min-width: 1100px) {
.db-hero-divider {
width: 1px;
height: auto;
margin: 1.9rem 0.8rem;
align-self: stretch;
}
}
/* ── Photo column ── */
.db-hero-photo {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 2.5rem 4.5rem;
flex-shrink: 0;
}
@media (min-width: 1100px) {
.db-hero-photo {
padding: 0rem 2rem 0rem 3rem;
}
}
/* Pulsing glow animations */
@keyframes glow-pulse-online {
0%, 100% { opacity: 0.55; transform: translateX(-52%) scale(1); }
50% { opacity: 1; transform: translateX(-52%) scale(1.12); }
}
@keyframes glow-pulse-offline {
0%, 100% { opacity: 0.45; transform: translateX(-52%) scale(1); }
50% { opacity: 0.9; transform: translateX(-52%) scale(1.1); }
}
/* Shared glow base */
.db-hero-photo--online::before,
.db-hero-photo--offline::before {
content: "";
position: absolute;
width: 230px;
height: 230px;
border-radius: 50%;
bottom: 10%;
left: 53%;
pointer-events: none;
}
/* Green pulsing glow when online */
.db-hero-photo--online::before {
background: radial-gradient(ellipse at center, rgba(169, 247, 95, 0.7) 10%, transparent 55%);
animation: glow-pulse-online 2.8s ease-in-out infinite;
}
/* Orange-red pulsing glow when offline */
.db-hero-photo--offline::before {
background: radial-gradient(ellipse at center, rgba(255, 120, 50, 0.65) 10%, transparent 55%);
animation: glow-pulse-offline 3.4s ease-in-out infinite;
}
.db-hero-photo__img {
position: relative; /* sits above the ::before glow */
max-width: 160px;
max-height: 210px;
object-fit: contain;
}
/* ── Info sections ── */
/*
* Each section is a 3-row grid with fixed row height.
* Every child lands in one row slot — identical height regardless of content
* (text, progress bar, etc.) — so rows align perfectly across all sections.
*/
.db-hero-section {
display: grid;
grid-template-rows: repeat(3, var(--hero-row-h, 65px));
align-items: center; /* centers content within each row */
align-content: space-between; /* ← centers the whole row group vertically in the section */
padding: 1rem 1rem;
flex-shrink: 0;
min-width: 0;
}
@media (min-width: 1100px) {
.db-hero-section {
padding: 1.5rem 3rem;
}
}
/* Section 3 Subscription: wide enough for both fields on one line */
.db-hero-section--sub {
min-width: 400px;
max-width: 560px;
flex: 1 1 400px;
}
/* Section 4 Uptime/Issue */
.db-hero-section--uptime {
min-width: 240px;
max-width: 340px;
flex: 1 1 240px;
}
.db-uptime-issue {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
/* ── Row wrappers ── */
.db-row {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
/* Subscription row 1: Tier left, Warranty right */
.db-sub-header-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
gap: 1.5rem;
white-space: nowrap;
}
/* Warranty column: pushed right by space-between, but its own content left-aligned */
.db-sub-header-row .db-info-field:last-child {
text-align: left;
}
/* ── Info field (label + value pair) ── */
.db-info-field {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.db-info-label {
font-weight: 600;
font-size: 0.68rem;
letter-spacing: 0.06em;
color: var(--text-muted);
opacity: 0.75;
text-transform: uppercase;
white-space: nowrap;
}
/* All values use identical styling — no special mono variant */
.db-info-value {
font-size: 0.92rem;
font-weight: 500;
line-height: 1.4;
color: var(--text-heading);
}
/* ── Progress bars ── */
.db-progress-track {
width: 100%;
max-width: 100%;
height: 12px;
border-radius: 999px;
background-color: var(--bg-primary);
border: 1px solid var(--border-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35);
overflow: hidden;
margin-top: 0.3rem;
}
.db-progress-fill {
height: 100%;
border-radius: 999px;
transition: width 0.35s ease;
}
/* ── Issue text ── */
.db-issue-source {
font-size: 0.88rem;
font-weight: 500;
color: #63B3ED;
line-height: 1.35;
}
.db-issue-body {
font-size: 0.82rem;
font-weight: 400;
line-height: 1.5;
color: var(--text-muted);
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
line-clamp: 4;
overflow: hidden;
}
/* ── Admin Notes ── */
.db-notes-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.db-notes-edit-btn {
background: none;
border: none;
padding: 0;
color: var(--text-muted);
font-size: 0.85rem;
cursor: pointer;
opacity: 0.85;
line-height: 1;
}
.db-notes-edit-btn:hover {
opacity: 1;
color: orange;
}
.db-notes-body {
font-size: 0.80rem;
font-weight: 400;
line-height: 1.5;
cursor: pointer;
text-align: left;
padding: 0rem 0.5rem 0rem 0rem;
flex: 1;
overflow-x: hidden;
overflow-y: auto;
min-height: 0; /* important — lets flex item shrink below content size */
}
.db-notes-body:hover {
opacity: 0.6;
}
.db-notes-textarea {
width: 100%;
min-height: 0;
padding: 0.3rem 0.65rem;
border-radius: 0.375rem;
font-size: 1rem;
border: 1px solid var(--border-input);
background-color: var(--bg-input);
color: var(--text-primary);
resize: vertical;
text-align: left;
}
/* ── Section cards (used in all tabs) ── */
.device-section-card {
border: 1px solid var(--border-primary);
border-radius: 0.75rem;
background-color: var(--bg-card);
padding: 2.25rem 2.5rem 2.5rem;
}
.device-section-card__title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-primary);
}
.device-section-card__title {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
/* Pencil edit button on section card titles */
.section-edit-btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.72rem;
font-weight: 600;
color: var(--text-muted);
background: none;
border: none;
cursor: pointer;
padding: 0.25rem 0.4rem;
border-radius: 0.3rem;
transition: color 0.15s ease;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.section-edit-btn:hover {
color: #ecc94b;
}
/* ── Toggle Switch ── */
.toggle-switch {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.toggle-switch__track {
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.35rem;
border-radius: 9999px;
background-color: #374151;
transition: background-color 0.2s ease;
flex-shrink: 0;
}
.toggle-switch__track[data-on="true"] {
background-color: var(--accent);
}
.toggle-switch__thumb {
position: absolute;
top: 0.175rem;
left: 0.175rem;
width: 1rem;
height: 1rem;
border-radius: 50%;
background-color: #fff;
transition: transform 0.2s ease;
}
.toggle-switch__track[data-on="true"] .toggle-switch__thumb {
transform: translateX(1.15rem);
}
.toggle-switch__label {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-muted);
}
.toggle-switch__label[data-on="true"] {
color: var(--accent);
}
/* ── Leaflet map isolation — prevents map z-indices bleeding over modals ── */
.device-map-wrap {
position: relative;
isolation: isolate;
}
/* ── Bell master strip (between Overview and bell cards) ── */
.bell-master-strip {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-radius: 0.6rem;
border: 1px solid var(--border-primary);
background-color: var(--bg-card);
}
.bell-master-strip__left {
display: flex;
align-items: center;
gap: 1rem;
}
.bell-master-strip__right {
display: flex;
align-items: center;
}
.bell-master-strip__warning-badge {
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.08em;
padding: 0.2rem 0.5rem;
border-radius: 0.3rem;
background-color: rgba(99, 179, 237, 0.1);
color: #63b3ed;
border: 1px solid rgba(99, 179, 237, 0.3);
}
/* ── Status text warning tone (for voided warranty) ── */
.device-status-text--warning {
color: #f59e0b;
}
.device-subtitle {
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.03em;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 0.65rem;
}
.device-hero-image-wrap {
border: 1px solid var(--border-primary);
border-radius: 0.75rem;
background: radial-gradient(circle at 20% 20%, rgba(116, 184, 22, 0.18), rgba(17, 24, 39, 0.6));
min-height: 180px;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
margin-bottom: 0.8rem;
}
.device-hero-image-wrap--square {
min-height: 280px;
max-width: 400px;
width: 100%;
aspect-ratio: 1 / 1;
}
.device-hero-image {
max-height: 280px;
max-width: 100%;
object-fit: contain;
}
.device-snapshot-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem 1.2rem;
align-items: start;
}
.device-progress-track {
width: 100%;
height: 10px;
border-radius: 999px;
background-color: var(--bg-primary);
border: 1px solid var(--border-primary);
overflow: hidden;
}
.device-progress-fill {
height: 100%;
border-radius: 999px;
transition: width 0.35s ease;
}
.device-field-row {
display: grid;
gap: 0 1.1rem; /* no vertical gap — row height handles spacing */
margin-bottom: 0;
align-items: start;
}
/* Each field is a fixed-height slot — same as --hero-row-h so tabs match the dashboard rhythm */
.device-field {
height: var(--field-row-h, 75px);
padding-right: 0.3rem;
display: flex;
flex-direction: column;
justify-content: center;
}
.device-field-value {
margin-top: 0.25rem;
line-height: 1.5;
}
.device-value-secondary {
font-size: 0.78rem;
color: var(--text-muted);
}
.device-status-text {
font-weight: 600;
letter-spacing: 0.01em;
}
.device-status-text--positive {
color: var(--success-text);
text-shadow: 0 0 14px rgba(116, 184, 22, 0.28);
}
.device-status-text--negative {
color: #ff9e4a;
text-shadow: 0 0 14px rgba(243, 75, 75, 0.25);
}
.device-status-text--info {
color: var(--badge-blue-text);
text-shadow: 0 0 16px rgba(99, 179, 237, 0.28);
}
.device-status-text--muted {
color: var(--text-muted);
}
.device-empty-cell {
height: var(--field-row-h, 65px);
}
.device-inline-facts {
display: flex;
flex-wrap: wrap;
gap: 6rem;
padding: 1rem 0px 0rem 0px;
}
.device-inline-facts > div {
display: inline-flex;
align-items: center;
gap: 0.6rem;
line-height: 1.1;
}
.device-inline-facts__label {
display: inline-flex;
align-items: center;
color: var(--text-muted);
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.03em;
line-height: 1.1;
}
.device-inline-facts--overview .device-inline-facts__label::after {
content: "|";
color: var(--border-primary);
opacity: 0.65;
margin: 0 0.6rem;
font-weight: 500;
}
.device-inline-facts--overview > div {
gap: 0;
}
.device-location-grid {
display: grid;
grid-template-columns: 1fr;
}
.dashboard-bottom-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
.bell-mechanism-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
gap: 0.85rem;
}
.bell-mechanism-card {
position: relative;
border-radius: 10px;
background: #1F2937;
padding: 2.5rem;
display: grid;
grid-template-columns: minmax(0, 1fr) 160px;
gap: 0 2rem;
overflow: hidden;
}
.bell-mechanism-card::before {
content: '';
position: absolute;
left: -160px;
top: 50%;
transform: translateY(-50%);
width: 330px;
height: 330px;
opacity: 0.02;
pointer-events: none;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6V11c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z'/%3E%3C/svg%3E") no-repeat center / contain;
}
/* Left column: title at top, info at bottom */
.bell-mechanism-card__left {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
overflow: hidden;
position: relative;
z-index: 1;
}
.bell-mechanism-card__left-top {
display: flex;
flex-direction: column;
}
.bell-mechanism-card__title {
font-family: 'Inter', sans-serif;
font-weight: 700;
font-size: 20px;
line-height: 24px;
color: #63B3ED;
text-shadow: 5px 5px 8px rgba(0, 0, 0, 0.25);
margin: 0 0 0.5rem 0;
white-space: nowrap;
}
.bell-mechanism-card__divider {
border: none;
border-top: 1px solid #374151;
opacity: 0.5;
width: 179px;
margin: 0 0 1rem 0;
}
.bell-mechanism-card__info {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.bell-mechanism-card__info p {
margin: 0;
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 15px;
line-height: 18px;
color: #9CA3AF;
white-space: nowrap;
}
.bell-mechanism-card__highlight {
font-weight: 700;
color: #74B816;
}
.bell-mechanism-card__highlight--strikes {
color: #F97316;
}
/* Right column: striker image at top, cert at bottom */
.bell-mechanism-card__right {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
position: relative;
z-index: 1;
}
.bell-mechanism-card__right-top {
display: flex;
align-items: flex-start;
}
.bell-mechanism-card__striker-img {
width: 150px;
height: 110px;
object-fit: contain;
object-position: top right;
pointer-events: none;
}
.bell-mechanism-card__cert {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: flex-end;
opacity: 0.52;
}
.bell-mechanism-card__cert-text {
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 13px;
line-height: 16px;
text-align: right;
color: #9CA3AF;
}
.bell-mechanism-card__cert-brand {
color: #63B3ED;
}
.bell-mechanism-card__cert-badge {
width: 36px;
height: 36px;
object-fit: contain;
flex-shrink: 0;
}
@media (min-width: 1024px) {
.device-tab-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.device-dashboard-main-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.device-location-grid {
grid-template-columns: 1.3fr 1fr;
column-gap: 2.5rem;
}
.device-snapshot-grid {
grid-template-columns: auto 1fr;
}
.dashboard-bottom-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1500px) {
.device-tab-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.device-dashboard-main-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.dashboard-bottom-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.dashboard-bottom-grid__notes {
grid-column: span 2;
}
.dashboard-bottom-grid__users {
grid-column: span 1;
}
}
/* File input */
input[type="file"]::file-selector-button {
background-color: var(--bg-card) !important;

View File

@@ -7,6 +7,13 @@ export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
hmr: {
clientPort: 80,
},
watch: {
usePolling: true,
interval: 500,
},
proxy: {
'/api': {
target: 'http://backend:8000',