From f192d31b5b9be71ad5132b4e71e75a3663f5085f Mon Sep 17 00:00:00 2001 From: pstruebi Date: Thu, 6 Mar 2025 16:15:01 +0100 Subject: [PATCH 01/11] use fastapi for the client/server --- pyproject.toml | 3 +- src/auracast/auracast_config.py | 13 +++- src/auracast/multicast.py | 2 +- src/auracast/multicast_client.py | 47 +++++++++--- src/auracast/multicast_server.py | 128 ++++++++++++++----------------- 5 files changed, 106 insertions(+), 87 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8632d7c..f999a06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,8 @@ dependencies = [ "lc3 @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git@7558637303106c7ea971e7bb8cedf379d3e08bcc", "sounddevice", "aioconsole", - "quart == 0.20.0", + "fastapi==0.115.11", + "uvicorn==0.34.0", "pydantic" ] diff --git a/src/auracast/auracast_config.py b/src/auracast/auracast_config.py index ad5ac03..b3be927 100644 --- a/src/auracast/auracast_config.py +++ b/src/auracast/auracast_config.py @@ -1,3 +1,4 @@ +from typing import List from pydantic import BaseModel # Define some base to hold the relevant parameters @@ -32,8 +33,8 @@ 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 - manufacturer_data: tuple[int, bytes] |None = None # TODO:pydantic does not support bytes serialization - + # TODO:pydantic does not support bytes serialization - use .hex and np.fromhex() + manufacturer_data: tuple[int, bytes] | tuple[None, None] = (None, None) # "Audio input. " # "'device' -> use the host's default sound input device, " @@ -95,4 +96,10 @@ class AuracastBigConfigIt(AuracastBigConfig): audio_source: str = 'file:./testdata/announcement_it.wav' -# TODO: could be best to merge all in just one CONFIG class and give every language an enable parameter \ No newline at end of file +# TODO: could be best to merge all in just one CONFIG class and give every language an enable parameter + +# TODO: Used in the client/server context, should probably be used verywhere +class AuracastConfigGroup(AuracastGlobalConfig): + bigs: List[AuracastBigConfig] = [ + AuracastBigConfigDe(), + ] diff --git a/src/auracast/multicast.py b/src/auracast/multicast.py index 67c041c..84ec027 100644 --- a/src/auracast/multicast.py +++ b/src/auracast/multicast.py @@ -186,7 +186,7 @@ async def init_broadcast( logger.info('Setup Advertising') advertising_manufacturer_data = ( b'' - if global_config.manufacturer_data is None + if global_config.manufacturer_data == (None, None) else bytes( core.AdvertisingData( [ diff --git a/src/auracast/multicast_client.py b/src/auracast/multicast_client.py index b3f8aa3..801664f 100644 --- a/src/auracast/multicast_client.py +++ b/src/auracast/multicast_client.py @@ -1,12 +1,30 @@ import requests + +from typing import List +from auracast import auracast_config + from auracast.utils.read_lc3_file import read_lc3_file BASE_URL = "http://127.0.0.1:5000" # Adjust based on your actual API URL -def initialize(): - response = requests.post(f"{BASE_URL}/init") +# TODO: put this to a common location +class AuracastConfigGroup(auracast_config.AuracastGlobalConfig): + bigs: List[auracast_config.AuracastBigConfig] = [ + auracast_config.AuracastBigConfigDe(), + ] + + +def request_init(request_data : AuracastConfigGroup): + response = requests.post(f"{BASE_URL}/init", json=request_data.model_dump()) + if response.status_code != 200: + raise(f"Error: {response.status_code}, {response.text}") + + +def send_audio(data_dict): + response = requests.post(f"{BASE_URL}/stream_lc3", json=data_dict) return response.json() + def shutdown(): response = requests.post(f"{BASE_URL}/shutdown") return response.json() @@ -15,22 +33,31 @@ def stop_audio(): response = requests.post(f"{BASE_URL}/stop_audio") return response.json() -def send_audio(data_dict): - response = requests.post(f"{BASE_URL}/stream_lc3", json=data_dict) - return response.json() def get_status(): response = requests.get(f"{BASE_URL}/status") return response.json() if __name__ == "__main__": - test_audio_data = { # TODO: investigate further whats the best way to actually transfer the data - "broadcast_deu": read_lc3_file('src/auracast/testdata/announcement_de_10_16_32.lc3').decode('latin-1'), - "broadcast_eng": read_lc3_file('src/auracast/testdata/announcement_en_10_16_32.lc3').decode('latin-1') + config = AuracastConfigGroup( + + ) + config.transport = 'serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' + + # Initialize language-based configurations + for conf in config.bigs: + conf.loop = False + + + audio_data = { # TODO: investigate further whats the best way to actually transfer the data + "deu": read_lc3_file('src/auracast/testdata/announcement_de_10_16_32.lc3').decode('latin-1'), + #"eng": read_lc3_file('src/auracast/testdata/announcement_en_10_16_32.lc3').decode('latin-1'), + #"fra": read_lc3_file('src/auracast/testdata/announcement_fr_10_16_32.lc3').decode('latin-1'), + } print("Getting status:", get_status()) - print("Initializing server:", initialize()) + print("Initializing server:", request_init(config)) print("Getting status:", get_status()) - print("Sending audio:", send_audio()) + print("Sending audio:", send_audio(audio_data)) print("Getting status:", get_status()) diff --git a/src/auracast/multicast_server.py b/src/auracast/multicast_server.py index b0b1f41..0dc9133 100644 --- a/src/auracast/multicast_server.py +++ b/src/auracast/multicast_server.py @@ -1,103 +1,87 @@ +from typing import List import logging as log -from dataclasses import asdict -from quart import Quart, request, jsonify -from auracast import multicast_control -from auracast import auracast_config +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from auracast import multicast_control, auracast_config -app = Quart(__name__) +app = FastAPI() -# TODO: redo this with fastapi, transfer whole radio config on init +# Initialize global configuration +global_config_group = AuracastConfigGroup() +global_config_group.transport = 'serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' -# Initialize the multicaster instance globally -global_conf = auracast_config.AuracastGlobalConfig() +# Create multicast controller +multicaster: multicast_control.Multicaster | None = None -#global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001057705357-if02,1000000,rtscts' # transport for nrf54l15dk -global_conf.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' #nrf52dongle hci_uart usb cdc -big_conf = { # TODO: use another dataclass for this to be able to iterate over the names - 'deu': auracast_config.AuracastBigConfigDe(), - 'eng': auracast_config.AuracastBigConfigEn(), - 'fra': auracast_config.AuracastBigConfigFr(), - #auracast_config.broadcast_es, - #auracast_config.broadcast_it, -} -for conf in big_conf.values(): - conf.loop = False - -multicaster = multicast_control.Multicaster( - global_conf, - list(big_conf.values()), -) - -@app.route('/init', methods=['POST']) -async def initialize(): +@app.post("/init") +async def initialize(conf: AuracastConfigGroup): """Initializes the broadcasters.""" - #data = await request.json - #global_conf = auracast_config.AuracastGlobalConfig.from_dict(data['global_config']) - #stream_configs = [auracast_config.AuracastBigConfig.from_dict(big) for big in data['big_configs']] + global global_config_group + global multicaster try: + # initialize the streams dict + global_config_group = conf + multicaster = multicast_control.Multicaster( + conf, + [big for big in conf.bigs], + ) await multicaster.init_broadcast() - return jsonify({"status": "initialized"}), 200 except Exception as e: - return jsonify({"error": str(e)}), 500 + raise HTTPException(status_code=500, detail=str(e)) -@app.route('/shutdown', methods=['POST']) -async def stop(): +@app.post("/stream_lc3") +async def send_audio(audio_data: dict[str, str]): + """Streams pre-coded LC3 audio.""" + if multicaster is None: + raise HTTPException(status_code=500, detail='Auracast endpoint was never intialized') + try: + for big in global_config_group.bigs: + assert big.language in audio_data, HTTPException(status_code=500, detail='language len missmatch') + log.info('Received a send audio request for %s', big.language) + big.audio_source = audio_data[big.language].encode('latin-1') + + multicaster.big_conf = global_config_group.bigs + multicaster.start_streaming() + return {"status": "audio_sent"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/shutdown") +async def shutdown(): """Stops broadcasting.""" try: await multicaster.reset() - return jsonify({"status": "stopped"}), 200 + return {"status": "stopped"} except Exception as e: - return jsonify({"error": str(e)}), 500 - -@app.route('/stop_audio', methods=['POST']) + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/stop_audio") async def stop_audio(): """Stops streaming.""" try: multicaster.stop_streaming() - return jsonify({"status": "stopped"}), 200 + return {"status": "stopped"} except Exception as e: - return jsonify({"error": str(e)}), 500 - -@app.route('/stream_lc3', methods=['POST']) -async def send_audio(): - """Streams pre-coded LC3 audio. - # post data in the format - { - broadcast_deu: b'' - broadcast_fra: b'' - - } - """ - post_data = await request.json - try: - for key, val in big_conf.items(): #TODO: loop over caster.big_conf directly - if key in post_data: - log.info('Received a send audio request for %s', key) - val.audio_source = post_data[key].encode('latin-1') - else: - val.audio_source = b'' - - multicaster.big_conf = list(big_conf.values()) - multicaster.start_streaming() - return jsonify({"status": "audio_sent"}), 200 - except Exception as e: - return jsonify({"error": str(e)}), 500 - -# TODO: Also a queue should be implemented - probably as its own endpoint, + raise HTTPException(status_code=500, detail=str(e)) -@app.route('/status', methods=['GET']) +@app.get("/status") async def get_status(): """Gets the current status of the multicaster.""" - status = multicaster.get_status() - # TODO: also get queue status, announcements, samples etc. - return jsonify({"status": status}), 200 + if multicaster: + status = multicaster.get_status() + return {"status": status} + else: + return {"status": None} if __name__ == '__main__': + import uvicorn log.basicConfig( level=log.INFO, format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s' ) - app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=5000) \ No newline at end of file -- 2.52.0 From cd556e62b2e73bcc4eca9b095b971f0b0fc3bdf1 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Thu, 6 Mar 2025 18:03:25 +0100 Subject: [PATCH 02/11] basic working cloud model --- src/auracast/multicast_client.py | 3 +-- src/auracast/multicast_server.py | 11 +++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/auracast/multicast_client.py b/src/auracast/multicast_client.py index 801664f..b1c4eb1 100644 --- a/src/auracast/multicast_client.py +++ b/src/auracast/multicast_client.py @@ -40,7 +40,7 @@ def get_status(): if __name__ == "__main__": config = AuracastConfigGroup( - + auracast_config.AuracastBigConfigDe() ) config.transport = 'serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' @@ -53,7 +53,6 @@ if __name__ == "__main__": "deu": read_lc3_file('src/auracast/testdata/announcement_de_10_16_32.lc3').decode('latin-1'), #"eng": read_lc3_file('src/auracast/testdata/announcement_en_10_16_32.lc3').decode('latin-1'), #"fra": read_lc3_file('src/auracast/testdata/announcement_fr_10_16_32.lc3').decode('latin-1'), - } print("Getting status:", get_status()) diff --git a/src/auracast/multicast_server.py b/src/auracast/multicast_server.py index 0dc9133..3160013 100644 --- a/src/auracast/multicast_server.py +++ b/src/auracast/multicast_server.py @@ -6,23 +6,24 @@ from auracast import multicast_control, auracast_config app = FastAPI() - # Initialize global configuration -global_config_group = AuracastConfigGroup() -global_config_group.transport = 'serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' +global_config_group = auracast_config.AuracastConfigGroup() # Create multicast controller multicaster: multicast_control.Multicaster | None = None @app.post("/init") -async def initialize(conf: AuracastConfigGroup): +async def initialize(conf: auracast_config.AuracastConfigGroup): """Initializes the broadcasters.""" global global_config_group global multicaster try: # initialize the streams dict global_config_group = conf + log.info( + 'Initializing multicaster with config:\n %s', conf.model_dump_json(indent=2) + ) multicaster = multicast_control.Multicaster( conf, [big for big in conf.bigs], @@ -31,6 +32,7 @@ async def initialize(conf: AuracastConfigGroup): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/stream_lc3") async def send_audio(audio_data: dict[str, str]): """Streams pre-coded LC3 audio.""" @@ -78,6 +80,7 @@ async def get_status(): else: return {"status": None} + if __name__ == '__main__': import uvicorn log.basicConfig( -- 2.52.0 From c10ab33f7e80383008b8c1dd460c2107286e2a03 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Tue, 11 Mar 2025 17:34:05 +0100 Subject: [PATCH 03/11] update everything to use three letter iso language codes --- src/auracast/auracast_config.py | 12 ++++++------ src/auracast/multicast.py | 6 +++--- src/auracast/multicast_client.py | 4 ++-- src/auracast/multicast_control.py | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/auracast/auracast_config.py b/src/auracast/auracast_config.py index b3be927..b147ef0 100644 --- a/src/auracast/auracast_config.py +++ b/src/auracast/auracast_config.py @@ -55,7 +55,7 @@ class AuracastBigConfig(BaseModel): precode_wav: bool = False iso_que_len: int = 64 -class AuracastBigConfigDe(AuracastBigConfig): +class AuracastBigConfigDeu(AuracastBigConfig): id: int = 12 random_address: str = 'F1:F1:F2:F3:F4:F5' name: str = 'Broadcast0' @@ -63,7 +63,7 @@ class AuracastBigConfigDe(AuracastBigConfig): program_info: str = 'Announcements German' audio_source: str = 'file:./testdata/announcement_de.wav' -class AuracastBigConfigEn(AuracastBigConfig): +class AuracastBigConfigEng(AuracastBigConfig): id: int = 123 random_address: str = 'F2:F1:F2:F3:F4:F5' name: str = 'Broadcast1' @@ -71,7 +71,7 @@ class AuracastBigConfigEn(AuracastBigConfig): program_info: str = 'Announcements English' audio_source: str = 'file:./testdata/announcement_en.wav' -class AuracastBigConfigFr(AuracastBigConfig): +class AuracastBigConfigFra(AuracastBigConfig): id: int = 1234 random_address: str = 'F3:F1:F2:F3:F4:F5' name: str = 'Broadcast2' @@ -79,7 +79,7 @@ class AuracastBigConfigFr(AuracastBigConfig): program_info: str = 'Announcements French' audio_source: str = 'file:./testdata/announcement_fr.wav' -class AuracastBigConfigEs(AuracastBigConfig): +class AuracastBigConfigSpa(AuracastBigConfig): id: int =12345 random_address: str = 'F4:F1:F2:F3:F4:F5' name: str = 'Broadcast3' @@ -87,7 +87,7 @@ class AuracastBigConfigEs(AuracastBigConfig): program_info: str = 'Announcements Spanish' audio_source: str = 'file:./testdata/announcement_es.wav' -class AuracastBigConfigIt(AuracastBigConfig): +class AuracastBigConfigIta(AuracastBigConfig): id: int =1234567 random_address: str = 'F5:F1:F2:F3:F4:F5' name: str = 'Broadcast4' @@ -101,5 +101,5 @@ class AuracastBigConfigIt(AuracastBigConfig): # TODO: Used in the client/server context, should probably be used verywhere class AuracastConfigGroup(AuracastGlobalConfig): bigs: List[AuracastBigConfig] = [ - AuracastBigConfigDe(), + AuracastBigConfigDeu(), ] diff --git a/src/auracast/multicast.py b/src/auracast/multicast.py index 84ec027..244d0fc 100644 --- a/src/auracast/multicast.py +++ b/src/auracast/multicast.py @@ -520,9 +520,9 @@ if __name__ == "__main__": # TODO: How can we use other iso interval than 10ms ?(medium or low rel) ? - nrf53audio receiver repports I2S tx underrun bigs = [ - auracast_config.AuracastBigConfigDe(), - auracast_config.AuracastBigConfigEn(), - auracast_config.AuracastBigConfigFr(), + auracast_config.AuracastBigConfigDeu(), + auracast_config.AuracastBigConfigEng(), + auracast_config.AuracastBigConfigFra(), #auracast_config.AuracastBigConfigEs(), #auracast_config.AuracastBigConfigIt(), ] diff --git a/src/auracast/multicast_client.py b/src/auracast/multicast_client.py index b1c4eb1..71fa5a2 100644 --- a/src/auracast/multicast_client.py +++ b/src/auracast/multicast_client.py @@ -10,7 +10,7 @@ BASE_URL = "http://127.0.0.1:5000" # Adjust based on your actual API URL # TODO: put this to a common location class AuracastConfigGroup(auracast_config.AuracastGlobalConfig): bigs: List[auracast_config.AuracastBigConfig] = [ - auracast_config.AuracastBigConfigDe(), + auracast_config.AuracastBigConfigDeu(), ] @@ -40,7 +40,7 @@ def get_status(): if __name__ == "__main__": config = AuracastConfigGroup( - auracast_config.AuracastBigConfigDe() + auracast_config.AuracastBigConfigDeu() ) config.transport = 'serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' diff --git a/src/auracast/multicast_control.py b/src/auracast/multicast_control.py index 094268e..507eed3 100644 --- a/src/auracast/multicast_control.py +++ b/src/auracast/multicast_control.py @@ -128,9 +128,9 @@ async def main(): global_conf.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' #nrf52dongle hci_uart usb cdc big_conf = [ - auracast_config.AuracastBigConfigDe(), - auracast_config.AuracastBigConfigEn(), - auracast_config.AuracastBigConfigFr(), + auracast_config.AuracastBigConfigDeu(), + auracast_config.AuracastBigConfigEng(), + auracast_config.AuracastBigConfigFra(), #auracast_config.broadcast_es, #auracast_config.broadcast_it, ] -- 2.52.0 From 2bedc8f4093e3b89efcc392cf9fff58898e3748c Mon Sep 17 00:00:00 2001 From: pstruebi Date: Thu, 13 Mar 2025 10:38:06 +0100 Subject: [PATCH 04/11] add auto init functionallity; add base url for client --- src/auracast/multicast_client.py | 51 +++++++++++++++++++++----------- src/auracast/multicast_server.py | 18 +++++++---- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/auracast/multicast_client.py b/src/auracast/multicast_client.py index 71fa5a2..055ec34 100644 --- a/src/auracast/multicast_client.py +++ b/src/auracast/multicast_client.py @@ -1,11 +1,12 @@ import requests -from typing import List +from typing import List, Optional, Dict, Any from auracast import auracast_config from auracast.utils.read_lc3_file import read_lc3_file -BASE_URL = "http://127.0.0.1:5000" # Adjust based on your actual API URL +BASE_URL = "http://127.0.0.1:5000" # Default base URL + # TODO: put this to a common location class AuracastConfigGroup(auracast_config.AuracastGlobalConfig): @@ -14,30 +15,46 @@ class AuracastConfigGroup(auracast_config.AuracastGlobalConfig): ] -def request_init(request_data : AuracastConfigGroup): - response = requests.post(f"{BASE_URL}/init", json=request_data.model_dump()) +def request_init(request_data : AuracastConfigGroup, base_url: Optional[str] = None): + url = base_url if base_url is not None else BASE_URL + response = requests.post(f"{url}/init", json=request_data.model_dump()) if response.status_code != 200: - raise(f"Error: {response.status_code}, {response.text}") - - -def send_audio(data_dict): - response = requests.post(f"{BASE_URL}/stream_lc3", json=data_dict) + raise Exception(f"Error: {response.status_code}, {response.text}") return response.json() -def shutdown(): - response = requests.post(f"{BASE_URL}/shutdown") - return response.json() - -def stop_audio(): - response = requests.post(f"{BASE_URL}/stop_audio") +def send_audio(data_dict: Dict[str, str], base_url: Optional[str] = None): + url = base_url if base_url is not None else BASE_URL + response = requests.post(f"{url}/stream_lc3", json=data_dict) + if response.status_code != 200: + raise Exception(f"Error: {response.status_code}, {response.text}") return response.json() -def get_status(): - response = requests.get(f"{BASE_URL}/status") +def shutdown(base_url: Optional[str] = None): + url = base_url if base_url is not None else BASE_URL + response = requests.post(f"{url}/shutdown") + if response.status_code != 200: + raise Exception(f"Error: {response.status_code}, {response.text}") return response.json() + +def stop_audio(base_url: Optional[str] = None): + url = base_url if base_url is not None else BASE_URL + response = requests.post(f"{url}/stop_audio") + if response.status_code != 200: + raise Exception(f"Error: {response.status_code}, {response.text}") + return response.json() + + +def get_status(base_url: Optional[str] = None): + url = base_url if base_url is not None else BASE_URL + response = requests.get(f"{url}/status") + if response.status_code != 200: + raise Exception(f"Error: {response.status_code}, {response.text}") + return response.json() + + if __name__ == "__main__": config = AuracastConfigGroup( auracast_config.AuracastBigConfigDeu() diff --git a/src/auracast/multicast_server.py b/src/auracast/multicast_server.py index 3160013..50ca5b5 100644 --- a/src/auracast/multicast_server.py +++ b/src/auracast/multicast_server.py @@ -1,7 +1,6 @@ -from typing import List +import glob import logging as log from fastapi import FastAPI, HTTPException -from pydantic import BaseModel from auracast import multicast_control, auracast_config app = FastAPI() @@ -19,6 +18,12 @@ async def initialize(conf: auracast_config.AuracastConfigGroup): global global_config_group global multicaster try: + if conf.transport == 'auto': + serial_devices = glob.glob('/dev/serial/by-id/*') + for device in serial_devices: + if 'usb-ZEPHYR_Zephyr_HCI_UART_sample' in device: + conf.transport = f'serial:{device},115200,rtscts' + break # initialize the streams dict global_config_group = conf log.info( @@ -75,10 +80,13 @@ async def stop_audio(): async def get_status(): """Gets the current status of the multicaster.""" if multicaster: - status = multicaster.get_status() - return {"status": status} + return multicaster.get_status() else: - return {"status": None} + return { + 'is_initialized': False, + 'is_streaming': False, + } + if __name__ == '__main__': -- 2.52.0 From 755e9f129187a40988565aa4b627228cd5d6a70e Mon Sep 17 00:00:00 2001 From: pstruebi Date: Mon, 17 Mar 2025 13:45:38 +0100 Subject: [PATCH 05/11] refractoring --- .gitignore | 1 + src/auracast/multicast_client.py | 15 ++++----------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 32b1bbf..b5cff35 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ coverage/ # Coverage results after running tests with coverage tools .dist-info/ # Wheel metadata (use poetry build to handle this) *.egg-info/ # Egg info directory (automatically created by pip) auracast.egg-info/ +/build # Ignore these file types and extensions *.pyc # Compiled Python files (.pyc, .pyo are automatically ignored by git) diff --git a/src/auracast/multicast_client.py b/src/auracast/multicast_client.py index 055ec34..29e4aba 100644 --- a/src/auracast/multicast_client.py +++ b/src/auracast/multicast_client.py @@ -1,21 +1,14 @@ import requests -from typing import List, Optional, Dict, Any +from typing import Optional, Dict from auracast import auracast_config - from auracast.utils.read_lc3_file import read_lc3_file +from auracast.auracast_config import AuracastConfigGroup BASE_URL = "http://127.0.0.1:5000" # Default base URL -# TODO: put this to a common location -class AuracastConfigGroup(auracast_config.AuracastGlobalConfig): - bigs: List[auracast_config.AuracastBigConfig] = [ - auracast_config.AuracastBigConfigDeu(), - ] - - -def request_init(request_data : AuracastConfigGroup, base_url: Optional[str] = None): +def init(request_data : AuracastConfigGroup, base_url: Optional[str] = None): url = base_url if base_url is not None else BASE_URL response = requests.post(f"{url}/init", json=request_data.model_dump()) if response.status_code != 200: @@ -73,7 +66,7 @@ if __name__ == "__main__": } print("Getting status:", get_status()) - print("Initializing server:", request_init(config)) + print("Initializing server:", init(config)) print("Getting status:", get_status()) print("Sending audio:", send_audio(audio_data)) print("Getting status:", get_status()) -- 2.52.0 From c98c1ea2ec48311886fae7f9bcf34fd49447fa10 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Tue, 18 Mar 2025 11:17:01 +0100 Subject: [PATCH 06/11] Add additional logging and use docker compose in host mode --- docker-compose.yaml | 18 +++++++++++------- src/auracast/multicast_server.py | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index ec410af..5c08d1c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,19 +1,23 @@ services: - app: + multicaster: + privileged: true # Grants full access to all devices (needed for serial access) + restart: unless-stopped + ports: + - "5000:5000" build: - context: . dockerfile: Dockerfile ssh: - default=~/.ssh/id_ed25519 #lappi #- default=~/.ssh/id_rsa #raspi - devices: - - /dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00 + volumes: + - "/dev/serial:/dev/serial" + #devices: + # - /dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00 environment: LOG_LEVEL: INFO - # - DOCKER_BUILDKIT=1 # Enable BuildKit (can also be passed during build) - #command: python ./auracast/multicast_server.py #devserver - command: python ./auracast/multicast.py # continously streaming test app + command: python ./auracast/multicast_server.py + #command: python ./auracast/multicast.py # continously streaming test app # use docker compose up --build \ No newline at end of file diff --git a/src/auracast/multicast_server.py b/src/auracast/multicast_server.py index 50ca5b5..32e4b98 100644 --- a/src/auracast/multicast_server.py +++ b/src/auracast/multicast_server.py @@ -20,8 +20,10 @@ async def initialize(conf: auracast_config.AuracastConfigGroup): try: if conf.transport == 'auto': serial_devices = glob.glob('/dev/serial/by-id/*') + log.info('Found serial devices: %s', serial_devices) for device in serial_devices: if 'usb-ZEPHYR_Zephyr_HCI_UART_sample' in device: + log.info('Using: %s', device) conf.transport = f'serial:{device},115200,rtscts' break # initialize the streams dict @@ -88,7 +90,6 @@ async def get_status(): } - if __name__ == '__main__': import uvicorn log.basicConfig( -- 2.52.0 From b45d6364c9a82fa168354e0acad84debce62135d Mon Sep 17 00:00:00 2001 From: pstruebi Date: Tue, 18 Mar 2025 13:45:35 +0100 Subject: [PATCH 07/11] implement better handling if auto init is not working --- src/auracast/multicast_server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/auracast/multicast_server.py b/src/auracast/multicast_server.py index 32e4b98..f98273a 100644 --- a/src/auracast/multicast_server.py +++ b/src/auracast/multicast_server.py @@ -26,6 +26,11 @@ async def initialize(conf: auracast_config.AuracastConfigGroup): log.info('Using: %s', device) conf.transport = f'serial:{device},115200,rtscts' break + + # check again if transport is still auto + if conf.transport == 'auto': + HTTPException(status_code=500, detail='No suitable transport found.') + # initialize the streams dict global_config_group = conf log.info( -- 2.52.0 From e9c50f6b96d6c6920cff41399a10c42ed6b48ee6 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Tue, 18 Mar 2025 15:01:21 +0100 Subject: [PATCH 08/11] refractoring --- src/auracast/auracast_config.py | 10 +++---- src/auracast/multicast.py | 45 +++++++++++++++----------------- src/auracast/multicast_server.py | 2 +- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/auracast/auracast_config.py b/src/auracast/auracast_config.py index b147ef0..56b14e7 100644 --- a/src/auracast/auracast_config.py +++ b/src/auracast/auracast_config.py @@ -60,7 +60,7 @@ class AuracastBigConfigDeu(AuracastBigConfig): random_address: str = 'F1:F1:F2:F3:F4:F5' name: str = 'Broadcast0' language: str ='deu' - program_info: str = 'Announcements German' + program_info: str = 'German' audio_source: str = 'file:./testdata/announcement_de.wav' class AuracastBigConfigEng(AuracastBigConfig): @@ -68,7 +68,7 @@ class AuracastBigConfigEng(AuracastBigConfig): random_address: str = 'F2:F1:F2:F3:F4:F5' name: str = 'Broadcast1' language: str ='eng' - program_info: str = 'Announcements English' + program_info: str = 'English' audio_source: str = 'file:./testdata/announcement_en.wav' class AuracastBigConfigFra(AuracastBigConfig): @@ -76,7 +76,7 @@ class AuracastBigConfigFra(AuracastBigConfig): random_address: str = 'F3:F1:F2:F3:F4:F5' name: str = 'Broadcast2' language: str ='fra' - program_info: str = 'Announcements French' + program_info: str = 'French' audio_source: str = 'file:./testdata/announcement_fr.wav' class AuracastBigConfigSpa(AuracastBigConfig): @@ -84,7 +84,7 @@ class AuracastBigConfigSpa(AuracastBigConfig): random_address: str = 'F4:F1:F2:F3:F4:F5' name: str = 'Broadcast3' language: str ='spa' - program_info: str = 'Announcements Spanish' + program_info: str = 'Spanish' audio_source: str = 'file:./testdata/announcement_es.wav' class AuracastBigConfigIta(AuracastBigConfig): @@ -92,7 +92,7 @@ class AuracastBigConfigIta(AuracastBigConfig): random_address: str = 'F5:F1:F2:F3:F4:F5' name: str = 'Broadcast4' language: str ='ita' - program_info: str = 'Announcements Italian' + program_info: str = 'Italian' audio_source: str = 'file:./testdata/announcement_it.wav' diff --git a/src/auracast/multicast.py b/src/auracast/multicast.py index 244d0fc..0bc8542 100644 --- a/src/auracast/multicast.py +++ b/src/auracast/multicast.py @@ -23,7 +23,7 @@ import logging import wave import itertools import struct -from typing import cast, Any, AsyncGenerator, Coroutine, Dict, Optional, Tuple, List +from typing import cast, Any, AsyncGenerator, Coroutine, List import itertools try: @@ -502,31 +502,27 @@ if __name__ == "__main__": ) os.chdir(os.path.dirname(__file__)) - global_conf = auracast_config.AuracastGlobalConfig( - qos_config=auracast_config.AuracastQosHigh() + + config = auracast_config.AuracastConfigGroup( + bigs = [ + auracast_config.AuracastBigConfigDeu(), + #auracast_config.AuracastBigConfigEng(), + #auracast_config.AuracastBigConfigFra(), + #auracast_config.AuracastBigConfigEs(), + #auracast_config.AuracastBigConfigIt(), + ] ) + # TODO: How can we use other iso interval than 10ms ?(medium or low rel) ? - nrf53audio receiver repports I2S tx underrun + config.qos_config=auracast_config.AuracastQosHigh() + #global_conf.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,1000000,rtscts' # transport for nrf52 dongle - #global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001050076061-if02,1000000,rtscts' # transport for nrf53dk - #global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001057705357-if02,1000000,rtscts' # transport for nrf54l15dk - - global_conf.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' #nrf52dongle hci_uart usb cdc - + config.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_95A087EADB030B24-if00,115200,rtscts' #nrf52dongle hci_uart usb cdc # global_conf.transport='usb:2fe3:000b' #nrf52dongle hci_usb # TODO: iso packet over usb not supported - - # TODO: How can we use other iso interval than 10ms ?(medium or low rel) ? - nrf53audio receiver repports I2S tx underrun - - bigs = [ - auracast_config.AuracastBigConfigDeu(), - auracast_config.AuracastBigConfigEng(), - auracast_config.AuracastBigConfigFra(), - #auracast_config.AuracastBigConfigEs(), - #auracast_config.AuracastBigConfigIt(), - ] - for big in bigs: # TODO: encrypted streams are not working + for big in config.bigs: # TODO: encrypted streams are not working #big.code = 'ff'*16 # returns hci/HCI_ENCRYPTION_MODE_NOT_ACCEPTABLE_ERROR #big.code = '78 e5 dc f1 34 ab 42 bf c1 92 ef dd 3a fd 67 ae' big.precode_wav = True @@ -539,14 +535,15 @@ if __name__ == "__main__": # TODO: find the bottleneck - probably airtime # TODO: test encrypted streams - global_conf.auracast_sampling_rate_hz = 16000 - global_conf.octets_per_frame = 40 # 32kbps@16kHz - #global_conf.debug = True + config.auracast_sampling_rate_hz = 16000 + config.octets_per_frame = 40 # 32kbps@16kHz + #config.debug = True + run_async( broadcast( - global_conf, - bigs + config, + config.bigs ) ) diff --git a/src/auracast/multicast_server.py b/src/auracast/multicast_server.py index f98273a..d63ed54 100644 --- a/src/auracast/multicast_server.py +++ b/src/auracast/multicast_server.py @@ -38,7 +38,7 @@ async def initialize(conf: auracast_config.AuracastConfigGroup): ) multicaster = multicast_control.Multicaster( conf, - [big for big in conf.bigs], + conf.bigs, ) await multicaster.init_broadcast() except Exception as e: -- 2.52.0 From cd808bcac9ffec5621f036c6cc73394c8e6a439a Mon Sep 17 00:00:00 2001 From: pstruebi Date: Wed, 19 Mar 2025 09:27:21 +0100 Subject: [PATCH 09/11] change default config values --- src/auracast/auracast_config.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/auracast/auracast_config.py b/src/auracast/auracast_config.py index 56b14e7..a732393 100644 --- a/src/auracast/auracast_config.py +++ b/src/auracast/auracast_config.py @@ -60,7 +60,7 @@ class AuracastBigConfigDeu(AuracastBigConfig): random_address: str = 'F1:F1:F2:F3:F4:F5' name: str = 'Broadcast0' language: str ='deu' - program_info: str = 'German' + program_info: str = 'Announcements German' audio_source: str = 'file:./testdata/announcement_de.wav' class AuracastBigConfigEng(AuracastBigConfig): @@ -68,7 +68,7 @@ class AuracastBigConfigEng(AuracastBigConfig): random_address: str = 'F2:F1:F2:F3:F4:F5' name: str = 'Broadcast1' language: str ='eng' - program_info: str = 'English' + program_info: str = 'Announcements English' audio_source: str = 'file:./testdata/announcement_en.wav' class AuracastBigConfigFra(AuracastBigConfig): @@ -76,7 +76,7 @@ class AuracastBigConfigFra(AuracastBigConfig): random_address: str = 'F3:F1:F2:F3:F4:F5' name: str = 'Broadcast2' language: str ='fra' - program_info: str = 'French' + program_info: str = 'Announcements French' audio_source: str = 'file:./testdata/announcement_fr.wav' class AuracastBigConfigSpa(AuracastBigConfig): @@ -84,7 +84,7 @@ class AuracastBigConfigSpa(AuracastBigConfig): random_address: str = 'F4:F1:F2:F3:F4:F5' name: str = 'Broadcast3' language: str ='spa' - program_info: str = 'Spanish' + program_info: str = 'Announcements Spanish' audio_source: str = 'file:./testdata/announcement_es.wav' class AuracastBigConfigIta(AuracastBigConfig): @@ -92,13 +92,10 @@ class AuracastBigConfigIta(AuracastBigConfig): random_address: str = 'F5:F1:F2:F3:F4:F5' name: str = 'Broadcast4' language: str ='ita' - program_info: str = 'Italian' + program_info: str = 'Announcements Italian' audio_source: str = 'file:./testdata/announcement_it.wav' -# TODO: could be best to merge all in just one CONFIG class and give every language an enable parameter - -# TODO: Used in the client/server context, should probably be used verywhere class AuracastConfigGroup(AuracastGlobalConfig): bigs: List[AuracastBigConfig] = [ AuracastBigConfigDeu(), -- 2.52.0 From 5932915f23fed5b408669ae2136f9634eb529176 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Wed, 19 Mar 2025 10:43:29 +0100 Subject: [PATCH 10/11] use asyncio for the multicast client --- pyproject.toml | 3 +- src/auracast/multicast_client.py | 96 ++++++++++++++++++-------------- 2 files changed, 55 insertions(+), 44 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f999a06..dcf581b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,8 @@ dependencies = [ "aioconsole", "fastapi==0.115.11", "uvicorn==0.34.0", - "pydantic" + "pydantic", + "aiohttp==3.9.3" ] [project.optional-dependencies] diff --git a/src/auracast/multicast_client.py b/src/auracast/multicast_client.py index 29e4aba..708874c 100644 --- a/src/auracast/multicast_client.py +++ b/src/auracast/multicast_client.py @@ -1,4 +1,5 @@ -import requests +import aiohttp +import asyncio from typing import Optional, Dict from auracast import auracast_config @@ -8,65 +9,74 @@ from auracast.auracast_config import AuracastConfigGroup BASE_URL = "http://127.0.0.1:5000" # Default base URL -def init(request_data : AuracastConfigGroup, base_url: Optional[str] = None): +async def init(request_data : AuracastConfigGroup, base_url: Optional[str] = None): url = base_url if base_url is not None else BASE_URL - response = requests.post(f"{url}/init", json=request_data.model_dump()) - if response.status_code != 200: - raise Exception(f"Error: {response.status_code}, {response.text}") - return response.json() + async with aiohttp.ClientSession() as session: + async with session.post(f"{url}/init", json=request_data.model_dump()) as response: + if response.status != 200: + raise Exception(f"Error: {response.status}, {await response.text()}") + return await response.json() -def send_audio(data_dict: Dict[str, str], base_url: Optional[str] = None): +async def send_audio(data_dict: Dict[str, str], base_url: Optional[str] = None): url = base_url if base_url is not None else BASE_URL - response = requests.post(f"{url}/stream_lc3", json=data_dict) - if response.status_code != 200: - raise Exception(f"Error: {response.status_code}, {response.text}") - return response.json() + async with aiohttp.ClientSession() as session: + async with session.post(f"{url}/stream_lc3", json=data_dict) as response: + if response.status != 200: + raise Exception(f"Error: {response.status}, {await response.text()}") + return await response.json() -def shutdown(base_url: Optional[str] = None): +async def shutdown(base_url: Optional[str] = None): url = base_url if base_url is not None else BASE_URL - response = requests.post(f"{url}/shutdown") - if response.status_code != 200: - raise Exception(f"Error: {response.status_code}, {response.text}") - return response.json() + async with aiohttp.ClientSession() as session: + async with session.post(f"{url}/shutdown") as response: + if response.status != 200: + raise Exception(f"Error: {response.status}, {await response.text()}") + return await response.json() -def stop_audio(base_url: Optional[str] = None): +async def stop_audio(base_url: Optional[str] = None): url = base_url if base_url is not None else BASE_URL - response = requests.post(f"{url}/stop_audio") - if response.status_code != 200: - raise Exception(f"Error: {response.status_code}, {response.text}") - return response.json() + async with aiohttp.ClientSession() as session: + async with session.post(f"{url}/stop_audio") as response: + if response.status != 200: + raise Exception(f"Error: {response.status}, {await response.text()}") + return await response.json() -def get_status(base_url: Optional[str] = None): +async def get_status(base_url: Optional[str] = None): url = base_url if base_url is not None else BASE_URL - response = requests.get(f"{url}/status") - if response.status_code != 200: - raise Exception(f"Error: {response.status_code}, {response.text}") - return response.json() + async with aiohttp.ClientSession() as session: + async with session.get(f"{url}/status") as response: + if response.status != 200: + raise Exception(f"Error: {response.status}, {await response.text()}") + return await response.json() if __name__ == "__main__": - config = AuracastConfigGroup( - auracast_config.AuracastBigConfigDeu() - ) - config.transport = 'serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' + import asyncio - # Initialize language-based configurations - for conf in config.bigs: - conf.loop = False + async def main(): + config = AuracastConfigGroup( + auracast_config.AuracastBigConfigDeu() + ) + config.transport = 'serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' + # Initialize language-based configurations + for conf in config.bigs: + conf.loop = False - audio_data = { # TODO: investigate further whats the best way to actually transfer the data - "deu": read_lc3_file('src/auracast/testdata/announcement_de_10_16_32.lc3').decode('latin-1'), - #"eng": read_lc3_file('src/auracast/testdata/announcement_en_10_16_32.lc3').decode('latin-1'), - #"fra": read_lc3_file('src/auracast/testdata/announcement_fr_10_16_32.lc3').decode('latin-1'), - } + audio_data = { # TODO: investigate further whats the best way to actually transfer the data + "deu": read_lc3_file('src/auracast/testdata/announcement_de_10_16_32.lc3').decode('latin-1'), + #"eng": read_lc3_file('src/auracast/testdata/announcement_en_10_16_32.lc3').decode('latin-1'), + #"fra": read_lc3_file('src/auracast/testdata/announcement_fr_10_16_32.lc3').decode('latin-1'), + } - print("Getting status:", get_status()) - print("Initializing server:", init(config)) - print("Getting status:", get_status()) - print("Sending audio:", send_audio(audio_data)) - print("Getting status:", get_status()) + print("Getting status:", await get_status()) + print("Initializing server:", await init(config)) + print("Getting status:", await get_status()) + print("Sending audio:", await send_audio(audio_data)) + print("Getting status:", await get_status()) + + asyncio.run(main()) -- 2.52.0 From c55f67944d31d1898fb22f7e074470ac248e56ac Mon Sep 17 00:00:00 2001 From: pstruebi Date: Wed, 19 Mar 2025 12:51:24 +0100 Subject: [PATCH 11/11] refractoring --- src/auracast/multicast_client.py | 5 +++++ src/auracast/multicast_server.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/auracast/multicast_client.py b/src/auracast/multicast_client.py index 708874c..035f73f 100644 --- a/src/auracast/multicast_client.py +++ b/src/auracast/multicast_client.py @@ -1,5 +1,6 @@ import aiohttp import asyncio +import base64 from typing import Optional, Dict from auracast import auracast_config @@ -19,6 +20,10 @@ async def init(request_data : AuracastConfigGroup, base_url: Optional[str] = Non async def send_audio(data_dict: Dict[str, str], base_url: Optional[str] = None): + # TODO: use base64 encoding + for language, lc3_data in data_dict.items(): + data_dict[language] =lc3_data.decode('latin-1') + url = base_url if base_url is not None else BASE_URL async with aiohttp.ClientSession() as session: async with session.post(f"{url}/stream_lc3", json=data_dict) as response: diff --git a/src/auracast/multicast_server.py b/src/auracast/multicast_server.py index d63ed54..e24b4f0 100644 --- a/src/auracast/multicast_server.py +++ b/src/auracast/multicast_server.py @@ -54,7 +54,7 @@ async def send_audio(audio_data: dict[str, str]): for big in global_config_group.bigs: assert big.language in audio_data, HTTPException(status_code=500, detail='language len missmatch') log.info('Received a send audio request for %s', big.language) - big.audio_source = audio_data[big.language].encode('latin-1') + big.audio_source = audio_data[big.language].encode('latin-1') # TODO: use base64 encoding multicaster.big_conf = global_config_group.bigs multicaster.start_streaming() -- 2.52.0