File size: 3,494 Bytes
ad88834 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | import asyncio
import os
import time
from collections import deque
from watcherobot.application import ApplicationContext
from watcherobot.application.context import ApplicationEnvironmentError
from watcherobot.application.transport import DaemonApplicationTransport
from watcherobot.errors import WatcheRobotError
class DeviceTransport(DaemonApplicationTransport):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.device_ready = asyncio.Event()
self.audio_sender_lock = asyncio.Lock()
self.events = deque(maxlen=128)
self.add_message_listener(self.observe_ready)
def observe_ready(self, message):
if message["type"] in {
"evt.audio.buffer_status", "evt.microphone.state", "evt.sdk.ready",
"evt.sdk.resource_snapshot", "sys.nack",
}:
self.events.append({"time": time.monotonic(), **message})
if message["type"] == "evt.sdk.ready":
self.device_ready.set()
def start(self, *, timeout=15):
super().start(timeout=timeout)
@property
def closed(self):
"""应用与守护进程之间的通道已经结束,之后的设备指令都无法执行。"""
thread = self._thread
return thread is not None and not thread.is_alive()
async def _on_channels_connected(self):
self.device_ready.clear()
async with asyncio.timeout(self.command_timeout):
await self._send_command("sys.sdk.ready.get", {}, self.command_timeout)
await self.device_ready.wait()
self._started_event.set()
async def _send_audio_stream(self, pcm, stream_id, chunk_bytes):
# 上一段发送任务完成取消清理后,下一段才可以修改共享流控状态。
async with self.audio_sender_lock:
await super()._send_audio_stream(pcm, stream_id, chunk_bytes)
async def _take_audio_credit(self, stream_id):
condition = self._audio_credit_condition
if condition is None:
raise WatcheRobotError("audio flow control is not initialized")
# 在发送任务内部等待,取消时同时结束 Condition 等待。
async with asyncio.timeout(self.command_timeout), condition:
await condition.wait_for(
lambda: self._audio_flow_stream_id != stream_id
or self._audio_credits > 0 or self._audio_flow_error is not None
)
if self._audio_flow_stream_id != stream_id:
raise WatcheRobotError("audio stream was replaced")
if self._audio_flow_error is not None:
raise WatcheRobotError(f"audio stream failed: {self._audio_flow_error}")
self._audio_credits -= 1
class DeviceContext(ApplicationContext):
@property
def connection_lost(self):
return self._transport.closed
@classmethod
def from_environment(cls):
required = (
"WATCHER_APP_ID", "WATCHER_APP_RUN_CREDENTIAL",
"WATCHER_APP_DESKTOP_WS_URL", "WATCHER_APP_DEVICE_WS_URL",
)
environment = {name: os.environ.get(name, "").strip() for name in required}
missing = [name for name, value in environment.items() if not value]
if missing:
raise ApplicationEnvironmentError(
"Application environment is incomplete: " + ", ".join(sorted(missing))
)
return cls(app_id=environment["WATCHER_APP_ID"], transport=DeviceTransport())
|