feat: add secondary radio status reporting and startup initialization improvements

- Expose secondary multicaster status in /status endpoint with dedicated "secondary" block containing runtime and persisted settings
- Initialize Radio 2 checkbox state from backend streaming status to reflect active state on frontend load
- Add I2C register verification after write operations with i2cget to confirm configuration
- Set ADC mixer level to 60% on startup via amixer command
- Preserve backward compatibility by
This commit is contained in:
2025-11-20 14:19:18 +01:00
parent 9bfea25fd2
commit 7c0d5405fc
2 changed files with 119 additions and 11 deletions

View File

@@ -100,6 +100,10 @@ except Exception:
# Define is_streaming early from the fetched status for use throughout the UI
is_streaming = bool(saved_settings.get("is_streaming", False))
# Extract secondary status, if provided by the backend /status endpoint.
secondary_status = saved_settings.get("secondary") or {}
secondary_is_streaming = bool(saved_settings.get("secondary_is_streaming", secondary_status.get("is_streaming", False)))
st.title("Auracast Audio Mode Control")
def render_stream_controls(status_streaming: bool, start_label: str, stop_label: str, mode_label: str):
@@ -476,9 +480,13 @@ else:
# --- Radio 2 controls ---
st.subheader("Radio 2")
# If the backend reports that the secondary radio is currently streaming,
# initialize the checkbox to checked so the UI reflects the active state
# when the frontend is loaded.
radio2_enabled_default = secondary_is_streaming
radio2_enabled = st.checkbox(
"Enable Radio 2",
value=False,
value=radio2_enabled_default,
help="Activate a second analog radio with its own quality and timing settings."
)

View File

@@ -144,23 +144,94 @@ _stream_lock = asyncio.Lock() # serialize initialize/stop_audio on API side
async def _init_i2c_on_startup() -> None:
cmds = [
["i2cset", "-f", "-y", "1", "0x4a", "0x00", "0x00"],
["i2cset", "-f", "-y", "1", "0x4a", "0x06", "0x10"],
["i2cset", "-f", "-y", "1", "0x4a", "0x07", "0x10"],
# Table of (register, expected_value)
dev_add = "0x4a"
reg_table = [
("0x00", "0x00"),
("0x06", "0x10"),
("0x07", "0x10"),
]
for cmd in cmds:
for reg, expected in reg_table:
write_cmd = ["i2cset", "-f", "-y", "1", dev_add, reg, expected]
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
*write_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
log.warning("i2cset failed (%s): rc=%s stderr=%s", " ".join(cmd), proc.returncode, (stderr or b"").decode(errors="ignore").strip())
log.warning(
"i2cset failed (%s): rc=%s stderr=%s",
" ".join(write_cmd),
proc.returncode,
(stderr or b"").decode(errors="ignore").strip(),
)
# If the write failed, skip verification for this register
continue
except Exception as e:
log.warning("Exception running i2cset (%s): %s", " ".join(cmd), e, exc_info=True)
log.warning("Exception running i2cset (%s): %s", " ".join(write_cmd), e, exc_info=True)
continue
# Verify configured register with i2cget
read_cmd = ["i2cget", "-f", "-y", "1", dev_add, reg]
try:
proc = await asyncio.create_subprocess_exec(
*read_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
log.warning(
"i2cget failed (%s): rc=%s stderr=%s",
" ".join(read_cmd),
proc.returncode,
(stderr or b"").decode(errors="ignore").strip(),
)
continue
value = (stdout or b"").decode(errors="ignore").strip()
if value != expected:
log.error(
"I2C register verify failed: addr=0x4a reg=%s expected=%s got=%s",
reg,
expected,
value,
)
else:
log.info(
"I2C register verified: addr=0x4a reg=%s value=%s",
reg,
value,
)
except Exception as e:
log.warning("Exception running i2cget (%s): %s", " ".join(read_cmd), e, exc_info=True)
async def _set_adc_level_on_startup() -> None:
"""Ensure ADC mixer level is set at startup.
Runs: amixer -c 2 set 'ADC' x%
"""
cmd = ["amixer", "-c", "2", "set", "ADC", "60%"]
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
log.warning(
"amixer ADC level command failed (rc=%s): %s",
proc.returncode,
(stderr or b"" ).decode(errors="ignore").strip(),
)
else:
log.info("amixer ADC level set successfully: %s", (stdout or b"" ).decode(errors="ignore").strip())
except Exception as e:
log.warning("Exception running amixer ADC level command: %s", e, exc_info=True)
async def _stop_all() -> bool:
@@ -187,6 +258,16 @@ async def _status_primary() -> dict:
return {'is_initialized': False, 'is_streaming': False}
return multicaster1.get_status()
async def _status_secondary() -> dict:
"""Return runtime status for the SECONDARY multicaster.
Mirrors _status_primary but for multicaster2 so that /status can expose
both primary and secondary state to the frontend.
"""
if multicaster2 is None:
return {'is_initialized': False, 'is_streaming': False}
return multicaster2.get_status()
async def _stream_lc3(audio_data: dict[str, str], bigs_template: list) -> None:
if multicaster1 is None:
raise HTTPException(status_code=500, detail='Auracast endpoint was never intialized')
@@ -348,8 +429,23 @@ async def send_audio(audio_data: dict[str, str]):
@app.get("/status")
async def get_status():
"""Gets current status (worker) merged with persisted settings cache."""
status = await _status_primary()
status.update(load_stream_settings())
primary_runtime = await _status_primary()
primary_persisted = load_stream_settings() or {}
# Preserve existing top-level shape for primary for compatibility
status: dict = {}
status.update(primary_runtime)
status.update(primary_persisted)
# Attach secondary block with its own runtime + persisted settings
secondary_runtime = await _status_secondary()
secondary_persisted = load_stream_settings2() or {}
secondary: dict = {}
secondary.update(secondary_runtime)
secondary.update(secondary_persisted)
status["secondary"] = secondary
status["secondary_is_streaming"] = bool(secondary.get("is_streaming", False))
return status
async def _autostart_from_settings():
@@ -538,6 +634,8 @@ async def _autostart_from_settings():
channel_names = settings.get('channel_names') or ["Broadcast0"]
program_info = settings.get('program_info') or channel_names
languages = settings.get('languages') or ["deu"]
big_ids = settings.get('big_ids') or []
big_addrs = settings.get('big_random_addresses') or []
stream_password = settings.get('stream_password')
original_ts = settings.get('timestamp')
previously_streaming = bool(settings.get('is_streaming'))
@@ -689,6 +787,8 @@ async def _startup_autostart_event():
# Hydrate settings cache once to avoid disk I/O during /status
_init_settings_cache_from_disk()
await _init_i2c_on_startup()
# Ensure ADC mixer level is set at startup
await _set_adc_level_on_startup()
refresh_pw_cache()
log.info("[STARTUP] Scheduling autostart task")
asyncio.create_task(_autostart_from_settings())