43 Commits

Author SHA1 Message Date
pober 77e8cd5729 Merge branch 'main' into bugfix/1087-UI-reset-refresh 2026-05-20 09:55:53 +00:00
pober edd23fc115 feature/1040-dante-activation (#31)
Makes the activation of dante possible.
Fixes issues with local link for DANTE.
First implementation of audiopipeline in its own thread.
Prevents Frontend from interrupting the audio stream.

Openproject:
#1040
#1069
#652
#1041
#1063

Reviewed-on: #31
2026-05-20 09:54:35 +00:00
pober 19a01e404c Removes manufacturer data. (#30)
Reviewed-on: #30
2026-05-20 09:40:22 +00:00
pstruebi efb55050c0 feature/1khz_testtone (#27)
- 1kHz test tone added
- all audio file converted to lc3 to save space
- streaming loop for lc3 files fixed

@pober

Reviewed-on: #27
Co-authored-by: pstruebi <struebin.patrick@gmail.com>
Co-committed-by: pstruebi <struebin.patrick@gmail.com>
2026-05-19 13:21:48 +00:00
pstruebi c82a17016e implement changes for dynamic power setting (#28)
Implements power control for the radios, both radios independent.

Reviewed-on: #28
Co-authored-by: pstruebi <struebin.patrick@gmail.com>
Co-committed-by: pstruebi <struebin.patrick@gmail.com>
2026-05-19 12:33:50 +00:00
pober 6375b215cb Autofocus password box on login screen. 2026-05-07 15:39:01 +02:00
pober 25df79eef5 Persists settings on refresh and power cycle. 2026-05-07 15:38:41 +02:00
pober 9c251b7a66 Fixes activation bug by adding the activation folder. 2026-04-28 09:05:39 +02:00
pober c24be9f366 Changed cpu pinning; more headroom for audiopipeline; dep and asrc on core 3 2026-04-28 09:01:58 +02:00
pober c659d632b0 Adds DEP service; gitignore for license; audiopipeline as its own thread with higher prio than http requests. 2026-04-27 15:35:20 +02:00
pober 14827288e7 relaxes asrc to not flip flop bang bang; use pyalsaaudio for dante. 2026-04-27 10:27:54 +02:00
pober 2410b01f15 Fixes loopback startup and makes it persistent with naming. 2026-04-27 10:26:04 +02:00
pober 67c774204a Dante buffers and cpu affinity. 2026-04-24 10:57:43 +02:00
pober c56012134c Fixes that an interface has both a local link and dhcp address - confuses dante. 2026-04-20 15:46:39 +02:00
pober 6d54e72f1d Add new reset mechanism with sleep. 2026-04-10 12:22:18 +02:00
pober df6c85d9ff Add new reset mechanism 2. 2026-04-10 11:58:16 +02:00
pober 8106f61d6a Add new reset mechanism. 2026-04-10 10:57:12 +02:00
pober 0a8dc74d5c Fixes script error in systemupdate. 2026-04-09 15:00:35 +02:00
pober 8475e4d068 New system update logic. 2026-04-09 14:46:17 +02:00
pober 3f01ef5968 Adds openocd with nrf support build to the server update function. Adds 2bad8ad2cd889d8c8d255b8e0dc0e7a187b98c9a hci_uart_beacon commit build hex file to project. (#26)
Co-authored-by: Pbopbo <p.obernesser@freenet.de>
Reviewed-on: #26
2026-04-09 12:04:18 +00:00
pober 67992e65ec Updates poetry lock. 2026-04-09 11:59:30 +02:00
pober 0b12323921 fix/gain-4dbU (#25)
Co-authored-by: Pbopbo <p.obernesser@freenet.de>
Reviewed-on: #25
2026-04-09 09:54:14 +00:00
Pbopbo 6e633d2880 Merge branch 'wip_alsaaudio' TODO poetry lock 2026-04-09 11:51:37 +02:00
pstruebi 7bdf6f8417 feature/blue_led (#23)
Co-authored-by: pstruebi <office@summitwave.eu>
Co-authored-by: pober <paul.obernesser@summitwave.eu>
Co-authored-by: Pbopbo <p.obernesser@freenet.de>
Reviewed-on: #23
Co-authored-by: pstruebi <struebin.patrick@gmail.com>
Co-committed-by: pstruebi <struebin.patrick@gmail.com>
2026-04-07 14:34:11 +00:00
Pbopbo 291d75b137 stereo seems to work, NEEDS RADIO FIRMWARE WITH 2 TX BUFFERS. 2026-04-07 14:36:15 +02:00
Pbopbo a126613739 First working version of two monos at the same time. 2026-04-02 18:56:17 +02:00
pober 036b5f80dd Updates poetry lock. 2026-04-02 18:10:23 +02:00
Pbopbo e818765b4f Adds sw_pyalsaaudio repo so our custom function works. 2026-04-02 17:37:38 +02:00
Pbopbo 3d59a6dabf ASRC: Adds NONBLOCK read from ALSA buffer; controls the amount of frames in the ALSA buffer; Adds resampling to get rid of audio glitches; no latency buildup anymore. 2026-04-01 14:00:26 +02:00
Pbopbo cf69ad2957 134ms constant delay, no build up, seems to be no glitches, bang bang control. 2026-03-30 14:45:25 +02:00
Pbopbo cdfecaf5eb delay method wip save to test no thread method. 2026-03-24 13:14:56 +01:00
pstruebi 4036fee1f5 Randomize Broadcast ID per stream instead of using static values 2026-03-24 12:09:16 +01:00
Pbopbo 1687a2b790 Latency lowered. 2026-03-18 17:37:34 +01:00
Pbopbo a605195646 First good audio with alsaaudio. 2026-03-18 16:55:55 +01:00
pober e1d717ed5c Adds DHCP/static IP toggle for both ports in the UI. 2026-03-03 15:50:19 +01:00
pober 540d8503ac Some corrections for Activates link local for both ports, removes fallback IP. 2026-03-03 15:35:13 +01:00
pober c82f375539 Activates link local for both ports, removes fallback IP. 2026-03-03 15:02:55 +01:00
pstruebi 70bde5295f Fixes mDNS issue; when DHCP IP is present use this for mDNS and not the static fallback IP. 2026-02-16 16:25:59 +01:00
pstruebi f5f93b4b8e analog_input_gain (#21)
- add input boost slider
- add level meter

Reviewed-on: https://gitea.pstruebi.xyz/auracaster/bumble-auracast/pulls/21
2026-02-12 17:09:46 +01:00
pstruebi 3322b9edf4 add 192.168.42.10 as default ip with update script 2026-02-12 17:08:23 +01:00
pstruebi d6230e7522 add software gain boost parameter for input signal amplification 2026-02-12 13:30:07 +01:00
pstruebi f2382470d8 add network information display showing hostname and IP address 2026-02-10 16:51:22 +01:00
pstruebi 7c2f0bf0cb add HTTP to HTTPS redirect server on port 80 2026-02-10 16:37:34 +01:00
90 changed files with 15139 additions and 386 deletions
+8 -1
View File
@@ -51,4 +51,11 @@ 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
# Dante license files
*.lic
src/dep/dante_package/dante_data/activation/device.lic
src/dep/dante_package/dante_data/activation/manufacturer.cert
+4 -1
View File
@@ -4,8 +4,11 @@
- this projects uses poetry for package management
- if something should be run in a python env use 'poetry run'
# Environment
- this application normally runs on an embedded linux on a cm4
## Application
- this is a bluetooth Auracast transmitter application
- if you add a new parameter for a stream make sure it is saved to the settings.json so it is persisted
- it consists of multicast_frontend.py and multicast_server.py mainly which connect to each other via a rest api
- after you implemented something the user will mainly test it and you should call the update_and_run_server_and_frontend.sh script if the server and frontend were already running.
Generated
+33 -46
View File
@@ -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"},
]
@@ -1849,6 +1804,22 @@ files = [
{file = "protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048"},
]
[[package]]
name = "pyalsaaudio"
version = "0.11.0"
description = "ALSA bindings"
optional = false
python-versions = "*"
groups = ["main"]
files = []
develop = false
[package.source]
type = "git"
url = "ssh://git@gitea.summitwave.work:222/auracaster/sw_pyalsaaudio.git"
reference = "b3d11582e03df6929b2e7acbaa1306afc7b8a6bc"
resolved_reference = "b3d11582e03df6929b2e7acbaa1306afc7b8a6bc"
[[package]]
name = "pyarrow"
version = "20.0.0"
@@ -2443,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"
@@ -2976,4 +2963,4 @@ test = ["pytest", "pytest-asyncio"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.11"
content-hash = "3c9f92c7a5af40f98da9c7824d9c2a6f7eb809e91e43cfef4995761b2e887256"
content-hash = "7bccf2978170ead195e1e8cff151823a5276823195a239622186fcec830154d9"
+3 -1
View File
@@ -17,7 +17,9 @@ 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)",
"pyalsaaudio @ git+ssh://git@gitea.summitwave.work:222/auracaster/sw_pyalsaaudio.git@b3d11582e03df6929b2e7acbaa1306afc7b8a6bc"
]
[project.optional-dependencies]
+28 -10
View File
@@ -1,5 +1,11 @@
from typing import List
from pydantic import BaseModel
from pydantic import BaseModel, field_validator
# Discrete TX power levels (dBm) supported by the Nordic SoftDevice Controller
# for the nRF radio PA. The HCI controller will clamp requested values to the
# nearest supported step. The maximum is bounded by CONFIG_BT_CTLR_TX_PWR_*
# in the hci_uart firmware (currently +8 dBm).
TX_POWER_VALID = [8, 7, 6, 5, 4, 3, 2, 0, -4, -8, -12, -16, -20]
# Define some base to hold the relevant parameters
class AuracastQoSConfig(BaseModel):
@@ -28,13 +34,24 @@ class AuracastGlobalConfig(BaseModel):
octets_per_frame: int = 40 #48kbps@24kHz # bitrate = octets_per_frame * 8 / frame len
frame_duration_us: int = 10000
presentation_delay_us: int = 40000
# TODO:pydantic does not support bytes serialization - use .hex and np.fromhex()
manufacturer_data: tuple[int, bytes] | tuple[None, None] = (None, None)
# LE Audio: Broadcast Audio Immediate Rendering (metadata type 0x09)
# When true, include a zero-length LTV with type 0x09 in the subgroup metadata
# so receivers may render earlier than the presentation delay for lower latency.
immediate_rendering: bool = False
assisted_listening_stream: bool = False
# Bluetooth advertising TX power for this radio in dBm (per advertising set).
# Sent through HCI_LE_Set_Extended_Advertising_Parameters; the SDC clamps to
# nearest supported hardware step and propagates to primary/secondary adv,
# the periodic advertising train and the BIS ISO PDUs.
advertising_tx_power: int = 8
@field_validator('advertising_tx_power')
@classmethod
def _snap_tx_power(cls, v: int) -> int:
# Snap to the nearest supported discrete step in TX_POWER_VALID.
if v in TX_POWER_VALID:
return v
return min(TX_POWER_VALID, key=lambda s: abs(s - v))
# "Audio input. "
# "'device' -> use the host's default sound input device, "
@@ -62,7 +79,7 @@ class AuracastBigConfigDeu(AuracastBigConfig):
name: str = 'Hörsaal A'
language: str ='deu'
program_info: str = 'Vorlesung DE'
audio_source: str = 'file:./testdata/wave_particle_5min_de.wav'
audio_source: str = 'file:./testdata/wave_particle_5min_de.lc3'
class AuracastBigConfigEng(AuracastBigConfig):
id: int = 123
@@ -70,7 +87,7 @@ class AuracastBigConfigEng(AuracastBigConfig):
name: str = 'Lecture Hall A'
language: str ='eng'
program_info: str = 'Lecture EN'
audio_source: str = 'file:./testdata/wave_particle_5min_en.wav'
audio_source: str = 'file:./testdata/wave_particle_5min_en.lc3'
class AuracastBigConfigFra(AuracastBigConfig):
id: int = 1234
@@ -79,7 +96,7 @@ class AuracastBigConfigFra(AuracastBigConfig):
name: str = 'Auditoire A'
language: str ='fra'
program_info: str = 'Auditoire FR'
audio_source: str = 'file:./testdata/wave_particle_5min_fr.wav'
audio_source: str = 'file:./testdata/wave_particle_5min_fr.lc3'
class AuracastBigConfigSpa(AuracastBigConfig):
id: int =12345
@@ -87,7 +104,7 @@ class AuracastBigConfigSpa(AuracastBigConfig):
name: str = 'Auditorio A'
language: str ='spa'
program_info: str = 'Auditorio ES'
audio_source: str = 'file:./testdata/wave_particle_5min_es.wav'
audio_source: str = 'file:./testdata/wave_particle_5min_es.lc3'
class AuracastBigConfigIta(AuracastBigConfig):
id: int =1234567
@@ -95,7 +112,7 @@ class AuracastBigConfigIta(AuracastBigConfig):
name: str = 'Aula A'
language: str ='ita'
program_info: str = 'Aula IT'
audio_source: str = 'file:./testdata/wave_particle_5min_it.wav'
audio_source: str = 'file:./testdata/wave_particle_5min_it.lc3'
class AuracastBigConfigPol(AuracastBigConfig):
@@ -104,11 +121,12 @@ class AuracastBigConfigPol(AuracastBigConfig):
name: str = 'Sala Wykładowa'
language: str ='pol'
program_info: str = 'Sala Wykładowa PL'
audio_source: str = 'file:./testdata/wave_particle_5min_pl.wav'
audio_source: str = 'file:./testdata/wave_particle_5min_pl.lc3'
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)
+222 -99
View File
@@ -30,6 +30,7 @@ import time
import threading
import numpy as np # for audio down-mix
import samplerate
import os
import lc3 # type: ignore # pylint: disable=E0401
@@ -48,6 +49,7 @@ import bumble.transport
import bumble.utils
from bumble.device import Host, AdvertisingChannelMap
from bumble.audio import io as audio_io
from bumble.vendor.zephyr.hci import HCI_Write_Tx_Power_Level_Command
from auracast import auracast_config
from auracast.utils.read_lc3_file import read_lc3_file
@@ -56,7 +58,7 @@ from auracast.utils.webrtc_audio_input import WebRTCAudioInput
# Patch sounddevice.InputStream globally to use low-latency settings
import sounddevice as sd
import alsaaudio
from collections import deque
@@ -139,96 +141,146 @@ class AlsaArecordAudioInput(audio_io.AudioInput):
self._proc = None
class ModSoundDeviceAudioInput(audio_io.SoundDeviceAudioInput):
"""Patched SoundDeviceAudioInput with low-latency capture and adaptive resampling."""
class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
"""PyALSA audio input with non-blocking reads - supports mono/stereo."""
def _open(self):
"""Create RawInputStream with low-latency parameters and initialize ring buffer."""
dev_info = sd.query_devices(self._device)
hostapis = sd.query_hostapis()
api_index = dev_info.get('hostapi')
api_name = hostapis[api_index]['name'] if isinstance(api_index, int) and 0 <= api_index < len(hostapis) else 'unknown'
pa_ver = sd.get_portaudio_version()
def __init__(self, device, pcm_format: audio_io.PcmFormat):
super().__init__()
logging.info("PyALSA: device = %s", device)
self._device = str(device) if not isinstance(device, str) else device
if self._device.isdigit():
self._device = 'default' if self._device == '0' else f'hw:{self._device}'
self._pcm_format = pcm_format
self._pcm = None
self._actual_channels = None
self._periodsize = None
self._hw_channels = None
self._first_read = True
self._resampler = None
self._resampler_buffer = np.empty(0, dtype=np.float32)
logging.info(
"SoundDevice backend=%s device='%s' (id=%s) ch=%s default_low_input_latency=%.4f default_high_input_latency=%.4f portaudio=%s",
api_name,
dev_info.get('name'),
self._device,
dev_info.get('max_input_channels'),
float(dev_info.get('default_low_input_latency') or 0.0),
float(dev_info.get('default_high_input_latency') or 0.0),
pa_ver[1] if isinstance(pa_ver, tuple) and len(pa_ver) >= 2 else pa_ver,
)
# Create RawInputStream with injected low-latency parameters
# Target ~2 ms blocksize (48 kHz -> 96 frames). For other rates, keep ~2 ms.
_sr = int(self._pcm_format.sample_rate)
self.counter=0
self.max_avail=0
self.logfile_name="available_samples.txt"
self.blocksize = 120
if os.path.exists(self.logfile_name):
os.remove(self.logfile_name)
self._stream = sd.RawInputStream(
samplerate=self._pcm_format.sample_rate,
def _open(self) -> audio_io.PcmFormat:
ALSA_PERIODSIZE = 240
ALSA_PERIODS = 4
ALSA_MODE = alsaaudio.PCM_NONBLOCK
requested_rate = int(self._pcm_format.sample_rate)
requested_channels = int(self._pcm_format.channels)
self._periodsize = ALSA_PERIODSIZE
self._pcm = alsaaudio.PCM(
type=alsaaudio.PCM_CAPTURE,
mode=ALSA_MODE,
device=self._device,
channels=self._pcm_format.channels,
dtype='int16',
blocksize=self.blocksize,
latency=0.004,
periods=ALSA_PERIODS,
)
self._stream.start()
self._pcm.setchannels(requested_channels)
self._pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
actual_rate = self._pcm.setrate(requested_rate)
self._pcm.setperiodsize(ALSA_PERIODSIZE)
logging.info("PyALSA: device=%s rate=%d ch=%d periodsize=%d (%.1fms) periods=%d mode=%s",
self._device, actual_rate, requested_channels, ALSA_PERIODSIZE,
(ALSA_PERIODSIZE / actual_rate) * 1000, ALSA_PERIODS, ALSA_MODE)
if actual_rate != requested_rate:
logging.warning("PyALSA: Sample rate mismatch! requested=%d actual=%d", requested_rate, actual_rate)
self._actual_channels = requested_channels
self._resampler = samplerate.Resampler('sinc_fastest', channels=requested_channels)
self._resampler_buffer = np.empty(0, dtype=np.float32)
self._bang_bang = 0
return audio_io.PcmFormat(
audio_io.PcmFormat.Endianness.LITTLE,
audio_io.PcmFormat.SampleType.INT16,
self._pcm_format.sample_rate,
1,
actual_rate,
requested_channels,
)
def _read(self, frame_size: int) -> bytes:
"""Read PCM samples from the stream."""
try:
avail = self._pcm.avail()
logging.debug("PyALSA: avail before read: %d", avail)
length, data = self._pcm.read_sw(frame_size + self._bang_bang)
avail = self._pcm.avail()
SETPOINT = 120
TOLERANCE = 80
if avail < SETPOINT - TOLERANCE:
self._bang_bang = -1
elif avail > SETPOINT + TOLERANCE:
self._bang_bang = 1
else:
self._bang_bang = 0
logging.debug("PyALSA: read length=%d, data length=%d, avail=%d, bang_bang=%d", length, len(data), avail, self._bang_bang)
#if self.counter % 50 == 0:
frame_size = frame_size + 1 # consume samples a little faster to avoid latency akkumulation
if length > 0:
if self._first_read:
expected_mono = self._periodsize * 2
expected_stereo = self._periodsize * 2 * 2
# self._hw_channels = 2 if len(data) == expected_stereo else 1
self._hw_channels = self._actual_channels
logging.info("PyALSA first read: bytes=%d detected_hw_channels=%d requested_channels=%d",
len(data), self._hw_channels, self._actual_channels)
self._first_read = False
if self._hw_channels == 2 and self._actual_channels == 1:
pcm_stereo = np.frombuffer(data, dtype=np.int16)
pcm_mono = pcm_stereo[::2]
data = pcm_mono.tobytes()
actual_samples = len(data) // (2 * self._actual_channels)
ratio = frame_size / actual_samples
pcm_f32 = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
if self._actual_channels > 1:
pcm_f32 = pcm_f32.reshape(-1, self._actual_channels)
resampled = self._resampler.process(pcm_f32, ratio, end_of_input=False)
if self._actual_channels > 1:
resampled = resampled.reshape(-1)
self._resampler_buffer = np.concatenate([self._resampler_buffer, resampled])
else:
logging.warning("PyALSA: No data read from ALSA")
self._resampler_buffer = np.concatenate([
self._resampler_buffer,
np.zeros(frame_size * self._actual_channels, dtype=np.float32),
])
except alsaaudio.ALSAAudioError as e:
logging.error("PyALSA: ALSA read error: %s", e)
self._resampler_buffer = np.concatenate([
self._resampler_buffer,
np.zeros(frame_size * self._actual_channels, dtype=np.float32),
])
except Exception as e:
logging.error("PyALSA: Unexpected error in _read: %s", e, exc_info=True)
self._resampler_buffer = np.concatenate([
self._resampler_buffer,
np.zeros(frame_size * self._actual_channels, dtype=np.float32),
])
pcm_buffer, overflowed = self._stream.read(frame_size)
if overflowed:
logging.warning("SoundDeviceAudioInput: overflowed")
needed = frame_size * self._actual_channels
if len(self._resampler_buffer) < needed:
pad = np.zeros(needed - len(self._resampler_buffer), dtype=np.float32)
self._resampler_buffer = np.concatenate([self._resampler_buffer, pad])
logging.debug("PyALSA: padded buffer with %d samples", needed - len(self._resampler_buffer))
n_available = self._stream.read_available
output = self._resampler_buffer[:needed]
self._resampler_buffer = self._resampler_buffer[needed:]
# adapt = n_available > 20
# if adapt:
# pcm_extra, overflowed = self._stream.read(3)
# logging.info('consuming extra samples, available was %d', n_available)
# if overflowed:
# logging.warning("SoundDeviceAudioInput: overflowed")
# out = bytes(pcm_buffer) + bytes(pcm_extra)
# else:
out = bytes(pcm_buffer)
logging.debug("PyALSA: resampler_buffer remaining=%d", len(self._resampler_buffer))
return np.clip(output * 32767.0, -32768, 32767).astype(np.int16).tobytes()
self.max_avail = max(self.max_avail, n_available)
#Diagnostics
#with open(self.logfile_name, "a", encoding="utf-8") as f:
# f.write(f"{n_available}, {adapt}, {round(self._runavg, 2)}, {overflowed}\n")
def _close(self) -> None:
if self._pcm:
self._pcm.close()
self._pcm = None
if self.counter % 500 == 0:
logging.info(
"read available=%d, max=%d, latency:%d",
n_available, self.max_avail, self._stream.latency
)
self.max_avail = 0
self.counter += 1
return out
audio_io.SoundDeviceAudioInput = ModSoundDeviceAudioInput
audio_io.SoundDeviceAudioInput = PyAlsaAudioInput
# modified from bumble
class ModWaveAudioInput(audio_io.ThreadedAudioInput):
@@ -411,21 +463,6 @@ async def init_broadcast(
],
)
logger.info('Setup Advertising')
advertising_manufacturer_data = (
b''
if global_config.manufacturer_data == (None, None)
else bytes(
core.AdvertisingData(
[
(
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
struct.pack('<H', global_config.manufacturer_data[0])
+ global_config.manufacturer_data[1],
)
]
)
)
)
bigs[f'big{i}']['broadcast_audio_announcement'] = bap.BroadcastAudioAnnouncement(conf.id)
# Build advertising data types list
@@ -468,13 +505,22 @@ async def init_broadcast(
advertising_sid=i,
primary_advertising_phy=hci.Phy.LE_1M, # 2m phy config throws error - because for primary advertising channels, 1mbit is only supported
secondary_advertising_phy=hci.Phy.LE_1M, # this is the secondary advertising beeing send on non advertising channels (extendend advertising)
#advertising_tx_power= # tx power in dbm (max 20)
# Pass NO_PREFERENCE (0x7F) here for two reasons:
# 1. The Nordic SoftDevice Controller ignores this field for
# advertising sets and always returns the compile-time
# CONFIG_BT_CTLR_TX_PWR_* value. The real TX power is
# applied via the Zephyr VS Write_Tx_Power_Level command
# issued right after create_advertising_set() returns.
# 2. Bumble's HCI metadata declares this field as 1-byte
# *unsigned* (a bumble bug — the BT spec defines it as
# signed int8), so negative values would raise
# "bytes must be in range(0, 256)" at serialization.
advertising_tx_power=hci.HCI_LE_Set_Extended_Advertising_Parameters_Command.TX_POWER_NO_PREFERENCE,
#secondary_advertising_max_skip=10,
),
advertising_data=(
bigs[f'big{i}']['broadcast_audio_announcement'].get_advertising_data()
+ bytes(core.AdvertisingData(advertising_data_types))
+ advertising_manufacturer_data
),
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
periodic_advertising_interval_min=80,
@@ -485,6 +531,48 @@ async def init_broadcast(
auto_start=True,
)
bigs[f'big{i}']['advertising_set'] = advertising_set
# NOTE: selected_tx_power below reflects the SDC's compile-time max
# (LE_Set_Ext_Adv_Params was sent with NO_PREFERENCE). The actual
# transmit power is set by the VS Write_Tx_Power_Level call below.
logging.debug(
'LE_Set_Ext_Adv_Params reports controller fallback TX power: %+d dBm (handle=%d)',
getattr(advertising_set, 'selected_tx_power', 0),
i,
)
# The Nordic SoftDevice Controller does not honor the per-set
# advertising_tx_power passed in HCI_LE_Set_Extended_Advertising_Parameters
# (it returns the compile-time CONFIG_BT_CTLR_TX_PWR_* value regardless).
# Apply the requested level via the Zephyr Vendor-Specific HCI command
# Write_Tx_Power_Level (opcode 0xFC0E), which the SDC honors per
# advertising handle. The SDC clamps the value to the nearest supported
# hardware step (max bounded by CONFIG_BT_CTLR_TX_PWR_PLUS_8).
try:
adv_handle = getattr(advertising_set, 'advertising_handle', i)
response = await device.send_command(
HCI_Write_Tx_Power_Level_Command(
handle_type=HCI_Write_Tx_Power_Level_Command.TX_POWER_HANDLE_TYPE_ADV,
connection_handle=adv_handle,
tx_power_level=global_config.advertising_tx_power,
)
)
rp = getattr(response, 'return_parameters', None)
status = getattr(rp, 'status', 0xFF) if rp is not None else 0xFF
selected = getattr(rp, 'selected_tx_power_level', None) if rp is not None else None
if status == 0 and selected is not None:
logging.info(
'Advertising TX power (VS Write_Tx_Power_Level): requested=%+d dBm, controller selected=%+d dBm (handle=%d)',
global_config.advertising_tx_power,
selected,
adv_handle,
)
else:
logging.warning(
'VS Write_Tx_Power_Level failed: status=0x%02X handle=%d requested=%+d dBm',
status, adv_handle, global_config.advertising_tx_power,
)
except Exception as e:
logging.warning('VS Write_Tx_Power_Level not supported by controller: %s', e)
logging.info('Start Periodic Advertising')
await advertising_set.start_periodic()
@@ -538,7 +626,7 @@ async def init_broadcast(
def on_flow():
data_packet_queue = iso_queue.data_packet_queue
print(
logging.info(
f'\rPACKETS: pending={data_packet_queue.pending}, '
f'queued={data_packet_queue.queued}, '
f'completed={data_packet_queue.completed}',
@@ -551,6 +639,29 @@ async def init_broadcast(
return bigs
def _lc3_file_byte_gen(filename: str, loop: bool = False):
"""Stream LC3 frames from disk as individual bytes, with optional looping.
Yields one byte (int) at a time so it is compatible with the existing
``bytes(itertools.islice(gen, bytes_per_frame))`` consumer without loading
the whole file into memory.
"""
while True:
with open(filename, 'rb') as f:
f.read(18) # skip 18-byte LC3 header
while True:
size_b = f.read(2)
if len(size_b) < 2:
break
frame_size = struct.unpack('=H', size_b)[0]
frame = f.read(frame_size)
if len(frame) < frame_size:
break
yield from frame
if not loop:
return
class Streamer():
"""
Streamer class that supports multiple input formats. See bumble for streaming from wav or device
@@ -638,6 +749,12 @@ class Streamer():
except Exception:
pass
def get_audio_levels(self) -> list[float]:
"""Return current RMS audio levels (0.0-1.0) for each BIG."""
if not self.bigs:
return []
return [big.get('_audio_level_rms', 0.0) for big in self.bigs.values()]
async def stream(self):
bigs = self.bigs
@@ -700,13 +817,7 @@ class Streamer():
big['precoded'] = True
big['lc3_bytes_per_frame'] = global_config.octets_per_frame
filename = big_config[i].audio_source.replace('file:', '')
lc3_bytes = read_lc3_file(filename)
lc3_frames = iter(lc3_bytes)
if big_config[i].loop:
lc3_frames = itertools.cycle(lc3_frames)
big['lc3_frames'] = lc3_frames
big['lc3_frames'] = _lc3_file_byte_gen(filename, loop=big_config[i].loop)
# use wav files and code them entirely before streaming
elif big_config[i].precode_wav and big_config[i].audio_source.endswith('.wav'):
@@ -754,7 +865,11 @@ class Streamer():
if input_format == 'auto':
raise ValueError('input format details required for alsa input')
pcm = audio_io.PcmFormat.from_str(input_format)
audio_input = AlsaArecordAudioInput(audio_source[5:], pcm)
device_name = audio_source[5:]
if device_name.startswith('dante_'):
audio_input = PyAlsaAudioInput(device_name, pcm)
else:
audio_input = AlsaArecordAudioInput(device_name, pcm)
else:
audio_input = await audio_io.create_audio_input(audio_source, input_format)
# Store early so stop_streaming can close even if open() fails
@@ -823,6 +938,9 @@ class Streamer():
if lc3_frame == b'': # Not all streams may stop at the same time
stream_finished[i] = True
continue
for q_idx in range(big.get('num_bis', 1)):
await big['iso_queues'][q_idx].write(lc3_frame)
else: # code lc3 on the fly with perf counters
# Ensure frames generator exists (so we can aclose() on stop)
frames_gen = big.get('frames_gen')
@@ -852,6 +970,11 @@ class Streamer():
stream_finished[i] = True
continue
# 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
big['_audio_level_rms'] = float(rms)
# Measure LC3 encoding time
t1 = time.perf_counter()
num_bis = big.get('num_bis', 1)
+10
View File
@@ -37,6 +37,12 @@ class Multicaster:
'is_initialized': self.is_auracast_init,
'is_streaming': streaming,
}
def get_audio_levels(self) -> list[float]:
"""Return current RMS audio levels (0.0-1.0) for each BIG."""
if self.streamer is not None and self.streamer.is_streaming:
return self.streamer.get_audio_levels()
return []
async def init_broadcast(self):
self.device_acm = multicast.create_device(self.global_conf)
@@ -137,6 +143,10 @@ async def main():
level=os.environ.get('LOG_LEVEL', logging.DEBUG),
format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s'
)
# Enable debug logging for bumble
# logging.getLogger('bumble').setLevel(logging.DEBUG)
os.chdir(os.path.dirname(__file__))
global_conf = auracast_config.AuracastGlobalConfig(
@@ -0,0 +1,42 @@
"""Minimal HTTP server that redirects all requests to HTTPS (port 443).
Run on port 80 alongside the HTTPS Streamlit frontend so that users who
type a bare IP address into their browser are automatically forwarded.
"""
import http.server
import sys
class RedirectHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
host = self.headers.get("Host", "").split(":")[0] or self.server.server_address[0]
target = f"https://{host}{self.path}"
self.send_response(301)
self.send_header("Location", target)
self.end_headers()
# Handle every method the same way
do_POST = do_GET
do_PUT = do_GET
do_DELETE = do_GET
do_HEAD = do_GET
def log_message(self, format, *args):
# Keep logging minimal
sys.stderr.write(f"[http-redirect] {self.address_string()} -> https {args[0] if args else ''}\n")
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 80
server = http.server.HTTPServer(("0.0.0.0", port), RedirectHandler)
print(f"HTTP->HTTPS redirect server listening on 0.0.0.0:{port}")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
server.server_close()
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -33,5 +33,12 @@ echo "Using Avahi domain: $AVAHI_DOMAIN"
# Path to poetry binary
POETRY_BIN="/home/caster/.local/bin/poetry"
# Start HTTP->HTTPS redirect server on port 80 (background)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
python3 "$SCRIPT_DIR/http_to_https_redirect.py" 80 &
REDIRECT_PID=$!
echo "HTTP->HTTPS redirect server started (PID $REDIRECT_PID)"
trap "kill $REDIRECT_PID 2>/dev/null" EXIT
# Start Streamlit HTTPS server (port 443)
$POETRY_BIN run streamlit run multicast_frontend.py --server.port 443 --server.address 0.0.0.0 --server.enableCORS false --server.enableXsrfProtection false --server.headless true --server.sslCertFile "$CERT" --server.sslKeyFile "$KEY" --browser.gatherUsageStats false
+90
View 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"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -21,12 +21,12 @@ def read_lc3_file(filepath):
logging.info('frame_duration %s', frame_duration)
logging.info('stream_length %s', stream_length)
lc3_bytes= b''
chunks = []
while True:
b = f_lc3.read(2)
if b == b'':
break
lc3_frame_size = struct.unpack('=H', b)[0]
lc3_bytes += f_lc3.read(lc3_frame_size)
chunks.append(f_lc3.read(lc3_frame_size))
return lc3_bytes
return b''.join(chunks)
@@ -0,0 +1 @@
Placeholder file to have the activation folder available, otherwise dante activation script fails with 'Unable to write'.
@@ -1,5 +1,5 @@
{
"trialMode": true,
"trialMode": false,
"$schema": "./dante.json_schema.json",
"platform":
{
@@ -16,7 +16,7 @@
48000
],
"samplesPerPeriod" : 16,
"periodsPerBuffer" : 300,
"periodsPerBuffer" : 150,
"networkLatencyMinMs" : 2,
"networkLatencyDefaultMs" : 5,
"supportedEncodings" :
@@ -24,7 +24,10 @@
"PCM16"
],
"defaultEncoding" : "PCM16",
"numDepCores" : 1
"numDepCores" :
[
3
]
},
"network" :
{
@@ -50,31 +53,32 @@
"alsaAsrc":
{
"enableAlsaAsrc": true,
"cpuAffinity": 3,
"deviceConfigurations": [
{
"deviceIdentifier": "hw:0,0",
"deviceIdentifier": "hw:6,0,0",
"direction": "playback",
"bitDepth": 16,
"numOpenChannels": 6,
"alsaChannelRange": "0-5",
"danteChannelRange": "0-5",
"bufferSize": 4800,
"bufferSize": 960,
"samplesPerPeriod": 16
}
]
},
"product" :
{
"manfId" : "Audinate",
"manfName" : "Audinate Pty Ltd",
"modelId" : "OEMDEP",
"modelName" : "Linux Dante Embedded Platform",
"manfId" : "SummitFC",
"manfName" : "Summitwave FlexCo",
"modelId" : "TX",
"modelName" : "Summitwave TX",
"modelVersion" :
{
"major" : 9,
"minor" : 9,
"bugfix" : 99
"major" : 1,
"minor" : 0,
"bugfix" : 0
},
"devicePrefix" : "DEP"
"devicePrefix" : "SW-TX"
}
}
+4 -4
View File
@@ -6,8 +6,8 @@ pcm.ch1 {
channels 2
rate 48000
format S16_LE
period_size 120
buffer_size 240
period_size 240
buffer_size 960
}
bindings.0 0
}
@@ -21,8 +21,8 @@ pcm.ch2 {
channels 2
rate 48000
format S16_LE
period_size 120
buffer_size 240
period_size 240
buffer_size 960
}
bindings.0 1
}
+2 -1
View File
@@ -1 +1,2 @@
sudo cp src/misc/asound.conf /etc/asound.conf
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
sudo cp "$SCRIPT_DIR/asound.conf" /etc/asound.conf
+56
View 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."
+13168
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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
View File
@@ -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
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
# NetworkManager dispatcher script: 10-link-local-mgmt
#
# Temporarily suppresses IPv4 link-local when a DHCP address is available,
# using nmcli device modify (active session only, NOT saved to the profile).
# The persistent profile always keeps ipv4.link-local=enabled so that
# direct-connect (no DHCP) plug-ins always activate and trigger events.
#
# Triggers: up, down, dhcp4-change on ethernet interfaces
# Install to: /etc/NetworkManager/dispatcher.d/10-link-local-mgmt
# Permissions: root:root 0755
INTERFACE="$1"
ACTION="$2"
CONNECTION_NAME="${CONNECTION_ID:-}"
# Only handle ethernet interfaces
if [[ ! "$INTERFACE" =~ ^eth ]]; then
exit 0
fi
# If CONNECTION_ID env var is not set, look up the active connection for this interface
if [ -z "$CONNECTION_NAME" ]; then
CONNECTION_NAME=$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null \
| grep ":${INTERFACE}$" | cut -d: -f1 | head -n1)
[ -z "$CONNECTION_NAME" ] && exit 0
fi
# Update /etc/avahi/hosts to point mDNS hostname at the best available DHCP address
# across all ethernet interfaces (so Avahi doesn't advertise a link-local address).
update_avahi() {
local hostname
hostname=$(hostname)
# Find first non-link-local IPv4 across all ethernet interfaces
local dhcp_ip
dhcp_ip=$(ip -4 addr show 2>/dev/null \
| grep -A5 ': eth' \
| grep -oP '(?<=inet\s)\d+(\.\d+){3}' \
| grep -v '^127\.' \
| grep -v '^169\.254\.' \
| head -n1)
if [ -n "$dhcp_ip" ]; then
mkdir -p /etc/avahi
echo "$dhcp_ip $hostname $hostname.local" > /etc/avahi/hosts
logger -t nm-link-local "Avahi: pinned $hostname -> $dhcp_ip"
else
rm -f /etc/avahi/hosts
logger -t nm-link-local "Avahi: removed hosts pin, using all addresses"
fi
systemctl restart avahi-daemon 2>/dev/null
}
case "$ACTION" in
up|dhcp4-change)
DHCP_IP=$(ip -4 addr show "$INTERFACE" 2>/dev/null \
| grep -oP '(?<=inet\s)\d+(\.\d+){3}' \
| grep -v '^127\.' \
| grep -v '^169\.254\.' \
| head -n1)
if [ -n "$DHCP_IP" ]; then
logger -t nm-link-local "[$INTERFACE] DHCP $DHCP_IP detected — suppressing link-local (session only)"
# Use device modify (not connection modify) so the persistent profile keeps
# ipv4.link-local=enabled. This ensures direct-connect plug-ins always activate.
# Run in background after a delay — nmcli blocks on NM, which is waiting for
# this dispatcher to return, causing a deadlock if called synchronously.
(sleep 2 && nmcli device modify "$INTERFACE" ipv4.link-local disabled 2>/dev/null \
&& logger -t nm-link-local "[$INTERFACE] Link-local suppressed for current session") &
else
logger -t nm-link-local "[$INTERFACE] No DHCP on $INTERFACE — keeping link-local active"
fi
update_avahi
;;
down)
# Profile always has ipv4.link-local=enabled so no action needed here.
# The suppression from device modify was session-only and is gone when the
# connection goes down.
logger -t nm-link-local "[$INTERFACE] Down — link-local will be active on next connect"
update_avahi
;;
esac
+2
View File
@@ -10,6 +10,8 @@ WorkingDirectory=/home/caster/bumble-auracast/src/auracast/server
ExecStart=/home/caster/bumble-auracast/src/auracast/server/start_frontend_https.sh
Restart=on-failure
Environment=LOG_LEVEL=INFO
AllowedCPUs=0
CPUAffinity=0
[Install]
WantedBy=multi-user.target
+2
View File
@@ -9,6 +9,8 @@ ExecStart=/home/caster/bumble-auracast/.venv/bin/python src/auracast/multicast_s
Restart=on-failure
Environment=PYTHONUNBUFFERED=1
Environment=LOG_LEVEL=INFO
AllowedCPUs=0
CPUAffinity=0
[Install]
WantedBy=default.target
+5 -2
View File
@@ -1,6 +1,7 @@
[Unit]
Description=Auracast Backend Server
After=network.target
After=network.target dep.service
Wants=dep.service
[Service]
Type=simple
@@ -10,8 +11,10 @@ Restart=on-failure
Environment=PYTHONUNBUFFERED=1
Environment=LOG_LEVEL=INFO
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=99
CPUSchedulingPriority=10
LimitRTPRIO=99
AllowedCPUs=0,1,2
CPUAffinity=0,1,2
[Install]
WantedBy=default.target
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=DEP (Dante Embedded Platform) Container
After=network.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/caster/bumble-auracast/src/dep/dante_package
ExecStart=/bin/bash dep.sh start
ExecStop=/bin/bash dep.sh stop
[Install]
WantedBy=multi-user.target
+2
View File
@@ -9,6 +9,8 @@ ExecStartPre=/bin/sh -lc 'for i in $(seq 1 60); do ip route show default >/dev/n
ExecStart=/usr/bin/pipewire-aes67 -c /home/caster/bumble-auracast/src/service/aes67/pipewire-aes67.conf
Restart=always
RestartSec=5s
AllowedCPUs=0
CPUAffinity=0
# Avoid StartLimitHit on quick failures during boot; let RestartSec handle pacing
StartLimitIntervalSec=0
+2
View File
@@ -6,6 +6,8 @@ After=network.target
Type=simple
ExecStart=/usr/sbin/ptp4l -i eth0 -f /home/caster/bumble-auracast/src/service/aes67/ptp_aes67_1.conf
Restart=on-failure
AllowedCPUs=0
CPUAffinity=0
StandardOutput=journal
StandardError=journal
+34 -2
View File
@@ -4,6 +4,33 @@ set -e
# This script installs, enables, and restarts the auracast-server and auracast-frontend services
# Requires sudo privileges
# Ensure static link local is activated (for direct laptop connection)
# Enable link-local for all wired ethernet connections
while IFS=: read -r name type; do
if [[ "$type" == *"ethernet"* ]]; then
echo "Enabling IPv4 link-local for connection: $name"
sudo nmcli connection modify "$name" ipv4.link-local enabled 2>/dev/null || echo "Failed to modify $name"
sudo nmcli connection up "$name" 2>/dev/null || echo "Failed to bring up $name"
fi
done < <(nmcli -t -f NAME,TYPE connection show)
# Ensure Loopback is loaded with a fixed name and index
# Needed for dante
# TODO image when we create the next image this should be part of it
echo "options snd-aloop index=6 id=Loopback pcm_substreams=6" | sudo tee /etc/modprobe.d/snd-aloop.conf
echo snd-aloop | sudo tee /etc/modules-load.d/snd-aloop.conf
# Install NetworkManager dispatcher script for link-local / Avahi management
sudo cp /home/caster/bumble-auracast/src/service/10-link-local-mgmt /etc/NetworkManager/dispatcher.d/10-link-local-mgmt
sudo chown root:root /etc/NetworkManager/dispatcher.d/10-link-local-mgmt
sudo chmod 755 /etc/NetworkManager/dispatcher.d/10-link-local-mgmt
# Copy system service file for DEP
sudo cp /home/caster/bumble-auracast/src/service/dep.service /etc/systemd/system/dep.service
# Copy system service file for frontend
sudo cp /home/caster/bumble-auracast/src/service/auracast-frontend.service /etc/systemd/system/auracast-frontend.service
@@ -11,20 +38,25 @@ sudo cp /home/caster/bumble-auracast/src/service/auracast-frontend.service /etc/
mkdir -p /home/caster/.config/systemd/user
cp /home/caster/bumble-auracast/src/service/auracast-server.service /home/caster/.config/systemd/user/auracast-server.service
# Reload systemd for frontend
# Reload systemd for frontend and dep
sudo systemctl daemon-reload
# Reload user systemd for server
systemctl --user daemon-reload
# Enable DEP to start on boot (system)
sudo systemctl enable dep.service
# Enable frontend to start on boot (system)
sudo systemctl enable auracast-frontend.service
# Enable server to start on boot (user)
systemctl --user enable auracast-server.service
# Restart both
# Restart all
sudo systemctl restart dep.service
sudo systemctl restart auracast-frontend.service
systemctl --user restart auracast-server.service
#print status
sudo systemctl status dep.service --no-pager
sudo systemctl status auracast-frontend.service --no-pager
systemctl --user status auracast-server.service --no-pager