update: overhauled firmware ui. Added public flash page.

This commit is contained in:
2026-03-18 17:49:40 +02:00
parent 4381a6681d
commit d0ac4f1d91
45 changed files with 6798 additions and 1723 deletions

View File

@@ -32,6 +32,7 @@ def _row_to_built_melody(row: dict) -> BuiltMelodyInDB:
name=row["name"],
pid=row["pid"],
steps=row["steps"],
is_builtin=row.get("is_builtin", False),
binary_path=binary_path,
binary_url=binary_url,
progmem_code=row.get("progmem_code"),
@@ -151,8 +152,12 @@ async def create_built_melody(data: BuiltMelodyCreate) -> BuiltMelodyInDB:
name=data.name,
pid=data.pid,
steps=data.steps,
is_builtin=data.is_builtin,
)
return await get_built_melody(melody_id)
# Auto-build binary and builtin code on creation
result = await get_built_melody(melody_id)
result = await _do_build(melody_id)
return result
async def update_built_melody(melody_id: str, data: BuiltMelodyUpdate) -> BuiltMelodyInDB:
@@ -163,11 +168,22 @@ async def update_built_melody(melody_id: str, data: BuiltMelodyUpdate) -> BuiltM
new_name = data.name if data.name is not None else row["name"]
new_pid = data.pid if data.pid is not None else row["pid"]
new_steps = data.steps if data.steps is not None else row["steps"]
new_is_builtin = data.is_builtin if data.is_builtin is not None else row.get("is_builtin", False)
await _check_unique(new_name, new_pid or "", exclude_id=melody_id)
await db.update_built_melody(melody_id, name=new_name, pid=new_pid, steps=new_steps)
return await get_built_melody(melody_id)
steps_changed = (data.steps is not None) and (data.steps != row["steps"])
await db.update_built_melody(melody_id, name=new_name, pid=new_pid, steps=new_steps, is_builtin=new_is_builtin)
# If steps changed, flag all assigned melodies as outdated, then rebuild
if steps_changed:
assigned_ids = row.get("assigned_melody_ids", [])
if assigned_ids:
await _flag_melodies_outdated(assigned_ids, True)
# Auto-rebuild binary and builtin code on every save
return await _do_build(melody_id)
async def delete_built_melody(melody_id: str) -> None:
@@ -175,6 +191,11 @@ async def delete_built_melody(melody_id: str) -> None:
if not row:
raise HTTPException(status_code=404, detail=f"Built melody '{melody_id}' not found")
# Flag all assigned melodies as outdated before deleting
assigned_ids = row.get("assigned_melody_ids", [])
if assigned_ids:
await _flag_melodies_outdated(assigned_ids, True)
# Delete the .bsm file if it exists
if row.get("binary_path"):
bsm_path = Path(row["binary_path"])
@@ -184,10 +205,26 @@ async def delete_built_melody(melody_id: str) -> None:
await db.delete_built_melody(melody_id)
async def toggle_builtin(melody_id: str) -> BuiltMelodyInDB:
"""Toggle the is_builtin flag for an archetype."""
row = await db.get_built_melody(melody_id)
if not row:
raise HTTPException(status_code=404, detail=f"Built melody '{melody_id}' not found")
new_value = not row.get("is_builtin", False)
await db.update_builtin_flag(melody_id, new_value)
return await get_built_melody(melody_id)
# ============================================================================
# Build Actions
# ============================================================================
async def _do_build(melody_id: str) -> BuiltMelodyInDB:
"""Internal: build both binary and PROGMEM code, return updated record."""
await build_binary(melody_id)
return await build_builtin_code(melody_id)
async def build_binary(melody_id: str) -> BuiltMelodyInDB:
"""Parse steps and write a .bsm binary file to storage."""
row = await db.get_built_melody(melody_id)
@@ -236,6 +273,48 @@ async def get_binary_path(melody_id: str) -> Optional[Path]:
return path
async def generate_builtin_list() -> str:
"""Generate a C++ header with PROGMEM arrays for all is_builtin archetypes."""
rows = await db.list_built_melodies()
builtin_rows = [r for r in rows if r.get("is_builtin")]
if not builtin_rows:
return "// No built-in archetypes defined.\n"
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
parts = [
f"// Auto-generated Built-in Archetype List",
f"// Generated: {timestamp}",
f"// Total built-ins: {len(builtin_rows)}",
"",
"#pragma once",
"#include <avr/pgmspace.h>",
"",
]
entry_refs = []
for row in builtin_rows:
values = steps_string_to_values(row["steps"])
array_name = f"melody_builtin_{row['name'].lower().replace(' ', '_')}"
display_name = row["name"].replace("_", " ").title()
pid = row.get("pid") or f"builtin_{row['name'].lower()}"
parts.append(f"// {display_name} | PID: {pid} | Steps: {len(values)}")
parts.append(format_melody_array(row["name"].lower().replace(" ", "_"), values))
parts.append("")
entry_refs.append((display_name, pid, array_name, len(values)))
# Generate MELODY_LIBRARY array
parts.append("// --- MELODY_LIBRARY entries ---")
parts.append("// Add these to your firmware's MELODY_LIBRARY[] array:")
parts.append("// {")
for display_name, pid, array_name, step_count in entry_refs:
parts.append(f'// {{ "{display_name}", "{pid}", {array_name}, {step_count} }},')
parts.append("// };")
return "\n".join(parts)
# ============================================================================
# Assignment
# ============================================================================
@@ -251,6 +330,9 @@ async def assign_to_melody(built_id: str, firestore_melody_id: str) -> BuiltMelo
assigned.append(firestore_melody_id)
await db.update_assigned_melody_ids(built_id, assigned)
# Clear outdated flag on the melody being assigned
await _flag_melodies_outdated([firestore_melody_id], False)
return await get_built_melody(built_id)
@@ -262,6 +344,10 @@ async def unassign_from_melody(built_id: str, firestore_melody_id: str) -> Built
assigned = [mid for mid in row.get("assigned_melody_ids", []) if mid != firestore_melody_id]
await db.update_assigned_melody_ids(built_id, assigned)
# Flag the melody as outdated since it no longer has an archetype
await _flag_melodies_outdated([firestore_melody_id], True)
return await get_built_melody(built_id)
@@ -272,3 +358,48 @@ async def get_built_melody_for_firestore_id(firestore_melody_id: str) -> Optiona
if firestore_melody_id in row.get("assigned_melody_ids", []):
return _row_to_built_melody(row)
return None
# ============================================================================
# Outdated Flag Helpers
# ============================================================================
async def _flag_melodies_outdated(melody_ids: List[str], outdated: bool) -> None:
"""Set or clear the outdated_archetype flag on a list of Firestore melody IDs.
This updates both SQLite (melody_drafts) and Firestore (published melodies).
We import inline to avoid circular imports.
"""
if not melody_ids:
return
try:
from melodies import database as melody_db
from shared.firebase import get_db as get_firestore
except ImportError:
logger.warning("Could not import melody/firebase modules — skipping outdated flag update")
return
firestore_db = get_firestore()
for melody_id in melody_ids:
try:
row = await melody_db.get_melody(melody_id)
if not row:
continue
data = row["data"]
info = dict(data.get("information", {}))
info["outdated_archetype"] = outdated
data["information"] = info
await melody_db.update_melody(melody_id, data)
# If published, also update Firestore
if row.get("status") == "published":
doc_ref = firestore_db.collection("melodies").document(melody_id)
doc_ref.update({"information.outdated_archetype": outdated})
logger.info(f"Set outdated_archetype={outdated} on melody {melody_id}")
except Exception as e:
logger.error(f"Failed to set outdated flag on melody {melody_id}: {e}")