update: overhauled firmware ui. Added public flash page.
This commit is contained in:
@@ -5,23 +5,34 @@ from database import get_db
|
||||
logger = logging.getLogger("builder.database")
|
||||
|
||||
|
||||
async def insert_built_melody(melody_id: str, name: str, pid: str, steps: str) -> None:
|
||||
async def insert_built_melody(melody_id: str, name: str, pid: str, steps: str, is_builtin: bool = False) -> None:
|
||||
db = await get_db()
|
||||
await db.execute(
|
||||
"""INSERT INTO built_melodies (id, name, pid, steps, assigned_melody_ids)
|
||||
VALUES (?, ?, ?, ?, ?)""",
|
||||
(melody_id, name, pid, steps, json.dumps([])),
|
||||
"""INSERT INTO built_melodies (id, name, pid, steps, assigned_melody_ids, is_builtin)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(melody_id, name, pid, steps, json.dumps([]), 1 if is_builtin else 0),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def update_built_melody(melody_id: str, name: str, pid: str, steps: str) -> None:
|
||||
async def update_built_melody(melody_id: str, name: str, pid: str, steps: str, is_builtin: bool = False) -> None:
|
||||
db = await get_db()
|
||||
await db.execute(
|
||||
"""UPDATE built_melodies
|
||||
SET name = ?, pid = ?, steps = ?, updated_at = datetime('now')
|
||||
SET name = ?, pid = ?, steps = ?, is_builtin = ?, updated_at = datetime('now')
|
||||
WHERE id = ?""",
|
||||
(name, pid, steps, melody_id),
|
||||
(name, pid, steps, 1 if is_builtin else 0, melody_id),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def update_builtin_flag(melody_id: str, is_builtin: bool) -> None:
|
||||
db = await get_db()
|
||||
await db.execute(
|
||||
"""UPDATE built_melodies
|
||||
SET is_builtin = ?, updated_at = datetime('now')
|
||||
WHERE id = ?""",
|
||||
(1 if is_builtin else 0, melody_id),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@@ -68,6 +79,7 @@ async def get_built_melody(melody_id: str) -> dict | None:
|
||||
return None
|
||||
row = dict(rows[0])
|
||||
row["assigned_melody_ids"] = json.loads(row["assigned_melody_ids"] or "[]")
|
||||
row["is_builtin"] = bool(row.get("is_builtin", 0))
|
||||
return row
|
||||
|
||||
|
||||
@@ -80,6 +92,7 @@ async def list_built_melodies() -> list[dict]:
|
||||
for row in rows:
|
||||
r = dict(row)
|
||||
r["assigned_melody_ids"] = json.loads(r["assigned_melody_ids"] or "[]")
|
||||
r["is_builtin"] = bool(r.get("is_builtin", 0))
|
||||
results.append(r)
|
||||
return results
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ class BuiltMelodyCreate(BaseModel):
|
||||
name: str
|
||||
pid: str
|
||||
steps: str # raw step string e.g. "1,2,2+1,1,2,3+1"
|
||||
is_builtin: bool = False
|
||||
|
||||
|
||||
class BuiltMelodyUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
pid: Optional[str] = None
|
||||
steps: Optional[str] = None
|
||||
is_builtin: Optional[bool] = None
|
||||
|
||||
|
||||
class BuiltMelodyInDB(BaseModel):
|
||||
@@ -19,6 +21,7 @@ class BuiltMelodyInDB(BaseModel):
|
||||
name: str
|
||||
pid: str
|
||||
steps: str
|
||||
is_builtin: bool = False
|
||||
binary_path: Optional[str] = None
|
||||
binary_url: Optional[str] = None
|
||||
progmem_code: Optional[str] = None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.responses import FileResponse, PlainTextResponse
|
||||
from auth.models import TokenPayload
|
||||
from auth.dependencies import require_permission
|
||||
from builder.models import (
|
||||
@@ -20,6 +20,7 @@ async def list_built_melodies(
|
||||
melodies = await service.list_built_melodies()
|
||||
return BuiltMelodyListResponse(melodies=melodies, total=len(melodies))
|
||||
|
||||
|
||||
@router.get("/for-melody/{firestore_melody_id}")
|
||||
async def get_for_firestore_melody(
|
||||
firestore_melody_id: str,
|
||||
@@ -32,6 +33,14 @@ async def get_for_firestore_melody(
|
||||
return result.model_dump()
|
||||
|
||||
|
||||
@router.get("/generate-builtin-list")
|
||||
async def generate_builtin_list(
|
||||
_user: TokenPayload = Depends(require_permission("melodies", "view")),
|
||||
):
|
||||
"""Generate a C++ header with PROGMEM arrays for all is_builtin archetypes."""
|
||||
code = await service.generate_builtin_list()
|
||||
return PlainTextResponse(content=code, media_type="text/plain")
|
||||
|
||||
|
||||
@router.get("/{melody_id}", response_model=BuiltMelodyInDB)
|
||||
async def get_built_melody(
|
||||
@@ -66,6 +75,15 @@ async def delete_built_melody(
|
||||
await service.delete_built_melody(melody_id)
|
||||
|
||||
|
||||
@router.post("/{melody_id}/toggle-builtin", response_model=BuiltMelodyInDB)
|
||||
async def toggle_builtin(
|
||||
melody_id: str,
|
||||
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
|
||||
):
|
||||
"""Toggle the is_builtin flag for an archetype."""
|
||||
return await service.toggle_builtin(melody_id)
|
||||
|
||||
|
||||
@router.post("/{melody_id}/build-binary", response_model=BuiltMelodyInDB)
|
||||
async def build_binary(
|
||||
melody_id: str,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user