Compare commits
11 Commits
wip_alsaau
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d54e72f1d | |||
| df6c85d9ff | |||
| 8106f61d6a | |||
| 0a8dc74d5c | |||
| 8475e4d068 | |||
| 3f01ef5968 | |||
|
|
67992e65ec | ||
| 0b12323921 | |||
|
|
6e633d2880 | ||
| 7bdf6f8417 | |||
| 4036fee1f5 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -51,4 +51,5 @@ src/auracast/available_samples.txt
|
||||
src/auracast/server/stream_settings2.json
|
||||
src/scripts/temperature_log*
|
||||
|
||||
src/auracast/server/recordings/
|
||||
src/auracast/server/recordings/
|
||||
src/auracast/server/led_settings.json
|
||||
|
||||
18
poetry.lock
generated
18
poetry.lock
generated
@@ -2414,6 +2414,22 @@ files = [
|
||||
{file = "rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpi-gpio"
|
||||
version = "0.7.1"
|
||||
description = "A module to control Raspberry Pi GPIO channels"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "RPi.GPIO-0.7.1-cp27-cp27mu-linux_armv6l.whl", hash = "sha256:b86b66dc02faa5461b443a1e1f0c1d209d64ab5229696f32fb3b0215e0600c8c"},
|
||||
{file = "RPi.GPIO-0.7.1-cp310-cp310-linux_armv6l.whl", hash = "sha256:57b6c044ef5375a78c8dda27cdfadf329e76aa6943cd6cffbbbd345a9adf9ca5"},
|
||||
{file = "RPi.GPIO-0.7.1-cp37-cp37m-linux_armv6l.whl", hash = "sha256:77afb817b81331ce3049a4b8f94a85e41b7c404d8e56b61ac0f1eb75c3120868"},
|
||||
{file = "RPi.GPIO-0.7.1-cp38-cp38-linux_armv6l.whl", hash = "sha256:29226823da8b5ccb9001d795a944f2e00924eeae583490f0bc7317581172c624"},
|
||||
{file = "RPi.GPIO-0.7.1-cp39-cp39-linux_armv6l.whl", hash = "sha256:15311d3b063b71dee738cd26570effc9985a952454d162937c34e08c0fc99902"},
|
||||
{file = "RPi.GPIO-0.7.1.tar.gz", hash = "sha256:cd61c4b03c37b62bba4a5acfea9862749c33c618e0295e7e90aa4713fb373b70"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "samplerate"
|
||||
version = "0.2.2"
|
||||
@@ -2947,4 +2963,4 @@ test = ["pytest", "pytest-asyncio"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11"
|
||||
content-hash = "7c3c5cf6a836a9d7705e3b120610d98912cfd228b9abe162e15e6bed5bcb44a1"
|
||||
content-hash = "7bccf2978170ead195e1e8cff151823a5276823195a239622186fcec830154d9"
|
||||
|
||||
@@ -18,6 +18,7 @@ dependencies = [
|
||||
"python-dotenv (>=1.1.1,<2.0.0)",
|
||||
"smbus2 (>=0.5.0,<0.6.0)",
|
||||
"samplerate (>=0.2.2,<0.3.0)",
|
||||
"rpi-gpio (>=0.7.1,<0.8.0)",
|
||||
"pyalsaaudio @ git+ssh://git@gitea.summitwave.work:222/auracaster/sw_pyalsaaudio.git@b3d11582e03df6929b2e7acbaa1306afc7b8a6bc"
|
||||
]
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ class AuracastBigConfig(BaseModel):
|
||||
precode_wav: bool = False
|
||||
iso_que_len: int = 64
|
||||
num_bis: int = 1 # 1 = mono (FRONT_LEFT), 2 = stereo (FRONT_LEFT + FRONT_RIGHT)
|
||||
input_gain_db: float = 0.0 # Software gain boost in dB applied before LC3 encoding (0 = off, max 20)
|
||||
|
||||
class AuracastBigConfigDeu(AuracastBigConfig):
|
||||
id: int = 12
|
||||
@@ -112,4 +111,5 @@ class AuracastConfigGroup(AuracastGlobalConfig):
|
||||
bigs: List[AuracastBigConfig] = [
|
||||
AuracastBigConfigDeu(),
|
||||
]
|
||||
analog_gain: int = 50 # ADC gain level for analog mode (10-60%)
|
||||
analog_gain_db_left: float = 0.0 # ADC gain level for analog mode left channel (-12 to 18 dB)
|
||||
analog_gain_db_right: float = 0.0 # ADC gain level for analog mode right channel (-12 to 18 dB)
|
||||
|
||||
@@ -862,13 +862,6 @@ class Streamer():
|
||||
big['encoder'] = encoders[0]
|
||||
big['precoded'] = False
|
||||
|
||||
# Pre-compute software gain multiplier from dB config (0 dB = 1.0 = no change)
|
||||
gain_db = getattr(big_config[i], 'input_gain_db', 0.0)
|
||||
gain_db = max(0.0, min(20.0, float(gain_db)))
|
||||
big['_gain_linear'] = 10.0 ** (gain_db / 20.0) if gain_db > 0 else 0.0
|
||||
if big['_gain_linear'] > 0.0:
|
||||
logging.info("Software gain for BIG %d: +%.1f dB (linear %.3f)", i, gain_db, big['_gain_linear'])
|
||||
|
||||
logging.info("Streaming audio...")
|
||||
bigs = self.bigs
|
||||
self.is_streaming = True
|
||||
@@ -916,14 +909,6 @@ class Streamer():
|
||||
stream_finished[i] = True
|
||||
continue
|
||||
|
||||
# Apply software gain boost if configured (> 0 dB)
|
||||
gain_lin = big.get('_gain_linear', 0.0)
|
||||
if gain_lin > 0.0:
|
||||
pcm_arr = np.frombuffer(pcm_frame, dtype=np.int16).astype(np.float32)
|
||||
pcm_arr *= gain_lin
|
||||
np.clip(pcm_arr, -32768, 32767, out=pcm_arr)
|
||||
pcm_frame = pcm_arr.astype(np.int16).tobytes()
|
||||
|
||||
# Compute RMS audio level (normalized 0.0-1.0) for level monitoring
|
||||
pcm_samples = np.frombuffer(pcm_frame, dtype=np.int16).astype(np.float32)
|
||||
rms = np.sqrt(np.mean(pcm_samples ** 2)) / 32768.0 if len(pcm_samples) > 0 else 0.0
|
||||
|
||||
@@ -198,35 +198,64 @@ else:
|
||||
start_stream, stop_stream = render_stream_controls(is_streaming, "Start Auracast", "Stop Auracast", running_mode, secondary_is_streaming)
|
||||
|
||||
# Analog gain control (only for Analog mode, placed below start button)
|
||||
analog_gain_value = 50 # default (ALSA 10-60 range)
|
||||
software_boost_db = 0 # default
|
||||
analog_gain_db_left = 0 # default (dB)
|
||||
analog_gain_db_right = 0 # default (dB)
|
||||
if audio_mode == "Analog":
|
||||
saved_analog_gain = saved_settings.get('analog_gain', 50)
|
||||
# Convert persisted ALSA value (10-60) to display value (0-100)
|
||||
saved_display = int(round((saved_analog_gain - 10) * 100 / 50))
|
||||
saved_display = max(0, min(100, saved_display))
|
||||
analog_gain_display = st.slider(
|
||||
"Analog Input Gain",
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
value=saved_display,
|
||||
step=5,
|
||||
disabled=is_streaming,
|
||||
format="%d%%",
|
||||
help="ADC gain level for both analog inputs. Default is 80%."
|
||||
)
|
||||
# Map display value (0-100) back to ALSA range (10-60)
|
||||
analog_gain_value = int(round(10 + analog_gain_display * 50 / 100))
|
||||
saved_boost = saved_settings.get('software_boost_db', 0)
|
||||
software_boost_db = st.slider(
|
||||
"Boost",
|
||||
min_value=0,
|
||||
max_value=20,
|
||||
value=min(int(saved_boost), 20),
|
||||
step=1,
|
||||
disabled=is_streaming,
|
||||
help="Digital gain boost applied before encoding (0-20 dB). Use this when the line-level signal is too quiet even at max ADC gain. Higher values may cause clipping on loud signals."
|
||||
if '_analog_gain_db_left' not in st.session_state:
|
||||
st.session_state['_analog_gain_db_left'] = max(-12, min(18, int(saved_settings.get('analog_gain_db_left', 0))))
|
||||
if '_analog_gain_db_right' not in st.session_state:
|
||||
st.session_state['_analog_gain_db_right'] = max(-12, min(18, int(saved_settings.get('analog_gain_db_right', 0))))
|
||||
if '_gain_link_channels' not in st.session_state:
|
||||
st.session_state['_gain_link_channels'] = True
|
||||
|
||||
link_channels = st.checkbox(
|
||||
"Link audio channel gain",
|
||||
key='_gain_link_channels',
|
||||
help="When enabled, Ch 2 mirrors Ch 1."
|
||||
)
|
||||
_gain_col1, _gain_col2 = st.columns(2)
|
||||
with _gain_col1:
|
||||
analog_gain_db_left = st.slider(
|
||||
"Ch 1 Input Gain",
|
||||
min_value=-12,
|
||||
max_value=18,
|
||||
key='_analog_gain_db_left',
|
||||
step=1,
|
||||
format="%d dB",
|
||||
help="ADC gain for channel 1 (-12 to 18 dB). Default is 0 dB."
|
||||
)
|
||||
with _gain_col2:
|
||||
if link_channels:
|
||||
st.session_state['_analog_gain_db_right'] = analog_gain_db_left
|
||||
elif st.session_state.get('_prev_gain_link_channels', True):
|
||||
# Transition: just unlinked — seed Ch 2 from Ch 1 so it doesn't jump to min
|
||||
st.session_state['_analog_gain_db_right'] = analog_gain_db_left
|
||||
st.session_state['_prev_gain_link_channels'] = link_channels
|
||||
analog_gain_db_right = st.slider(
|
||||
"Ch 2 Input Gain",
|
||||
min_value=-12,
|
||||
max_value=18,
|
||||
key='_analog_gain_db_right',
|
||||
step=1,
|
||||
format="%d dB",
|
||||
disabled=link_channels,
|
||||
help="Uncheck 'Link audio channel gain' to adjust Ch 2 independently." if link_channels else "ADC gain for channel 2 (-12 to 18 dB). Default is 0 dB."
|
||||
)
|
||||
# Apply gain live while streaming whenever either slider value changes
|
||||
if is_streaming:
|
||||
prev_left = st.session_state.get('_prev_analog_gain_db_left')
|
||||
prev_right = st.session_state.get('_prev_analog_gain_db_right')
|
||||
if prev_left != analog_gain_db_left or prev_right != analog_gain_db_right:
|
||||
try:
|
||||
requests.post(
|
||||
f"{BACKEND_URL}/adc_gain",
|
||||
json={"gain_db_left": analog_gain_db_left, "gain_db_right": analog_gain_db_right},
|
||||
timeout=1,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
st.session_state['_prev_analog_gain_db_left'] = analog_gain_db_left
|
||||
st.session_state['_prev_analog_gain_db_right'] = analog_gain_db_right
|
||||
|
||||
# Audio level monitor (checkbox, not persisted across reloads)
|
||||
show_level_monitor = st.checkbox("Audio level monitor", value=False, disabled=not is_streaming,
|
||||
@@ -684,8 +713,8 @@ else:
|
||||
'immediate_rendering': immediate_rendering2,
|
||||
'presentation_delay_ms': presentation_delay_ms2,
|
||||
'qos_preset': qos_preset2,
|
||||
'analog_gain': analog_gain_value,
|
||||
'software_boost_db': software_boost_db,
|
||||
'analog_gain_db_left': analog_gain_db_left,
|
||||
'analog_gain_db_right': analog_gain_db_right,
|
||||
}
|
||||
|
||||
radio1_cfg = {
|
||||
@@ -701,8 +730,8 @@ else:
|
||||
'presentation_delay_ms': presentation_delay_ms1,
|
||||
'qos_preset': qos_preset1,
|
||||
'stereo_mode': stereo_enabled,
|
||||
'analog_gain': analog_gain_value,
|
||||
'software_boost_db': software_boost_db,
|
||||
'analog_gain_db_left': analog_gain_db_left,
|
||||
'analog_gain_db_right': analog_gain_db_right,
|
||||
}
|
||||
|
||||
if audio_mode == "Network - Dante":
|
||||
@@ -1585,10 +1614,10 @@ if start_stream:
|
||||
immediate_rendering=bool(cfg['immediate_rendering']),
|
||||
presentation_delay_us=int(cfg['presentation_delay_ms'] * 1000),
|
||||
qos_config=QOS_PRESET_MAP[cfg['qos_preset']],
|
||||
analog_gain=cfg.get('analog_gain', 50),
|
||||
analog_gain_db_left=cfg.get('analog_gain_db_left', 0.0),
|
||||
analog_gain_db_right=cfg.get('analog_gain_db_right', 0.0),
|
||||
bigs=[
|
||||
auracast_config.AuracastBigConfig(
|
||||
id=cfg.get('id', 123456),
|
||||
code=(cfg['stream_passwort'].strip() or None),
|
||||
name=cfg['name'],
|
||||
program_info=cfg['program_info'],
|
||||
@@ -1599,7 +1628,6 @@ if start_stream:
|
||||
sampling_frequency=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
num_bis=channels, # 1=mono, 2=stereo - this determines the behavior
|
||||
input_gain_db=float(cfg.get('software_boost_db', 0)),
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -1643,8 +1671,6 @@ if start_stream:
|
||||
if not stream.get('input_device'):
|
||||
continue
|
||||
|
||||
stream_id = radio_id * 1000 + i + 1 # Unique ID per stream
|
||||
|
||||
# Check if this specific stream uses stereo (dante_stereo_X_Y device)
|
||||
input_device = stream['input_device']
|
||||
stream_is_stereo = is_stereo_mode or input_device.startswith('dante_stereo_')
|
||||
@@ -1652,7 +1678,6 @@ if start_stream:
|
||||
num_channels = 2 if stream_is_stereo else 1
|
||||
|
||||
bigs.append(auracast_config.AuracastBigConfig(
|
||||
id=stream_id,
|
||||
code=(stream.get('stream_password', '').strip() or None),
|
||||
name=stream['name'],
|
||||
program_info=stream['program_info'],
|
||||
@@ -1755,7 +1780,21 @@ if is_started or is_stopped:
|
||||
# System expander (collapsed)
|
||||
############################
|
||||
with st.expander("System control", expanded=False):
|
||||
|
||||
|
||||
st.subheader("Status LED")
|
||||
led_enabled_current = bool(saved_settings.get("led_enabled", True))
|
||||
led_enabled = st.checkbox(
|
||||
"Blue LED on while transmitting",
|
||||
value=led_enabled_current,
|
||||
help="When enabled, the blue LED on GPIO pin 12 lights up while the stream is active."
|
||||
)
|
||||
if led_enabled != led_enabled_current:
|
||||
try:
|
||||
requests.post(f"{BACKEND_URL}/set_led_enabled", json={"led_enabled": led_enabled}, timeout=2)
|
||||
except Exception as e:
|
||||
st.error(f"Failed to update LED setting: {e}")
|
||||
st.rerun()
|
||||
|
||||
st.subheader("System temperatures")
|
||||
temp_col1, temp_col2, temp_col3 = st.columns([1, 1, 1])
|
||||
with temp_col1:
|
||||
|
||||
@@ -3,6 +3,7 @@ TODO: in the future the multicaster objects should run in their own threads or e
|
||||
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import logging as log
|
||||
import json
|
||||
from datetime import datetime
|
||||
@@ -27,6 +28,53 @@ from auracast.utils.sounddevice_utils import (
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Blue LED on GPIO pin 12 (BCM) – turns on while transmitting
|
||||
LED_PIN = 12
|
||||
try:
|
||||
import RPi.GPIO as _GPIO
|
||||
_GPIO.setmode(_GPIO.BCM)
|
||||
_GPIO.setup(LED_PIN, _GPIO.OUT)
|
||||
_GPIO_AVAILABLE = True
|
||||
except Exception:
|
||||
_GPIO_AVAILABLE = False
|
||||
_GPIO = None # type: ignore
|
||||
|
||||
_LED_ENABLED: bool = True # toggled via /set_led_enabled
|
||||
_LED_SETTINGS_FILE = os.path.join(os.path.dirname(__file__), 'led_settings.json')
|
||||
|
||||
def _load_led_settings() -> None:
|
||||
global _LED_ENABLED
|
||||
try:
|
||||
if os.path.exists(_LED_SETTINGS_FILE):
|
||||
with open(_LED_SETTINGS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
_LED_ENABLED = bool(data.get('led_enabled', True))
|
||||
except Exception:
|
||||
_LED_ENABLED = True
|
||||
|
||||
def _save_led_settings() -> None:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(_LED_SETTINGS_FILE), exist_ok=True)
|
||||
with open(_LED_SETTINGS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump({'led_enabled': _LED_ENABLED}, f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _led_on():
|
||||
if _GPIO_AVAILABLE and _LED_ENABLED:
|
||||
try:
|
||||
_GPIO.output(LED_PIN, _GPIO.LOW)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _led_off():
|
||||
if _GPIO_AVAILABLE:
|
||||
try:
|
||||
_GPIO.output(LED_PIN, _GPIO.HIGH)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Configure bumble debug logging
|
||||
# log.getLogger('bumble').setLevel(log.DEBUG)
|
||||
|
||||
@@ -137,6 +185,10 @@ def save_settings(persisted: dict, secondary: bool = False) -> None:
|
||||
def gen_random_add() -> str:
|
||||
return ':'.join(['%02X' % random.randint(0, 255) for _ in range(6)])
|
||||
|
||||
def gen_random_broadcast_id() -> int:
|
||||
"""Generate a random 24-bit Broadcast ID (1..0xFFFFFF)."""
|
||||
return random.randint(1, 0xFFFFFF)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Allow CORS for frontend on localhost
|
||||
@@ -242,16 +294,18 @@ async def _init_i2c_on_startup() -> None:
|
||||
log.warning("Exception running i2cget (%s): %s", " ".join(read_cmd), e, exc_info=True)
|
||||
|
||||
|
||||
async def _set_adc_level(gain_percent: int = 50) -> None:
|
||||
"""Set ADC mixer level.
|
||||
async def _set_adc_level(gain_db_left: float = 0.0, gain_db_right: float = 0.0) -> None:
|
||||
"""Set ADC mixer gain in dB for left and right channels independently.
|
||||
|
||||
Runs: amixer -c 1 set 'ADC' x%
|
||||
Runs: amixer -c 2 sset ADC {gain_db_left}dB,{gain_db_right}dB
|
||||
|
||||
Args:
|
||||
gain_percent: Gain level 10-60, default 50 (above 60% causes clipping/silence)
|
||||
gain_db_left: Left channel gain in dB (-12 to 18), default 0
|
||||
gain_db_right: Right channel gain in dB (-12 to 18), default 0
|
||||
"""
|
||||
gain_percent = max(10, min(60, gain_percent))
|
||||
cmd = ["amixer", "-c", "1", "set", "ADC", f"{gain_percent}%"]
|
||||
gain_db_left = max(-12.0, min(18.0, gain_db_left))
|
||||
gain_db_right = max(-12.0, min(18.0, gain_db_right))
|
||||
cmd = ["amixer", "-c", "2", "sset", "ADC", "--", f"{int(gain_db_left)}dB,{int(gain_db_right)}dB"]
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
@@ -260,15 +314,46 @@ async def _set_adc_level(gain_percent: int = 50) -> None:
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
log.warning(
|
||||
log.error(
|
||||
"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())
|
||||
read_proc = await asyncio.create_subprocess_exec(
|
||||
"amixer", "-c", "2", "sget", "ADC",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
read_stdout, read_stderr = await read_proc.communicate()
|
||||
if read_proc.returncode != 0:
|
||||
log.error(
|
||||
"amixer ADC sget failed (rc=%s): %s",
|
||||
read_proc.returncode,
|
||||
(read_stderr or b"").decode(errors="ignore").strip(),
|
||||
)
|
||||
else:
|
||||
sget_output = (read_stdout or b"").decode(errors="ignore")
|
||||
actual = {}
|
||||
for line in sget_output.splitlines():
|
||||
for ch_key, ch_name in (("left", "Front Left"), ("right", "Front Right")):
|
||||
if ch_name in line:
|
||||
m = re.search(r'\[(-?\d+(?:\.\d+)?)dB\]', line)
|
||||
if m:
|
||||
actual[ch_key] = round(float(m.group(1)))
|
||||
expected_left = int(gain_db_left)
|
||||
expected_right = int(gain_db_right)
|
||||
if actual.get("left") != expected_left or actual.get("right") != expected_right:
|
||||
mismatch = (
|
||||
f"ADC level mismatch after set: expected L={expected_left}dB R={expected_right}dB, "
|
||||
f"got L={actual.get('left')}dB R={actual.get('right')}dB"
|
||||
)
|
||||
log.error(mismatch)
|
||||
else:
|
||||
log.info("ADC level set successfully: L=%sdB R=%sdB", expected_left, expected_right)
|
||||
except Exception as e:
|
||||
log.warning("Exception running amixer ADC level command: %s", e, exc_info=True)
|
||||
log.error("Exception running amixer ADC level command: %s", e, exc_info=True)
|
||||
|
||||
|
||||
async def _stop_all() -> bool:
|
||||
@@ -288,6 +373,7 @@ async def _stop_all() -> bool:
|
||||
was_running = True
|
||||
finally:
|
||||
multicaster2 = None
|
||||
_led_off()
|
||||
return was_running
|
||||
|
||||
async def _status_primary() -> dict:
|
||||
@@ -347,8 +433,9 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
|
||||
if input_device_name in ('ch1', 'ch2'):
|
||||
audio_mode_persist = 'Analog'
|
||||
# Set ADC gain level for analog mode
|
||||
analog_gain = getattr(conf, 'analog_gain', 50)
|
||||
await _set_adc_level(analog_gain)
|
||||
analog_gain_db_left = getattr(conf, 'analog_gain_db_left', 0.0)
|
||||
analog_gain_db_right = getattr(conf, 'analog_gain_db_right', 0.0)
|
||||
await _set_adc_level(analog_gain_db_left, analog_gain_db_right)
|
||||
elif input_device_name in dante_channels:
|
||||
audio_mode_persist = 'Network - Dante'
|
||||
else:
|
||||
@@ -455,10 +542,12 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
|
||||
|
||||
conf.qos_config.max_transport_latency_ms = int(conf.qos_config.number_of_retransmissions) * 10 + 3
|
||||
|
||||
# Only generate a new random_address if the BIG is still at the model default.
|
||||
# Generate fresh random_address and broadcast ID for any BIG still at model defaults.
|
||||
for big in conf.bigs:
|
||||
if not getattr(big, 'random_address', None) or big.random_address == DEFAULT_RANDOM_ADDRESS:
|
||||
big.random_address = gen_random_add()
|
||||
if big.id == DEFAULT_BIG_ID:
|
||||
big.id = gen_random_broadcast_id()
|
||||
|
||||
# Log the final, fully-updated configuration just before creating the Multicaster
|
||||
log.info('Final multicaster config (transport=%s):\n %s', transport, conf.model_dump_json(indent=2))
|
||||
@@ -469,6 +558,7 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
|
||||
auto_started = False
|
||||
if any(isinstance(big.audio_source, str) and (big.audio_source.startswith("device:") or big.audio_source.startswith("alsa:") or big.audio_source.startswith("file:")) for big in conf.bigs):
|
||||
await mc.start_streaming()
|
||||
_led_on()
|
||||
auto_started = True
|
||||
|
||||
demo_count = sum(1 for big in conf.bigs if isinstance(big.audio_source, str) and big.audio_source.startswith('file:'))
|
||||
@@ -493,8 +583,8 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
|
||||
'immediate_rendering': getattr(conf, 'immediate_rendering', False),
|
||||
'assisted_listening_stream': getattr(conf, 'assisted_listening_stream', False),
|
||||
'analog_stereo_mode': getattr(conf.bigs[0], 'analog_stereo_mode', False) if conf.bigs else False,
|
||||
'analog_gain': getattr(conf, 'analog_gain', 50),
|
||||
'software_boost_db': getattr(conf.bigs[0], 'input_gain_db', 0.0) if conf.bigs else 0.0,
|
||||
'analog_gain_db_left': getattr(conf, 'analog_gain_db_left', 0.0),
|
||||
'analog_gain_db_right': getattr(conf, 'analog_gain_db_right', 0.0),
|
||||
'stream_password': (conf.bigs[0].code if conf.bigs and getattr(conf.bigs[0], 'code', None) else None),
|
||||
'big_ids': [getattr(big, 'id', DEFAULT_BIG_ID) for big in conf.bigs],
|
||||
'big_random_addresses': [getattr(big, 'random_address', DEFAULT_RANDOM_ADDRESS) for big in conf.bigs],
|
||||
@@ -529,6 +619,16 @@ async def initialize2(conf: auracast_config.AuracastConfigGroup):
|
||||
multicaster2 = mc
|
||||
save_settings(persisted, secondary=True)
|
||||
|
||||
@app.post("/set_led_enabled")
|
||||
async def set_led_enabled(body: dict):
|
||||
"""Enable or disable the blue status LED. Persisted across restarts."""
|
||||
global _LED_ENABLED
|
||||
_LED_ENABLED = bool(body.get("led_enabled", True))
|
||||
_save_led_settings()
|
||||
if not _LED_ENABLED:
|
||||
_led_off()
|
||||
return {"led_enabled": _LED_ENABLED}
|
||||
|
||||
@app.post("/stop_audio")
|
||||
async def stop_audio():
|
||||
"""Stops streaming on both multicaster1 and multicaster2 (worker thread)."""
|
||||
@@ -557,6 +657,28 @@ async def stop_audio():
|
||||
log.error("Exception in /stop_audio: %s", traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/adc_gain")
|
||||
async def set_adc_gain(payload: dict):
|
||||
"""Set ADC gain in dB for left and right channels without restarting the stream.
|
||||
|
||||
Body: {"gain_db_left": float, "gain_db_right": float}
|
||||
"""
|
||||
try:
|
||||
gain_db_left = float(payload.get("gain_db_left", 0.0))
|
||||
gain_db_right = float(payload.get("gain_db_right", 0.0))
|
||||
await _set_adc_level(gain_db_left, gain_db_right)
|
||||
# Persist the new values so they survive a restart
|
||||
for load_fn, save_fn in [(load_stream_settings, save_stream_settings), (load_stream_settings2, save_stream_settings2)]:
|
||||
s = load_fn() or {}
|
||||
if s:
|
||||
s['analog_gain_db_left'] = gain_db_left
|
||||
s['analog_gain_db_right'] = gain_db_right
|
||||
save_fn(s)
|
||||
return {"status": "ok", "gain_db_left": gain_db_left, "gain_db_right": gain_db_right}
|
||||
except Exception as e:
|
||||
log.error("Exception in /adc_gain: %s", traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/stream_lc3")
|
||||
async def send_audio(audio_data: dict[str, str]):
|
||||
"""Sends a block of pre-coded LC3 audio via the worker."""
|
||||
@@ -592,6 +714,7 @@ async def get_status():
|
||||
secondary.update(secondary_persisted)
|
||||
status["secondary"] = secondary
|
||||
status["secondary_is_streaming"] = bool(secondary.get("is_streaming", False))
|
||||
status["led_enabled"] = _LED_ENABLED
|
||||
|
||||
return status
|
||||
|
||||
@@ -752,7 +875,6 @@ async def _autostart_from_settings():
|
||||
iso_que_len=1,
|
||||
sampling_frequency=rate,
|
||||
octets_per_frame=octets,
|
||||
input_gain_db=float(settings.get('software_boost_db', 0)),
|
||||
)
|
||||
]
|
||||
conf = auracast_config.AuracastConfigGroup(
|
||||
@@ -762,7 +884,8 @@ async def _autostart_from_settings():
|
||||
immediate_rendering=immediate_rendering,
|
||||
assisted_listening_stream=assisted_listening_stream,
|
||||
presentation_delay_us=pres_delay if pres_delay is not None else 40000,
|
||||
analog_gain=settings.get('analog_gain', 50),
|
||||
analog_gain_db_left=settings.get('analog_gain_db_left', 0.0),
|
||||
analog_gain_db_right=settings.get('analog_gain_db_right', 0.0),
|
||||
bigs=bigs,
|
||||
)
|
||||
# Set num_bis for stereo mode if needed
|
||||
@@ -907,7 +1030,6 @@ async def _autostart_from_settings():
|
||||
iso_que_len=1,
|
||||
sampling_frequency=rate,
|
||||
octets_per_frame=octets,
|
||||
input_gain_db=float(settings.get('software_boost_db', 0)),
|
||||
)
|
||||
]
|
||||
conf = auracast_config.AuracastConfigGroup(
|
||||
@@ -917,7 +1039,8 @@ async def _autostart_from_settings():
|
||||
immediate_rendering=immediate_rendering,
|
||||
assisted_listening_stream=assisted_listening_stream,
|
||||
presentation_delay_us=pres_delay if pres_delay is not None else 40000,
|
||||
analog_gain=settings.get('analog_gain', 50),
|
||||
analog_gain_db_left=settings.get('analog_gain_db_left', 0.0),
|
||||
analog_gain_db_right=settings.get('analog_gain_db_right', 0.0),
|
||||
bigs=bigs,
|
||||
)
|
||||
conf.qos_config = QOS_PRESET_MAP.get(saved_qos_preset, QOS_PRESET_MAP["Fast"])
|
||||
@@ -939,6 +1062,7 @@ async def _autostart_from_settings():
|
||||
async def _startup_autostart_event():
|
||||
# Spawn the autostart task without blocking startup
|
||||
log.info("[STARTUP] Auracast multicast server startup: initializing settings cache, I2C, and PipeWire cache")
|
||||
_led_off()
|
||||
|
||||
# Run install_asoundconf.sh script
|
||||
script_path = os.path.join(os.path.dirname(__file__), '..', '..', 'misc', 'install_asoundconf.sh')
|
||||
@@ -952,10 +1076,11 @@ async def _startup_autostart_event():
|
||||
log.error(f"[STARTUP] Error running install_asoundconf.sh: {str(e)}")
|
||||
|
||||
# Hydrate settings cache once to avoid disk I/O during /status
|
||||
_load_led_settings()
|
||||
_init_settings_cache_from_disk()
|
||||
await _init_i2c_on_startup()
|
||||
# Ensure ADC mixer level is set at startup (default 50%)
|
||||
await _set_adc_level(50)
|
||||
# Ensure ADC mixer level is set at startup (default 0 dB)
|
||||
await _set_adc_level(0.0, 0.0)
|
||||
refresh_pw_cache()
|
||||
log.info("[STARTUP] Scheduling autostart task")
|
||||
asyncio.create_task(_autostart_from_settings())
|
||||
@@ -1246,26 +1371,12 @@ async def system_update():
|
||||
log.error("git checkout failed: %s", stderr.decode())
|
||||
raise HTTPException(status_code=500, detail=f"git checkout failed: {stderr.decode()}")
|
||||
|
||||
# 2. Run poetry install (use full path as poetry is in user's ~/.local/bin)
|
||||
poetry_path = os.path.expanduser("~/.local/bin/poetry")
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
poetry_path, "install",
|
||||
cwd=project_root,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
log.error("poetry install failed: %s", stderr.decode())
|
||||
raise HTTPException(status_code=500, detail=f"poetry install failed: {stderr.decode()}")
|
||||
|
||||
# 3. Restart services via the update script
|
||||
update_script = os.path.join(project_root, 'src', 'service', 'update_and_run_server_and_frontend.sh')
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
# 2. Hand off remaining work to the (now-updated) system_update.sh script
|
||||
update_script = os.path.join(os.path.dirname(__file__), 'system_update.sh')
|
||||
log.info("Handing off to system_update.sh...")
|
||||
await asyncio.create_subprocess_exec(
|
||||
"bash", update_script,
|
||||
cwd=project_root,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
# Don't wait for completion as we'll be restarted
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
90
src/auracast/server/system_update.sh
Normal file
90
src/auracast/server/system_update.sh
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# system_update.sh - Runs after git checkout in the Python system_update endpoint.
|
||||
# Called with the current working directory = project root.
|
||||
# All output is also written to /tmp/system_update.log for debugging.
|
||||
|
||||
exec > >(tee -a /tmp/system_update.log) 2>&1
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
POETRY="$HOME/.local/bin/poetry"
|
||||
OPENOCD_SRC="$HOME/sw_openocd"
|
||||
OPENOCD_REPO="ssh://git@gitea.summitwave.work:222/auracaster/sw_openocd.git"
|
||||
OPENOCD_BRANCH="change-8818"
|
||||
OPENOCD_MARKER="$OPENOCD_SRC/.last_built_commit"
|
||||
OPENOCD_DIR="$PROJECT_ROOT/src/openocd"
|
||||
|
||||
echo "[system_update] Starting post-checkout update. project_root=$PROJECT_ROOT"
|
||||
|
||||
# 1. poetry install
|
||||
echo "[system_update] Running poetry install..."
|
||||
(cd "$PROJECT_ROOT" && "$POETRY" install)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "[system_update] ERROR: poetry install failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Clone/update and build sw_openocd if needed
|
||||
if [ ! -d "$OPENOCD_SRC" ]; then
|
||||
echo "[system_update] Installing sw_openocd build dependencies..."
|
||||
sudo apt install -y git build-essential libtool autoconf texinfo \
|
||||
libusb-1.0-0-dev libftdi1-dev libhidapi-dev pkg-config || \
|
||||
echo "[system_update] WARNING: apt install deps had errors, continuing"
|
||||
sudo apt-get install -y pkg-config libjim-dev || \
|
||||
echo "[system_update] WARNING: apt-get install libjim-dev had errors, continuing"
|
||||
|
||||
echo "[system_update] Cloning sw_openocd branch $OPENOCD_BRANCH..."
|
||||
git clone --branch "$OPENOCD_BRANCH" --single-branch "$OPENOCD_REPO" "$OPENOCD_SRC"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "[system_update] ERROR: git clone sw_openocd failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "[system_update] Updating sw_openocd..."
|
||||
git -C "$OPENOCD_SRC" fetch origin "$OPENOCD_BRANCH"
|
||||
git -C "$OPENOCD_SRC" checkout "$OPENOCD_BRANCH"
|
||||
git -C "$OPENOCD_SRC" pull
|
||||
fi
|
||||
|
||||
OPENOCD_COMMIT=$(git -C "$OPENOCD_SRC" rev-parse HEAD)
|
||||
LAST_BUILT=""
|
||||
[ -f "$OPENOCD_MARKER" ] && LAST_BUILT=$(cat "$OPENOCD_MARKER")
|
||||
|
||||
if [ "$OPENOCD_COMMIT" != "$LAST_BUILT" ]; then
|
||||
echo "[system_update] Building sw_openocd (commit $OPENOCD_COMMIT)..."
|
||||
|
||||
(cd "$OPENOCD_SRC" && ./bootstrap)
|
||||
if [ $? -ne 0 ]; then echo "[system_update] ERROR: openocd bootstrap failed"; exit 1; fi
|
||||
|
||||
(cd "$OPENOCD_SRC" && ./configure --enable-bcm2835gpio --enable-sysfsgpio)
|
||||
if [ $? -ne 0 ]; then echo "[system_update] ERROR: openocd configure failed"; exit 1; fi
|
||||
|
||||
(cd "$OPENOCD_SRC" && make)
|
||||
if [ $? -ne 0 ]; then echo "[system_update] ERROR: openocd make failed"; exit 1; fi
|
||||
|
||||
(cd "$OPENOCD_SRC" && sudo make install)
|
||||
if [ $? -ne 0 ]; then echo "[system_update] ERROR: openocd make install failed"; exit 1; fi
|
||||
|
||||
echo "$OPENOCD_COMMIT" > "$OPENOCD_MARKER"
|
||||
echo "[system_update] sw_openocd built and installed (commit $OPENOCD_COMMIT)"
|
||||
else
|
||||
echo "[system_update] sw_openocd up to date (commit $OPENOCD_COMMIT), skipping build"
|
||||
fi
|
||||
|
||||
# 3. Flash firmware to both SWD interfaces
|
||||
FLASH_SCRIPT="$OPENOCD_DIR/flash.sh"
|
||||
HEX_FILE="$OPENOCD_DIR/merged.hex"
|
||||
|
||||
for IFACE in swd0 swd1; do
|
||||
echo "[system_update] Flashing $IFACE..."
|
||||
(cd "$OPENOCD_DIR" && bash "$FLASH_SCRIPT" -i "$IFACE" -f "$HEX_FILE")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "[system_update] ERROR: flash $IFACE failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "[system_update] Flash $IFACE complete"
|
||||
done
|
||||
|
||||
# 4. Restart services (this will kill this process too)
|
||||
echo "[system_update] Restarting services..."
|
||||
bash "$PROJECT_ROOT/src/service/update_and_run_server_and_frontend.sh"
|
||||
56
src/openocd/flash.sh
Normal file
56
src/openocd/flash.sh
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
INTERFACE="swd0"
|
||||
HEX_FILE=""
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 -f <hex_file> [-i swd0|swd1]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
while getopts "f:i:h" opt; do
|
||||
case "$opt" in
|
||||
f) HEX_FILE="$OPTARG" ;;
|
||||
i)
|
||||
if [[ "$OPTARG" == "swd0" || "$OPTARG" == "swd1" ]]; then
|
||||
INTERFACE="$OPTARG"
|
||||
else
|
||||
usage
|
||||
fi
|
||||
;;
|
||||
h) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$HEX_FILE" ]] || usage
|
||||
[[ -f "$HEX_FILE" ]] || { echo "HEX file not found: $HEX_FILE"; exit 1; }
|
||||
|
||||
sudo openocd \
|
||||
-f ./raspberrypi-${INTERFACE}.cfg \
|
||||
-c "init" \
|
||||
-c "reset init" \
|
||||
-c "flash banks" \
|
||||
-c "flash write_image $HEX_FILE" \
|
||||
-c "verify_image $HEX_FILE" \
|
||||
-c "reset run" \
|
||||
-c "shutdown"
|
||||
|
||||
sudo openocd \
|
||||
-f ./raspberrypi-${INTERFACE}.cfg \
|
||||
-c "init" \
|
||||
-c "nrf54l.dap apreg 2 0x000 0x1" \
|
||||
-c "sleep 100" \
|
||||
-c "nrf54l.dap apreg 2 0x000 0x0" \
|
||||
-c "shutdown"
|
||||
|
||||
sudo openocd \
|
||||
-f ./raspberrypi-${INTERFACE}.cfg \
|
||||
-c "init" \
|
||||
-c "nrf54l.dap apreg 2 0x000 0x4" \
|
||||
-c "sleep 100" \
|
||||
-c "nrf54l.dap apreg 2 0x000 0x0" \
|
||||
-c "shutdown"
|
||||
|
||||
echo "Flashing complete."
|
||||
13111
src/openocd/merged.hex
Normal file
13111
src/openocd/merged.hex
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,4 +5,9 @@ adapter gpio swdio 26
|
||||
#adapter gpio trst 26
|
||||
#reset_config trst_only
|
||||
|
||||
|
||||
source [find target/nordic/nrf54l.cfg]
|
||||
|
||||
flash bank $_CHIPNAME.flash nrf54 0x00000000 0 0 0 $_TARGETNAME
|
||||
|
||||
adapter speed 1000
|
||||
|
||||
@@ -5,4 +5,9 @@ adapter gpio swdio 24
|
||||
#adapter gpio trst 27
|
||||
#reset_config trst_only
|
||||
|
||||
|
||||
source [find target/nordic/nrf54l.cfg]
|
||||
|
||||
flash bank $_CHIPNAME.flash nrf54 0x00000000 0 0 0 $_TARGETNAME
|
||||
|
||||
adapter speed 1000
|
||||
|
||||
Reference in New Issue
Block a user