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