Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
barbibulle committed Nov 15, 2023
1 parent adcf159 commit 77a0ac0
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 79 deletions.
152 changes: 92 additions & 60 deletions bumble/avrcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
from bumble import l2cap
from bumble import avc
from bumble import avctp
from bumble import utils


# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -962,23 +963,30 @@ def __bytes__(self) -> bytes:

# -----------------------------------------------------------------------------
class Delegate:
"""Base class for AVRCP delegates."""
"""
Base class for AVRCP delegates.
volume: int
All the methods are async, even if they don't always need to be, so that
delegates that do need to wait for an async result may do so.
"""

class DelegateException(Exception):
pass
class Error(Exception):
"""The delegate method failed, with a specified status code."""

def __init__(self, status_code: Protocol.StatusCode) -> None:
self.status_code = status_code

supported_events: List[EventId]
volume: int

def __init__(self, supported_events: Iterable[EventId] = ()) -> None:
self.supported_events = list(supported_events)
self.volume = 0

def get_supported_events(self) -> List[EventId]:
async def get_supported_events(self) -> List[EventId]:
return self.supported_events

def set_absolute_volume(self, volume: int) -> None:
async def set_absolute_volume(self, volume: int) -> None:
"""
Set the absolute volume.
Expand All @@ -987,7 +995,7 @@ def set_absolute_volume(self, volume: int) -> None:
logger.debug(f"@@@ set_absolute_volume: volume={volume}")
self.volume = volume

def get_absolute_volume(self) -> int:
async def get_absolute_volume(self) -> int:
return self.volume

# TODO add other delegate methods
Expand Down Expand Up @@ -1052,19 +1060,19 @@ class InvalidPidError(Exception):
class NotPendingError(Exception):
"""There is no pending command for a transaction label."""

class MismatchedResponse(Exception):
class MismatchedResponseError(Exception):
"""The response type does not corresponding to the request type."""

def __init__(self, response: Response) -> None:
self.response = response

class UnexpectedResponseType(Exception):
class UnexpectedResponseTypeError(Exception):
"""The response type is not the expected one."""

def __init__(self, response: Protocol.ResponseContext) -> None:
self.response = response

class UnexpectedResponseCode(Exception):
class UnexpectedResponseCodeError(Exception):
"""The response code was not the expected one."""

def __init__(
Expand Down Expand Up @@ -1183,16 +1191,38 @@ def check_response(
response_context.response_code
!= avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE
):
raise Protocol.UnexpectedResponseCode(
raise Protocol.UnexpectedResponseCodeError(
response_context.response_code, response_context.response
)

if not (isinstance(response_context.response, expected_type)):
raise Protocol.MismatchedResponse(response_context.response)
raise Protocol.MismatchedResponseError(response_context.response)

return response_context.response

raise Protocol.UnexpectedResponseType(response_context)
raise Protocol.UnexpectedResponseTypeError(response_context)

def delegate_command(
self, transaction_label: int, command: Command, method: Awaitable
) -> None:
async def call():
try:
await method
except Delegate.Error as error:
self.send_rejected_avrcp_response(
transaction_label,
command.pdu_id,
error.status_code,
)
except Exception:
logger.exception("delegate method raised exception")
self.send_rejected_avrcp_response(
transaction_label,
command.pdu_id,
Protocol.StatusCode.INTERNAL_ERROR,
)

utils.AsyncRunner.spawn(call())

async def get_supported_events(self) -> List[EventId]:
"""Get the list of events supported by the connected peer."""
Expand Down Expand Up @@ -1229,7 +1259,7 @@ async def monitor_events(
) -> AsyncIterator[Event]:
def check_response(response) -> Event:
if not isinstance(response, RegisterNotificationResponse):
raise self.MismatchedResponse(response)
raise self.MismatchedResponseError(response)

return response.event

Expand All @@ -1247,11 +1277,11 @@ def check_response(response) -> Event:
response = await response.final

if not isinstance(response, self.FinalResponse):
raise self.UnexpectedResponseType(response)
raise self.UnexpectedResponseTypeError(response)

logger.debug(f"final: {response}")
if response.response_code != avc.ResponseFrame.ResponseCode.CHANGED:
raise self.UnexpectedResponseCode(
raise self.UnexpectedResponseCodeError(
response.response_code, response.response
)

Expand Down Expand Up @@ -1783,67 +1813,69 @@ def on_get_capabilities_command(
) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}")

if (
command.capability_id
== GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
):
supported_events = self.delegate.get_supported_events()
async def get_supported_events():
if (
command.capability_id
!= GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
):
raise Protocol.InvalidParameterError

supported_events = await self.delegate.get_supported_events()
self.send_avrcp_response(
transaction_label,
avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
GetCapabilitiesResponse(command.capability_id, supported_events),
)
return

self.send_rejected_avrcp_response(
transaction_label,
self.PduId.GET_CAPABILITIES,
self.StatusCode.INVALID_PARAMETER,
)
self.delegate_command(transaction_label, command, get_supported_events())

def on_set_absolute_volume_command(
self, transaction_label: int, command: SetAbsoluteVolumeCommand
) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}")

self.delegate.set_absolute_volume(command.volume)
self.send_avrcp_response(
transaction_label,
avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
SetAbsoluteVolumeResponse(self.delegate.get_absolute_volume()),
)
async def set_absolute_volume():
await self.delegate.set_absolute_volume(command.volume)
effective_volume = await self.delegate.get_absolute_volume()
self.send_avrcp_response(
transaction_label,
avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
SetAbsoluteVolumeResponse(effective_volume),
)

self.delegate_command(transaction_label, command, set_absolute_volume())

def on_register_notification_command(
self, transaction_label: int, command: RegisterNotificationCommand
) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}")

# Check if the event is supported.
supported_events = self.delegate.get_supported_events()
if command.event_id in supported_events:
if command.event_id == EventId.VOLUME_CHANGED:
response = RegisterNotificationResponse(
VolumeChangedEvent(volume=self.delegate.get_absolute_volume())
)
self.send_avrcp_response(
transaction_label, avc.ResponseFrame.ResponseCode.INTERIM, response
)
self.register_notification_listener(transaction_label, command)
return
async def register_notification():
# Check if the event is supported.
supported_events = await self.delegate.get_supported_events()
if command.event_id in supported_events:
if command.event_id == EventId.VOLUME_CHANGED:
volume = await self.delegate.get_absolute_volume()
response = RegisterNotificationResponse(VolumeChangedEvent(volume))
self.send_avrcp_response(
transaction_label,
avc.ResponseFrame.ResponseCode.INTERIM,
response,
)
self.register_notification_listener(transaction_label, command)
return

if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
# TODO: testing only, use delegate
response = RegisterNotificationResponse(
PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
)
self.send_avrcp_response(
transaction_label, avc.ResponseFrame.ResponseCode.INTERIM, response
)
self.register_notification_listener(transaction_label, command)
return
if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
# TODO: testing only, use delegate
response = RegisterNotificationResponse(
PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
)
self.send_avrcp_response(
transaction_label,
avc.ResponseFrame.ResponseCode.INTERIM,
response,
)
self.register_notification_listener(transaction_label, command)
return

self.send_rejected_avrcp_response(
transaction_label,
self.PduId.REGISTER_NOTIFICATION,
self.StatusCode.INVALID_PARAMETER,
)
self.delegate_command(transaction_label, command, register_notification())
85 changes: 66 additions & 19 deletions examples/avrcp_as_sink.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,40 @@
<div id="buttons"></div><br>
<hr>
<button onclick="onGetPlayStatusButtonClicked()">Get Play Status</button><br>
<span id="getPlayStatusResponseText"></span>
<div id="getPlayStatusResponseTable"></div>
<hr>
<button onclick="onGetElementAttributesButtonClicked()">Get Element Attributes</button><br>
<span id="getElementAttributesResponseText"></span>
<div id="getElementAttributesResponseTable"></div>
<hr>
<b>VOLUME</b>:
<button onclick="onVolumeDownButtonClicked()">-</button>
<button onclick="onVolumeUpButtonClicked()">+</button>
<span id="volumeText"></span><br>
<b>PLAYBACK STATUS</b>: <span id="playbackStatusText"></span><br>
<b>POSITION</b>: <span id="positionText"></span><br>
<b>TRACK</b>: <span id="trackText"></span><br>
<b>ADDRESSED PLAYER</b>: <span id="addressedPlayerText"></span><br>
<b>UID COUNTER</b>: <span id="uidCounterText"></span><br>
<b>SUPPORTED EVENTS</b>: <span id="supportedEventsText"></span><br>
<b>PLAYER SETTINGS</b>: <br><pre><span id="playerSettingsText"></span></pre>
<table>
<tr>
<b>VOLUME</b>:
<button onclick="onVolumeDownButtonClicked()">-</button>
<button onclick="onVolumeUpButtonClicked()">+</button>&nbsp;
<span id="volumeText"></span><br>
</tr>
<tr>
<td><b>PLAYBACK STATUS</b></td><td><span id="playbackStatusText"></span></td>
</tr>
<tr>
<td><b>POSITION</b></td><td><span id="positionText"></span></td>
</tr>
<tr>
<td><b>TRACK</b></td><td><span id="trackText"></span></td>
</tr>
<tr>
<td><b>ADDRESSED PLAYER</b></td><td><span id="addressedPlayerText"></span></td>
</tr>
<tr>
<td><b>UID COUNTER</b></td><td><span id="uidCounterText"></span></td>
</tr>
<tr>
<td><b>SUPPORTED EVENTS</b></td><td><span id="supportedEventsText"></span></td>
</tr>
<tr>
<td><b>PLAYER SETTINGS</b></td><td><div id="playerSettingsTable"></div></td>
</tr>
</table>
<script>
const portInput = document.getElementById("port")
const connectButton = document.getElementById("connectButton")
Expand All @@ -41,9 +59,9 @@
const addressedPlayerText = document.getElementById("addressedPlayerText")
const uidCounterText = document.getElementById("uidCounterText")
const supportedEventsText = document.getElementById("supportedEventsText")
const playerSettingsText = document.getElementById("playerSettingsText")
const getPlayStatusResponseText = document.getElementById("getPlayStatusResponseText")
const getElementAttributesResponseText = document.getElementById("getElementAttributesResponseText")
const playerSettingsTable = document.getElementById("playerSettingsTable")
const getPlayStatusResponseTable = document.getElementById("getPlayStatusResponseTable")
const getElementAttributesResponseTable = document.getElementById("getElementAttributesResponseTable")
let socket
let volume = 0

Expand Down Expand Up @@ -164,6 +182,35 @@
return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}:${position}`
}

function setTableHead(table, columns) {
let thead = table.createTHead()
let row = thead.insertRow()
for (let column of columns) {
let th = document.createElement("th")
let text = document.createTextNode(column)
th.appendChild(text)
row.appendChild(th)
}
}

function createTable(rows) {
const table = document.createElement("table")

if (rows.length != 0) {
columns = Object.keys(rows[0])
setTableHead(table, columns)
}
for (let element of rows) {
let row = table.insertRow()
for (key in element) {
let cell = row.insertCell()
let text = document.createTextNode(element[key])
cell.appendChild(text)
}
}
return table
}

function onMessage(message) {
console.log(message)
if (message.type == "set-volume") {
Expand All @@ -175,17 +222,17 @@
} else if (message.type == "playback-status-changed") {
playbackStatusText.innerText = message.params.status
} else if (message.type == "player-settings-changed") {
playerSettingsText.innerText = JSON.stringify(message.params.settings, undefined, 2)
playerSettingsTable.replaceChildren(message.params.settings)
} else if (message.type == "track-changed") {
trackText.innerText = message.params.identifier
} else if (message.type == "addressed-player-changed") {
addressedPlayerText.innerText = JSON.stringify(message.params.player)
} else if (message.type == "uids-changed") {
uidCounterText.innerText = message.params.uid_counter
} else if (message.type == "get-play-status-response") {
getPlayStatusResponseText.innerText = JSON.stringify(message.params)
getPlayStatusResponseTable.replaceChildren(message.params)
} else if (message.type == "get-element-attributes-response") {
getElementAttributesResponseText.innerText = JSON.stringify(message.params)
getElementAttributesResponseTable.replaceChildren(createTable(message.params))
}
}

Expand Down

0 comments on commit 77a0ac0

Please sign in to comment.