diff --git a/.gitignore b/.gitignore index 3660390..f23cc0f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ src/auracast/available_samples.txt src/auracast/server/stream_settings2.json src/scripts/temperature_log* -src/auracast/server/recordings/ \ No newline at end of file +src/auracast/server/recordings/ +src/auracast/server/led_settings.json diff --git a/poetry.lock b/poetry.lock index c609b04..d853482 100644 --- a/poetry.lock +++ b/poetry.lock @@ -270,51 +270,6 @@ optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "av-14.4.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:10219620699a65b9829cfa08784da2ed38371f1a223ab8f3523f440a24c8381c"}, - {file = "av-14.4.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8bac981fde1c05e231df9f73a06ed9febce1f03fb0f1320707ac2861bba2567f"}, - {file = "av-14.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc634ed5bdeb362f0523b73693b079b540418d35d7f3003654f788ae6c317eef"}, - {file = "av-14.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23973ed5c5bec9565094d2b3643f10a6996707ddffa5252e112d578ad34aa9ae"}, - {file = "av-14.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0655f7207db6a211d7cedb8ac6a2f7ccc9c4b62290130e393a3fd99425247311"}, - {file = "av-14.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1edaab73319bfefe53ee09c4b1cf7b141ea7e6678a0a1c62f7bac1e2c68ec4e7"}, - {file = "av-14.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b54838fa17c031ffd780df07b9962fac1be05220f3c28468f7fe49474f1bf8d2"}, - {file = "av-14.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f4b59ac6c563b9b6197299944145958a8ec34710799fd851f1a889b0cbcd1059"}, - {file = "av-14.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:a0192a584fae9f6cedfac03c06d5bf246517cdf00c8779bc33414404796a526e"}, - {file = "av-14.4.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5b21d5586a88b9fce0ab78e26bd1c38f8642f8e2aad5b35e619f4d202217c701"}, - {file = "av-14.4.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:cf8762d90b0f94a20c9f6e25a94f1757db5a256707964dfd0b1d4403e7a16835"}, - {file = "av-14.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0ac9f08920c7bbe0795319689d901e27cb3d7870b9a0acae3f26fc9daa801a6"}, - {file = "av-14.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56d9ad2afdb638ec0404e962dc570960aae7e08ae331ad7ff70fbe99a6cf40e"}, - {file = "av-14.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bed513cbcb3437d0ae47743edc1f5b4a113c0b66cdd4e1aafc533abf5b2fbf2"}, - {file = "av-14.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d030c2d3647931e53d51f2f6e0fcf465263e7acf9ec6e4faa8dbfc77975318c3"}, - {file = "av-14.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1cc21582a4f606271d8c2036ec7a6247df0831050306c55cf8a905701d0f0474"}, - {file = "av-14.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce7c9cd452153d36f1b1478f904ed5f9ab191d76db873bdd3a597193290805d4"}, - {file = "av-14.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd261e31cc6b43ca722f80656c39934199d8f2eb391e0147e704b6226acebc29"}, - {file = "av-14.4.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:a53e682b239dd23b4e3bc9568cfb1168fc629ab01925fdb2e7556eb426339e94"}, - {file = "av-14.4.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:5aa0b901751a32703fa938d2155d56ce3faf3630e4a48d238b35d2f7e49e5395"}, - {file = "av-14.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b316fed3597675fe2aacfed34e25fc9d5bb0196dc8c0b014ae5ed4adda48de"}, - {file = "av-14.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a587b5c5014c3c0e16143a0f8d99874e46b5d0c50db6111aa0b54206b5687c81"}, - {file = "av-14.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d53f75e8ac1ec8877a551c0db32a83c0aaeae719d05285281eaaba211bbc30"}, - {file = "av-14.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c8558cfde79dd8fc92d97c70e0f0fa8c94c7a66f68ae73afdf58598f0fe5e10d"}, - {file = "av-14.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:455b6410dea0ab2d30234ffb28df7d62ca3cdf10708528e247bec3a4cdcced09"}, - {file = "av-14.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1661efbe9d975f927b8512d654704223d936f39016fad2ddab00aee7c40f412c"}, - {file = "av-14.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbbeef1f421a3461086853d6464ad5526b56ffe8ccb0ab3fd0a1f121dfbf26ad"}, - {file = "av-14.4.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3d2aea7c602b105363903e4017103bc4b60336e7aff80e1c22e8b4ec09fd125f"}, - {file = "av-14.4.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:38c18f036aeb6dc9abf5e867d998c867f9ec93a5f722b60721fdffc123bbb2ae"}, - {file = "av-14.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58c1e18c8be73b6eada2d9ec397852ec74ebe51938451bdf83644a807189d6c8"}, - {file = "av-14.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4c32ff03a357feb030634f093089a73cb474b04efe7fbfba31f229cb2fab115"}, - {file = "av-14.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af31d16ae25964a6a02e09cc132b9decd5ee493c5dcb21bcdf0d71b2d6adbd59"}, - {file = "av-14.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9fb297009e528f4851d25f3bb2781b2db18b59b10aed10240e947b77c582fb7"}, - {file = "av-14.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:573314cb9eafec2827dc98c416c965330dc7508193adbccd281700d8673b9f0a"}, - {file = "av-14.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f82ab27ee57c3b80eb50a5293222307dfdc02f810ea41119078cfc85ea3cf9a8"}, - {file = "av-14.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f682003bbcaac620b52f68ff0e85830fff165dea53949e217483a615993ca20"}, - {file = "av-14.4.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8ff683777e0bb3601f7cfb4545dca25db92817585330b773e897e1f6f9d612f7"}, - {file = "av-14.4.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:fe372acf7b1814bc2b16d89161609db63f81dad88684da76d26dd32cd1c16f92"}, - {file = "av-14.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de869030eb8acfdfe39f39965de3a899dcde9b08df2db41f183c6166ca6f6d09"}, - {file = "av-14.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9117ed91fba6299b7d5233dd3e471770bab829f97e5a157f182761e9fb59254c"}, - {file = "av-14.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54e8f9209184098b7755e6250be8ffa48a8aa5b554a02555406120583da17373"}, - {file = "av-14.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:38ea51e62a014663caec7f621d6601cf269ef450f3c8705f5e3225e5623fd15d"}, - {file = "av-14.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5d1d89842efe913448482573a253bd6955ce30a77f8a4cd04a1a3537cc919896"}, - {file = "av-14.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c3048e333da1367a2bca47e69593e10bc70f027d876adee9d1582c8cb818f36a"}, - {file = "av-14.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d6f25570d0782dd05640c7e1f71cb29857d94d915b5521a1e757ecae78a5a50"}, {file = "av-14.4.0.tar.gz", hash = "sha256:3ecbf803a7fdf67229c0edada0830d6bfaea4d10bfb24f0c3f4e607cd1064b42"}, ] @@ -2443,6 +2398,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" @@ -2976,4 +2947,4 @@ test = ["pytest", "pytest-asyncio"] [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "3c9f92c7a5af40f98da9c7824d9c2a6f7eb809e91e43cfef4995761b2e887256" +content-hash = "e39f622c983015c1a1c86236114c339044130db172cd420eecdd17f546af20de" diff --git a/pyproject.toml b/pyproject.toml index e93c66d..32a5dac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = [ "sounddevice (>=0.5.2,<0.6.0)", "python-dotenv (>=1.1.1,<2.0.0)", "smbus2 (>=0.5.0,<0.6.0)", - "samplerate (>=0.2.2,<0.3.0)" + "samplerate (>=0.2.2,<0.3.0)", + "rpi-gpio (>=0.7.1,<0.8.0)" ] [project.optional-dependencies] diff --git a/src/auracast/server/multicast_frontend.py b/src/auracast/server/multicast_frontend.py index 2b940d3..e5c4ed0 100644 --- a/src/auracast/server/multicast_frontend.py +++ b/src/auracast/server/multicast_frontend.py @@ -1751,7 +1751,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: diff --git a/src/auracast/server/multicast_server.py b/src/auracast/server/multicast_server.py index 4bdd1fb..3635030 100644 --- a/src/auracast/server/multicast_server.py +++ b/src/auracast/server/multicast_server.py @@ -26,6 +26,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 + # make sure pipewire sets latency # Primary and secondary persisted settings files STREAM_SETTINGS_FILE1 = os.path.join(os.path.dirname(__file__), 'stream_settings.json') @@ -288,6 +335,7 @@ async def _stop_all() -> bool: was_running = True finally: multicaster2 = None + _led_off() return was_running async def _status_primary() -> dict: @@ -474,6 +522,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:')) @@ -534,6 +583,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).""" @@ -597,6 +656,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 @@ -944,6 +1004,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') @@ -957,6 +1018,7 @@ 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%)