Linting + touches
CI / lint (push) Successful in 8s
Build edge-node / build (push) Failing after 22s
Build icecast / build (push) Failing after 23s
CI / test (push) Successful in 23s
Build op25 / build (push) Failing after 16s

This commit is contained in:
Logan
2026-04-21 00:56:50 -04:00
parent c5984f6318
commit d0e4734cf9
7 changed files with 27 additions and 23 deletions
+9 -9
View File
@@ -10,10 +10,10 @@ from app.config import settings
from app.internal import credentials from app.internal import credentials
from app.internal.logger import logger from app.internal.logger import logger
MAX_RECORDING_SECONDS = 600 # safety cap; drop call if it runs this long MAX_RECORDING_SECONDS = 600 # safety cap; drop call if it runs this long
PRE_BUFFER_SECONDS = 1.0 # seconds of audio to include before call_start PRE_BUFFER_SECONDS = 1.0 # seconds of audio to include before call_start
RING_BUFFER_SECONDS = 60 # how much history to keep when no call is active RING_BUFFER_SECONDS = 60 # how much history to keep when no call is active
READ_CHUNK_BYTES = 4096 # bytes per httpx read READ_CHUNK_BYTES = 4096 # bytes per httpx read
class CallRecorder: class CallRecorder:
@@ -119,9 +119,9 @@ class CallRecorder:
if not self._call_id: if not self._call_id:
return None return None
call_id = self._call_id call_id = self._call_id
call_start = self._call_start_mono call_start = self._call_start_mono
self._call_id = None self._call_id = None
self._call_start_mono = None self._call_start_mono = None
# Slice: everything from (call_start - pre_buffer) to now # Slice: everything from (call_start - pre_buffer) to now
@@ -180,8 +180,8 @@ class CallRecorder:
return None return None
upload_url = f"{settings.c2_url}/upload" upload_url = f"{settings.c2_url}/upload"
api_key = credentials.get_api_key() api_key = credentials.get_api_key()
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
form: dict = {"call_id": call_id, "node_id": settings.node_id} form: dict = {"call_id": call_id, "node_id": settings.node_id}
if talkgroup_id is not None: if talkgroup_id is not None:
@@ -1,6 +1,5 @@
import json import json
from pathlib import Path from pathlib import Path
from typing import Optional
from app.config import settings from app.config import settings
from app.models import NodeConfig, SystemConfig from app.models import NodeConfig, SystemConfig
from app.internal.logger import logger from app.internal.logger import logger
+3 -2
View File
@@ -6,7 +6,7 @@ from app.internal.logger import logger
BOT_READY_TIMEOUT = 15 # seconds to wait for Discord bot to become ready BOT_READY_TIMEOUT = 15 # seconds to wait for Discord bot to become ready
WATCHDOG_INTERVAL = 30 # seconds between voice-connection health checks WATCHDOG_INTERVAL = 30 # seconds between voice-connection health checks
REJOIN_DELAY = 5 # seconds to wait before attempting a rejoin REJOIN_DELAY = 5 # seconds to wait before attempting a rejoin
class RadioBot: class RadioBot:
@@ -116,7 +116,8 @@ class RadioBot:
def _on_stream_end(self, error): def _on_stream_end(self, error):
if error: if error:
logger.error(f"Stream ended with error: {error}") logger.error(f"Stream ended with error: {error}")
if not (self._loop and self._voice_client and self._voice_client.is_connected() and not self._voice_client.is_playing()): vc = self._voice_client
if not (self._loop and vc and vc.is_connected() and not vc.is_playing()):
return return
if error: if error:
# Back off before retrying — prevents tight loop when PulseAudio is unavailable # Back off before retrying — prevents tight loop when PulseAudio is unavailable
+9 -9
View File
@@ -24,14 +24,14 @@ class MQTTManager:
self.on_api_key: Optional[ApiKeyCallback] = None self.on_api_key: Optional[ApiKeyCallback] = None
nid = settings.node_id nid = settings.node_id
self._t_checkin = f"nodes/{nid}/checkin" self._t_checkin = f"nodes/{nid}/checkin"
self._t_status = f"nodes/{nid}/status" self._t_status = f"nodes/{nid}/status"
self._t_metadata = f"nodes/{nid}/metadata" self._t_metadata = f"nodes/{nid}/metadata"
self._t_commands = f"nodes/{nid}/commands" self._t_commands = f"nodes/{nid}/commands"
self._t_config = f"nodes/{nid}/config" self._t_config = f"nodes/{nid}/config"
self._t_api_key = f"nodes/{nid}/api_key" self._t_api_key = f"nodes/{nid}/api_key"
self._t_key_request = f"nodes/{nid}/key_request" self._t_key_request = f"nodes/{nid}/key_request"
self._t_discovery = "nodes/discovery/request" self._t_discovery = "nodes/discovery/request"
def _build_client(self) -> mqtt.Client: def _build_client(self) -> mqtt.Client:
client = mqtt.Client( client = mqtt.Client(
@@ -49,9 +49,9 @@ class MQTTManager:
client.will_set(self._t_status, lwt, qos=1, retain=True) client.will_set(self._t_status, lwt, qos=1, retain=True)
client.reconnect_delay_set(min_delay=2, max_delay=60) client.reconnect_delay_set(min_delay=2, max_delay=60)
client.on_connect = self._on_connect client.on_connect = self._on_connect
client.on_disconnect = self._on_disconnect client.on_disconnect = self._on_disconnect
client.on_message = self._on_message client.on_message = self._on_message
return client return client
def _on_connect(self, client, userdata, flags, reason_code, properties): def _on_connect(self, client, userdata, flags, reason_code, properties):
+4 -1
View File
@@ -44,7 +44,10 @@ async def on_call_end(data: dict):
else: else:
logger.error(f"Audio upload failed for call {data['call_id']}. Verify C2_URL and Node API Key.") logger.error(f"Audio upload failed for call {data['call_id']}. Verify C2_URL and Node API Key.")
else: else:
logger.warning(f"No recording file generated for call {data['call_id']} — call may have been too short or Icecast unreachable.") logger.warning(
f"No recording file generated for call {data['call_id']} "
"— call may have been too short or Icecast unreachable."
)
await mqtt_manager.publish_metadata("call_end", data) await mqtt_manager.publish_metadata("call_end", data)
await mqtt_manager.publish_status("online") await mqtt_manager.publish_status("online")
+1
View File
@@ -1,3 +1,4 @@
# Icecast streaming server
FROM debian:bookworm-slim FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
+1 -1
View File
@@ -1,4 +1,4 @@
## OP25 Core Container # OP25 Core Container
FROM python:slim-trixie FROM python:slim-trixie
# Set environment variables # Set environment variables