use fastapi for the client/server

This commit is contained in:
2025-03-06 16:15:01 +01:00
parent 948f3a1d90
commit f192d31b5b
5 changed files with 106 additions and 87 deletions
+2 -1
View File
@@ -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"
]
+10 -3
View File
@@ -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
# 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(),
]
+1 -1
View File
@@ -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(
[
+37 -10
View File
@@ -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())
+56 -72
View File
@@ -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)
uvicorn.run(app, host="0.0.0.0", port=5000)