| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2011-2014 Intel Corporation |
| * Copyright (C) 2002-2010 Marcel Holtmann <marcel@holtmann.org> |
| * |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <ctype.h> |
| #include <inttypes.h> |
| |
| #include "lib/bluetooth.h" |
| |
| #include "src/shared/util.h" |
| #include "bt.h" |
| #include "packet.h" |
| #include "display.h" |
| #include "l2cap.h" |
| #include "uuid.h" |
| #include "keys.h" |
| #include "sdp.h" |
| #include "avctp.h" |
| |
| /* ctype entries */ |
| #define AVC_CTYPE_CONTROL 0x0 |
| #define AVC_CTYPE_STATUS 0x1 |
| #define AVC_CTYPE_SPECIFIC_INQUIRY 0x2 |
| #define AVC_CTYPE_NOTIFY 0x3 |
| #define AVC_CTYPE_GENERAL_INQUIRY 0x4 |
| #define AVC_CTYPE_NOT_IMPLEMENTED 0x8 |
| #define AVC_CTYPE_ACCEPTED 0x9 |
| #define AVC_CTYPE_REJECTED 0xA |
| #define AVC_CTYPE_IN_TRANSITION 0xB |
| #define AVC_CTYPE_STABLE 0xC |
| #define AVC_CTYPE_CHANGED 0xD |
| #define AVC_CTYPE_INTERIM 0xF |
| |
| /* subunit type */ |
| #define AVC_SUBUNIT_MONITOR 0x00 |
| #define AVC_SUBUNIT_AUDIO 0x01 |
| #define AVC_SUBUNIT_PRINTER 0x02 |
| #define AVC_SUBUNIT_DISC 0x03 |
| #define AVC_SUBUNIT_TAPE 0x04 |
| #define AVC_SUBUNIT_TUNER 0x05 |
| #define AVC_SUBUNIT_CA 0x06 |
| #define AVC_SUBUNIT_CAMERA 0x07 |
| #define AVC_SUBUNIT_PANEL 0x09 |
| #define AVC_SUBUNIT_BULLETIN_BOARD 0x0a |
| #define AVC_SUBUNIT_CAMERA_STORAGE 0x0b |
| #define AVC_SUBUNIT_VENDOR_UNIQUE 0x0c |
| #define AVC_SUBUNIT_EXTENDED 0x1e |
| #define AVC_SUBUNIT_UNIT 0x1f |
| |
| /* opcodes */ |
| #define AVC_OP_VENDORDEP 0x00 |
| #define AVC_OP_UNITINFO 0x30 |
| #define AVC_OP_SUBUNITINFO 0x31 |
| #define AVC_OP_PASSTHROUGH 0x7c |
| |
| /* notification events */ |
| #define AVRCP_EVENT_PLAYBACK_STATUS_CHANGED 0x01 |
| #define AVRCP_EVENT_TRACK_CHANGED 0x02 |
| #define AVRCP_EVENT_TRACK_REACHED_END 0x03 |
| #define AVRCP_EVENT_TRACK_REACHED_START 0x04 |
| #define AVRCP_EVENT_PLAYBACK_POS_CHANGED 0x05 |
| #define AVRCP_EVENT_BATT_STATUS_CHANGED 0x06 |
| #define AVRCP_EVENT_SYSTEM_STATUS_CHANGED 0x07 |
| #define AVRCP_EVENT_PLAYER_APPLICATION_SETTING_CHANGED 0x08 |
| #define AVRCP_EVENT_NOW_PLAYING_CONTENT_CHANGED 0x09 |
| #define AVRCP_EVENT_AVAILABLE_PLAYERS_CHANGED 0x0a |
| #define AVRCP_EVENT_ADDRESSED_PLAYER_CHANGED 0x0b |
| #define AVRCP_EVENT_UIDS_CHANGED 0x0c |
| #define AVRCP_EVENT_VOLUME_CHANGED 0x0d |
| |
| /* error statuses */ |
| #define AVRCP_STATUS_INVALID_COMMAND 0x00 |
| #define AVRCP_STATUS_INVALID_PARAMETER 0x01 |
| #define AVRCP_STATUS_NOT_FOUND 0x02 |
| #define AVRCP_STATUS_INTERNAL_ERROR 0x03 |
| #define AVRCP_STATUS_SUCCESS 0x04 |
| #define AVRCP_STATUS_UID_CHANGED 0x05 |
| #define AVRCP_STATUS_INVALID_DIRECTION 0x07 |
| #define AVRCP_STATUS_NOT_DIRECTORY 0x08 |
| #define AVRCP_STATUS_DOES_NOT_EXIST 0x09 |
| #define AVRCP_STATUS_INVALID_SCOPE 0x0a |
| #define AVRCP_STATUS_OUT_OF_BOUNDS 0x0b |
| #define AVRCP_STATUS_IS_DIRECTORY 0x0c |
| #define AVRCP_STATUS_MEDIA_IN_USE 0x0d |
| #define AVRCP_STATUS_NOW_PLAYING_LIST_FULL 0x0e |
| #define AVRCP_STATUS_SEARCH_NOT_SUPPORTED 0x0f |
| #define AVRCP_STATUS_SEARCH_IN_PROGRESS 0x10 |
| #define AVRCP_STATUS_INVALID_PLAYER_ID 0x11 |
| #define AVRCP_STATUS_PLAYER_NOT_BROWSABLE 0x12 |
| #define AVRCP_STATUS_PLAYER_NOT_ADDRESSED 0x13 |
| #define AVRCP_STATUS_NO_VALID_SEARCH_RESULTS 0x14 |
| #define AVRCP_STATUS_NO_AVAILABLE_PLAYERS 0x15 |
| #define AVRCP_STATUS_ADDRESSED_PLAYER_CHANGED 0x16 |
| |
| /* pdu ids */ |
| #define AVRCP_GET_CAPABILITIES 0x10 |
| #define AVRCP_LIST_PLAYER_ATTRIBUTES 0x11 |
| #define AVRCP_LIST_PLAYER_VALUES 0x12 |
| #define AVRCP_GET_CURRENT_PLAYER_VALUE 0x13 |
| #define AVRCP_SET_PLAYER_VALUE 0x14 |
| #define AVRCP_GET_PLAYER_ATTRIBUTE_TEXT 0x15 |
| #define AVRCP_GET_PLAYER_VALUE_TEXT 0x16 |
| #define AVRCP_DISPLAYABLE_CHARSET 0x17 |
| #define AVRCP_CT_BATTERY_STATUS 0x18 |
| #define AVRCP_GET_ELEMENT_ATTRIBUTES 0x20 |
| #define AVRCP_GET_PLAY_STATUS 0x30 |
| #define AVRCP_REGISTER_NOTIFICATION 0x31 |
| #define AVRCP_REQUEST_CONTINUING 0x40 |
| #define AVRCP_ABORT_CONTINUING 0x41 |
| #define AVRCP_SET_ABSOLUTE_VOLUME 0x50 |
| #define AVRCP_SET_ADDRESSED_PLAYER 0x60 |
| #define AVRCP_SET_BROWSED_PLAYER 0x70 |
| #define AVRCP_GET_FOLDER_ITEMS 0x71 |
| #define AVRCP_CHANGE_PATH 0x72 |
| #define AVRCP_GET_ITEM_ATTRIBUTES 0x73 |
| #define AVRCP_PLAY_ITEM 0x74 |
| #define AVRCP_GET_TOTAL_NUMBER_OF_ITEMS 0x75 |
| #define AVRCP_SEARCH 0x80 |
| #define AVRCP_ADD_TO_NOW_PLAYING 0x90 |
| #define AVRCP_GENERAL_REJECT 0xA0 |
| |
| /* Packet types */ |
| #define AVRCP_PACKET_TYPE_SINGLE 0x00 |
| #define AVRCP_PACKET_TYPE_START 0x01 |
| #define AVRCP_PACKET_TYPE_CONTINUING 0x02 |
| #define AVRCP_PACKET_TYPE_END 0x03 |
| |
| /* player attributes */ |
| #define AVRCP_ATTRIBUTE_ILEGAL 0x00 |
| #define AVRCP_ATTRIBUTE_EQUALIZER 0x01 |
| #define AVRCP_ATTRIBUTE_REPEAT_MODE 0x02 |
| #define AVRCP_ATTRIBUTE_SHUFFLE 0x03 |
| #define AVRCP_ATTRIBUTE_SCAN 0x04 |
| |
| /* media attributes */ |
| #define AVRCP_MEDIA_ATTRIBUTE_ILLEGAL 0x00 |
| #define AVRCP_MEDIA_ATTRIBUTE_TITLE 0x01 |
| #define AVRCP_MEDIA_ATTRIBUTE_ARTIST 0x02 |
| #define AVRCP_MEDIA_ATTRIBUTE_ALBUM 0x03 |
| #define AVRCP_MEDIA_ATTRIBUTE_TRACK 0x04 |
| #define AVRCP_MEDIA_ATTRIBUTE_TOTAL 0x05 |
| #define AVRCP_MEDIA_ATTRIBUTE_GENRE 0x06 |
| #define AVRCP_MEDIA_ATTRIBUTE_DURATION 0x07 |
| |
| /* play status */ |
| #define AVRCP_PLAY_STATUS_STOPPED 0x00 |
| #define AVRCP_PLAY_STATUS_PLAYING 0x01 |
| #define AVRCP_PLAY_STATUS_PAUSED 0x02 |
| #define AVRCP_PLAY_STATUS_FWD_SEEK 0x03 |
| #define AVRCP_PLAY_STATUS_REV_SEEK 0x04 |
| #define AVRCP_PLAY_STATUS_ERROR 0xFF |
| |
| /* media scope */ |
| #define AVRCP_MEDIA_PLAYER_LIST 0x00 |
| #define AVRCP_MEDIA_PLAYER_VFS 0x01 |
| #define AVRCP_MEDIA_SEARCH 0x02 |
| #define AVRCP_MEDIA_NOW_PLAYING 0x03 |
| |
| /* Media Item Type */ |
| #define AVRCP_MEDIA_PLAYER_ITEM_TYPE 0x01 |
| #define AVRCP_FOLDER_ITEM_TYPE 0x02 |
| #define AVRCP_MEDIA_ELEMENT_ITEM_TYPE 0x03 |
| |
| /* operands in passthrough commands */ |
| #define AVC_PANEL_VOLUME_UP 0x41 |
| #define AVC_PANEL_VOLUME_DOWN 0x42 |
| #define AVC_PANEL_MUTE 0x43 |
| #define AVC_PANEL_PLAY 0x44 |
| #define AVC_PANEL_STOP 0x45 |
| #define AVC_PANEL_PAUSE 0x46 |
| #define AVC_PANEL_RECORD 0x47 |
| #define AVC_PANEL_REWIND 0x48 |
| #define AVC_PANEL_FAST_FORWARD 0x49 |
| #define AVC_PANEL_EJECT 0x4a |
| #define AVC_PANEL_FORWARD 0x4b |
| #define AVC_PANEL_BACKWARD 0x4c |
| |
| struct avctp_frame { |
| uint8_t hdr; |
| uint8_t pt; |
| uint16_t pid; |
| struct l2cap_frame l2cap_frame; |
| }; |
| |
| static struct avrcp_continuing { |
| uint16_t num; |
| uint16_t size; |
| } avrcp_continuing; |
| |
| static const char *ctype2str(uint8_t ctype) |
| { |
| switch (ctype & 0x0f) { |
| case AVC_CTYPE_CONTROL: |
| return "Control"; |
| case AVC_CTYPE_STATUS: |
| return "Status"; |
| case AVC_CTYPE_SPECIFIC_INQUIRY: |
| return "Specific Inquiry"; |
| case AVC_CTYPE_NOTIFY: |
| return "Notify"; |
| case AVC_CTYPE_GENERAL_INQUIRY: |
| return "General Inquiry"; |
| case AVC_CTYPE_NOT_IMPLEMENTED: |
| return "Not Implemented"; |
| case AVC_CTYPE_ACCEPTED: |
| return "Accepted"; |
| case AVC_CTYPE_REJECTED: |
| return "Rejected"; |
| case AVC_CTYPE_IN_TRANSITION: |
| return "In Transition"; |
| case AVC_CTYPE_STABLE: |
| return "Stable"; |
| case AVC_CTYPE_CHANGED: |
| return "Changed"; |
| case AVC_CTYPE_INTERIM: |
| return "Interim"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *subunit2str(uint8_t subunit) |
| { |
| switch (subunit) { |
| case AVC_SUBUNIT_MONITOR: |
| return "Monitor"; |
| case AVC_SUBUNIT_AUDIO: |
| return "Audio"; |
| case AVC_SUBUNIT_PRINTER: |
| return "Printer"; |
| case AVC_SUBUNIT_DISC: |
| return "Disc"; |
| case AVC_SUBUNIT_TAPE: |
| return "Tape"; |
| case AVC_SUBUNIT_TUNER: |
| return "Tuner"; |
| case AVC_SUBUNIT_CA: |
| return "CA"; |
| case AVC_SUBUNIT_CAMERA: |
| return "Camera"; |
| case AVC_SUBUNIT_PANEL: |
| return "Panel"; |
| case AVC_SUBUNIT_BULLETIN_BOARD: |
| return "Bulletin Board"; |
| case AVC_SUBUNIT_CAMERA_STORAGE: |
| return "Camera Storage"; |
| case AVC_SUBUNIT_VENDOR_UNIQUE: |
| return "Vendor Unique"; |
| case AVC_SUBUNIT_EXTENDED: |
| return "Extended to next byte"; |
| case AVC_SUBUNIT_UNIT: |
| return "Unit"; |
| default: |
| return "Reserved"; |
| } |
| } |
| |
| static const char *opcode2str(uint8_t opcode) |
| { |
| switch (opcode) { |
| case AVC_OP_VENDORDEP: |
| return "Vendor Dependent"; |
| case AVC_OP_UNITINFO: |
| return "Unit Info"; |
| case AVC_OP_SUBUNITINFO: |
| return "Subunit Info"; |
| case AVC_OP_PASSTHROUGH: |
| return "Passthrough"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static char *cap2str(uint8_t cap) |
| { |
| switch (cap) { |
| case 0x2: |
| return "CompanyID"; |
| case 0x3: |
| return "EventsID"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static char *event2str(uint8_t event) |
| { |
| switch (event) { |
| case AVRCP_EVENT_PLAYBACK_STATUS_CHANGED: |
| return "EVENT_PLAYBACK_STATUS_CHANGED"; |
| case AVRCP_EVENT_TRACK_CHANGED: |
| return "EVENT_TRACK_CHANGED"; |
| case AVRCP_EVENT_TRACK_REACHED_END: |
| return "EVENT_TRACK_REACHED_END"; |
| case AVRCP_EVENT_TRACK_REACHED_START: |
| return "EVENT_TRACK_REACHED_START"; |
| case AVRCP_EVENT_PLAYBACK_POS_CHANGED: |
| return "EVENT_PLAYBACK_POS_CHANGED"; |
| case AVRCP_EVENT_BATT_STATUS_CHANGED: |
| return "EVENT_BATT_STATUS_CHANGED"; |
| case AVRCP_EVENT_SYSTEM_STATUS_CHANGED: |
| return "EVENT_SYSTEM_STATUS_CHANGED"; |
| case AVRCP_EVENT_PLAYER_APPLICATION_SETTING_CHANGED: |
| return "EVENT_PLAYER_APPLICATION_SETTING_CHANGED"; |
| case AVRCP_EVENT_NOW_PLAYING_CONTENT_CHANGED: |
| return "EVENT_NOW_PLAYING_CONTENT_CHANGED"; |
| case AVRCP_EVENT_AVAILABLE_PLAYERS_CHANGED: |
| return "EVENT_AVAILABLE_PLAYERS_CHANGED"; |
| case AVRCP_EVENT_ADDRESSED_PLAYER_CHANGED: |
| return "EVENT_ADDRESSED_PLAYER_CHANGED"; |
| case AVRCP_EVENT_UIDS_CHANGED: |
| return "EVENT_UIDS_CHANGED"; |
| case AVRCP_EVENT_VOLUME_CHANGED: |
| return "EVENT_VOLUME_CHANGED"; |
| default: |
| return "Reserved"; |
| } |
| } |
| |
| static const char *error2str(uint8_t status) |
| { |
| switch (status) { |
| case AVRCP_STATUS_INVALID_COMMAND: |
| return "Invalid Command"; |
| case AVRCP_STATUS_INVALID_PARAMETER: |
| return "Invalid Parameter"; |
| case AVRCP_STATUS_NOT_FOUND: |
| return "Not Found"; |
| case AVRCP_STATUS_INTERNAL_ERROR: |
| return "Internal Error"; |
| case AVRCP_STATUS_SUCCESS: |
| return "Success"; |
| case AVRCP_STATUS_UID_CHANGED: |
| return "UID Changed"; |
| case AVRCP_STATUS_INVALID_DIRECTION: |
| return "Invalid Direction"; |
| case AVRCP_STATUS_NOT_DIRECTORY: |
| return "Not a Directory"; |
| case AVRCP_STATUS_DOES_NOT_EXIST: |
| return "Does Not Exist"; |
| case AVRCP_STATUS_INVALID_SCOPE: |
| return "Invalid Scope"; |
| case AVRCP_STATUS_OUT_OF_BOUNDS: |
| return "Range Out of Bounds"; |
| case AVRCP_STATUS_MEDIA_IN_USE: |
| return "Media in Use"; |
| case AVRCP_STATUS_IS_DIRECTORY: |
| return "UID is a Directory"; |
| case AVRCP_STATUS_NOW_PLAYING_LIST_FULL: |
| return "Now Playing List Full"; |
| case AVRCP_STATUS_SEARCH_NOT_SUPPORTED: |
| return "Search Not Supported"; |
| case AVRCP_STATUS_SEARCH_IN_PROGRESS: |
| return "Search in Progress"; |
| case AVRCP_STATUS_INVALID_PLAYER_ID: |
| return "Invalid Player ID"; |
| case AVRCP_STATUS_PLAYER_NOT_BROWSABLE: |
| return "Player Not Browsable"; |
| case AVRCP_STATUS_PLAYER_NOT_ADDRESSED: |
| return "Player Not Addressed"; |
| case AVRCP_STATUS_NO_VALID_SEARCH_RESULTS: |
| return "No Valid Search Result"; |
| case AVRCP_STATUS_NO_AVAILABLE_PLAYERS: |
| return "No Available Players"; |
| case AVRCP_STATUS_ADDRESSED_PLAYER_CHANGED: |
| return "Addressed Player Changed"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *pdu2str(uint8_t pduid) |
| { |
| switch (pduid) { |
| case AVRCP_GET_CAPABILITIES: |
| return "GetCapabilities"; |
| case AVRCP_LIST_PLAYER_ATTRIBUTES: |
| return "ListPlayerApplicationSettingAttributes"; |
| case AVRCP_LIST_PLAYER_VALUES: |
| return "ListPlayerApplicationSettingValues"; |
| case AVRCP_GET_CURRENT_PLAYER_VALUE: |
| return "GetCurrentPlayerApplicationSettingValue"; |
| case AVRCP_SET_PLAYER_VALUE: |
| return "SetPlayerApplicationSettingValue"; |
| case AVRCP_GET_PLAYER_ATTRIBUTE_TEXT: |
| return "GetPlayerApplicationSettingAttributeText"; |
| case AVRCP_GET_PLAYER_VALUE_TEXT: |
| return "GetPlayerApplicationSettingValueText"; |
| case AVRCP_DISPLAYABLE_CHARSET: |
| return "InformDisplayableCharacterSet"; |
| case AVRCP_CT_BATTERY_STATUS: |
| return "InformBatteryStatusOfCT"; |
| case AVRCP_GET_ELEMENT_ATTRIBUTES: |
| return "GetElementAttributes"; |
| case AVRCP_GET_PLAY_STATUS: |
| return "GetPlayStatus"; |
| case AVRCP_REGISTER_NOTIFICATION: |
| return "RegisterNotification"; |
| case AVRCP_REQUEST_CONTINUING: |
| return "RequestContinuingResponse"; |
| case AVRCP_ABORT_CONTINUING: |
| return "AbortContinuingResponse"; |
| case AVRCP_SET_ABSOLUTE_VOLUME: |
| return "SetAbsoluteVolume"; |
| case AVRCP_SET_ADDRESSED_PLAYER: |
| return "SetAddressedPlayer"; |
| case AVRCP_SET_BROWSED_PLAYER: |
| return "SetBrowsedPlayer"; |
| case AVRCP_GET_FOLDER_ITEMS: |
| return "GetFolderItems"; |
| case AVRCP_CHANGE_PATH: |
| return "ChangePath"; |
| case AVRCP_GET_ITEM_ATTRIBUTES: |
| return "GetItemAttributes"; |
| case AVRCP_PLAY_ITEM: |
| return "PlayItem"; |
| case AVRCP_GET_TOTAL_NUMBER_OF_ITEMS: |
| return "GetTotalNumOfItems"; |
| case AVRCP_SEARCH: |
| return "Search"; |
| case AVRCP_ADD_TO_NOW_PLAYING: |
| return "AddToNowPlaying"; |
| case AVRCP_GENERAL_REJECT: |
| return "GeneralReject"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *pt2str(uint8_t pt) |
| { |
| switch (pt) { |
| case AVRCP_PACKET_TYPE_SINGLE: |
| return "Single"; |
| case AVRCP_PACKET_TYPE_START: |
| return "Start"; |
| case AVRCP_PACKET_TYPE_CONTINUING: |
| return "Continuing"; |
| case AVRCP_PACKET_TYPE_END: |
| return "End"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *attr2str(uint8_t attr) |
| { |
| switch (attr) { |
| case AVRCP_ATTRIBUTE_ILEGAL: |
| return "Illegal"; |
| case AVRCP_ATTRIBUTE_EQUALIZER: |
| return "Equalizer ON/OFF Status"; |
| case AVRCP_ATTRIBUTE_REPEAT_MODE: |
| return "Repeat Mode Status"; |
| case AVRCP_ATTRIBUTE_SHUFFLE: |
| return "Shuffle ON/OFF Status"; |
| case AVRCP_ATTRIBUTE_SCAN: |
| return "Scan ON/OFF Status"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *value2str(uint8_t attr, uint8_t value) |
| { |
| switch (attr) { |
| case AVRCP_ATTRIBUTE_ILEGAL: |
| return "Illegal"; |
| case AVRCP_ATTRIBUTE_EQUALIZER: |
| switch (value) { |
| case 0x01: |
| return "OFF"; |
| case 0x02: |
| return "ON"; |
| default: |
| return "Reserved"; |
| } |
| case AVRCP_ATTRIBUTE_REPEAT_MODE: |
| switch (value) { |
| case 0x01: |
| return "OFF"; |
| case 0x02: |
| return "Single Track Repeat"; |
| case 0x03: |
| return "All Track Repeat"; |
| case 0x04: |
| return "Group Repeat"; |
| default: |
| return "Reserved"; |
| } |
| case AVRCP_ATTRIBUTE_SHUFFLE: |
| switch (value) { |
| case 0x01: |
| return "OFF"; |
| case 0x02: |
| return "All Track Shuffle"; |
| case 0x03: |
| return "Group Shuffle"; |
| default: |
| return "Reserved"; |
| } |
| case AVRCP_ATTRIBUTE_SCAN: |
| switch (value) { |
| case 0x01: |
| return "OFF"; |
| case 0x02: |
| return "All Track Scan"; |
| case 0x03: |
| return "Group Scan"; |
| default: |
| return "Reserved"; |
| } |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *charset2str(uint16_t charset) |
| { |
| switch (charset) { |
| case 1: |
| case 2: |
| return "Reserved"; |
| case 3: |
| return "ASCII"; |
| case 4: |
| return "ISO_8859-1"; |
| case 5: |
| return "ISO_8859-2"; |
| case 6: |
| return "ISO_8859-3"; |
| case 7: |
| return "ISO_8859-4"; |
| case 8: |
| return "ISO_8859-5"; |
| case 9: |
| return "ISO_8859-6"; |
| case 10: |
| return "ISO_8859-7"; |
| case 11: |
| return "ISO_8859-8"; |
| case 12: |
| return "ISO_8859-9"; |
| case 106: |
| return "UTF-8"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *mediattr2str(uint32_t attr) |
| { |
| switch (attr) { |
| case AVRCP_MEDIA_ATTRIBUTE_ILLEGAL: |
| return "Illegal"; |
| case AVRCP_MEDIA_ATTRIBUTE_TITLE: |
| return "Title"; |
| case AVRCP_MEDIA_ATTRIBUTE_ARTIST: |
| return "Artist"; |
| case AVRCP_MEDIA_ATTRIBUTE_ALBUM: |
| return "Album"; |
| case AVRCP_MEDIA_ATTRIBUTE_TRACK: |
| return "Track"; |
| case AVRCP_MEDIA_ATTRIBUTE_TOTAL: |
| return "Track Total"; |
| case AVRCP_MEDIA_ATTRIBUTE_GENRE: |
| return "Genre"; |
| case AVRCP_MEDIA_ATTRIBUTE_DURATION: |
| return "Track duration"; |
| default: |
| return "Reserved"; |
| } |
| } |
| |
| static const char *playstatus2str(uint8_t status) |
| { |
| switch (status) { |
| case AVRCP_PLAY_STATUS_STOPPED: |
| return "STOPPED"; |
| case AVRCP_PLAY_STATUS_PLAYING: |
| return "PLAYING"; |
| case AVRCP_PLAY_STATUS_PAUSED: |
| return "PAUSED"; |
| case AVRCP_PLAY_STATUS_FWD_SEEK: |
| return "FWD_SEEK"; |
| case AVRCP_PLAY_STATUS_REV_SEEK: |
| return "REV_SEEK"; |
| case AVRCP_PLAY_STATUS_ERROR: |
| return "ERROR"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *status2str(uint8_t status) |
| { |
| switch (status) { |
| case 0x0: |
| return "NORMAL"; |
| case 0x1: |
| return "WARNING"; |
| case 0x2: |
| return "CRITICAL"; |
| case 0x3: |
| return "EXTERNAL"; |
| case 0x4: |
| return "FULL_CHARGE"; |
| default: |
| return "Reserved"; |
| } |
| } |
| |
| static const char *scope2str(uint8_t scope) |
| { |
| switch (scope) { |
| case AVRCP_MEDIA_PLAYER_LIST: |
| return "Media Player List"; |
| case AVRCP_MEDIA_PLAYER_VFS: |
| return "Media Player Virtual Filesystem"; |
| case AVRCP_MEDIA_SEARCH: |
| return "Search"; |
| case AVRCP_MEDIA_NOW_PLAYING: |
| return "Now Playing"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static char *op2str(uint8_t op) |
| { |
| switch (op & 0x7f) { |
| case AVC_PANEL_VOLUME_UP: |
| return "VOLUME UP"; |
| case AVC_PANEL_VOLUME_DOWN: |
| return "VOLUME DOWN"; |
| case AVC_PANEL_MUTE: |
| return "MUTE"; |
| case AVC_PANEL_PLAY: |
| return "PLAY"; |
| case AVC_PANEL_STOP: |
| return "STOP"; |
| case AVC_PANEL_PAUSE: |
| return "PAUSE"; |
| case AVC_PANEL_RECORD: |
| return "RECORD"; |
| case AVC_PANEL_REWIND: |
| return "REWIND"; |
| case AVC_PANEL_FAST_FORWARD: |
| return "FAST FORWARD"; |
| case AVC_PANEL_EJECT: |
| return "EJECT"; |
| case AVC_PANEL_FORWARD: |
| return "FORWARD"; |
| case AVC_PANEL_BACKWARD: |
| return "BACKWARD"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| static const char *type2str(uint8_t type) |
| { |
| switch (type) { |
| case AVRCP_MEDIA_PLAYER_ITEM_TYPE: |
| return "Media Player"; |
| case AVRCP_FOLDER_ITEM_TYPE: |
| return "Folder"; |
| case AVRCP_MEDIA_ELEMENT_ITEM_TYPE: |
| return "Media Element"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char *playertype2str(uint8_t type) |
| { |
| switch (type & 0x0F) { |
| case 0x01: |
| return "Audio"; |
| case 0x02: |
| return "Video"; |
| case 0x03: |
| return "Audio, Video"; |
| case 0x04: |
| return "Audio Broadcasting"; |
| case 0x05: |
| return "Audio, Audio Broadcasting"; |
| case 0x06: |
| return "Video, Audio Broadcasting"; |
| case 0x07: |
| return "Audio, Video, Audio Broadcasting"; |
| case 0x08: |
| return "Video Broadcasting"; |
| case 0x09: |
| return "Audio, Video Broadcasting"; |
| case 0x0A: |
| return "Video, Video Broadcasting"; |
| case 0x0B: |
| return "Audio, Video, Video Broadcasting"; |
| case 0x0C: |
| return "Audio Broadcasting, Video Broadcasting"; |
| case 0x0D: |
| return "Audio, Audio Broadcasting, Video Broadcasting"; |
| case 0x0E: |
| return "Video, Audio Broadcasting, Video Broadcasting"; |
| case 0x0F: |
| return "Audio, Video, Audio Broadcasting, Video Broadcasting"; |
| } |
| |
| return "None"; |
| } |
| |
| static const char *playersubtype2str(uint32_t subtype) |
| { |
| switch (subtype & 0x03) { |
| case 0x01: |
| return "Audio Book"; |
| case 0x02: |
| return "Podcast"; |
| case 0x03: |
| return "Audio Book, Podcast"; |
| } |
| |
| return "None"; |
| } |
| |
| static const char *foldertype2str(uint8_t type) |
| { |
| switch (type) { |
| case 0x00: |
| return "Mixed"; |
| case 0x01: |
| return "Titles"; |
| case 0x02: |
| return "Albums"; |
| case 0x03: |
| return "Artists"; |
| case 0x04: |
| return "Genres"; |
| case 0x05: |
| return "Playlists"; |
| case 0x06: |
| return "Years"; |
| } |
| |
| return "Reserved"; |
| } |
| |
| static const char *elementtype2str(uint8_t type) |
| { |
| switch (type) { |
| case 0x00: |
| return "Audio"; |
| case 0x01: |
| return "Video"; |
| } |
| |
| return "Reserved"; |
| } |
| |
| static bool avrcp_passthrough_packet(struct avctp_frame *avctp_frame, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t op, len; |
| |
| if (!l2cap_frame_get_u8(frame, &op)) |
| return false; |
| |
| print_field("%*cOperation: 0x%02x (%s %s)", (indent - 8), ' ', op, |
| op2str(op), op & 0x80 ? "Released" : "Pressed"); |
| |
| if (!l2cap_frame_get_u8(frame, &len)) |
| return false; |
| |
| print_field("%*cLength: 0x%02x", (indent - 8), ' ', len); |
| |
| packet_hexdump(frame->data, frame->size); |
| return true; |
| } |
| |
| static bool avrcp_get_capabilities(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t cap, count; |
| int i; |
| |
| if (!l2cap_frame_get_u8(frame, &cap)) |
| return false; |
| |
| print_field("%*cCapabilityID: 0x%02x (%s)", (indent - 8), ' ', cap, |
| cap2str(cap)); |
| |
| if (len == 1) |
| return true; |
| |
| if (!l2cap_frame_get_u8(frame, &count)) |
| return false; |
| |
| print_field("%*cCapabilityCount: 0x%02x", (indent - 8), ' ', count); |
| |
| switch (cap) { |
| case 0x2: |
| for (; count > 0; count--) { |
| uint8_t company[3]; |
| |
| if (!l2cap_frame_get_u8(frame, &company[0]) || |
| !l2cap_frame_get_u8(frame, &company[1]) || |
| !l2cap_frame_get_u8(frame, &company[2])) |
| return false; |
| |
| print_field("%*c%s: 0x%02x%02x%02x", (indent - 8), ' ', |
| cap2str(cap), company[0], company[1], |
| company[2]); |
| } |
| break; |
| case 0x3: |
| for (i = 0; count > 0; count--, i++) { |
| uint8_t event; |
| |
| if (!l2cap_frame_get_u8(frame, &event)) |
| return false; |
| |
| print_field("%*c%s: 0x%02x (%s)", (indent - 8), ' ', |
| cap2str(cap), event, event2str(event)); |
| } |
| break; |
| default: |
| packet_hexdump(frame->data, frame->size); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_list_player_attributes(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t num; |
| int i; |
| |
| if (len == 0) |
| return true; |
| |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (i = 0; num > 0; num--, i++) { |
| uint8_t attr; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", (indent - 8), ' ', |
| attr, attr2str(attr)); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_list_player_values(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| static uint8_t attr = 0; |
| uint8_t num; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", (indent - 8), ' ', |
| attr, attr2str(attr)); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| print_field("%*cValueCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (; num > 0; num--) { |
| uint8_t value; |
| |
| if (!l2cap_frame_get_u8(frame, &value)) |
| return false; |
| |
| print_field("%*cValueID: 0x%02x (%s)", (indent - 8), |
| ' ', value, value2str(attr, value)); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_get_current_player_value(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t num; |
| |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| print_field("%*cAttributeCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (; num > 0; num--) { |
| uint8_t attr; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", (indent - 8), |
| ' ', attr, attr2str(attr)); |
| } |
| |
| return true; |
| |
| response: |
| print_field("%*cValueCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (; num > 0; num--) { |
| uint8_t attr, value; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", (indent - 8), |
| ' ', attr, attr2str(attr)); |
| |
| if (!l2cap_frame_get_u8(frame, &value)) |
| return false; |
| |
| print_field("%*cValueID: 0x%02x (%s)", (indent - 8), |
| ' ', value, value2str(attr, value)); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_set_player_value(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t num; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| return true; |
| |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (; num > 0; num--) { |
| uint8_t attr, value; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", (indent - 8), ' ', |
| attr, attr2str(attr)); |
| |
| if (!l2cap_frame_get_u8(frame, &value)) |
| return false; |
| |
| print_field("%*cValueID: 0x%02x (%s)", (indent - 8), ' ', |
| value, value2str(attr, value)); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_get_player_attribute_text(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t num; |
| |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x", (indent - 8), ' ', num); |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| for (; num > 0; num--) { |
| uint8_t attr; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", (indent - 8), |
| ' ', attr, attr2str(attr)); |
| } |
| |
| return true; |
| |
| response: |
| for (; num > 0; num--) { |
| uint8_t attr, len; |
| uint16_t charset; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", (indent - 8), |
| ' ', attr, attr2str(attr)); |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", (indent - 8), |
| ' ', charset, charset2str(charset)); |
| |
| if (!l2cap_frame_get_u8(frame, &len)) |
| return false; |
| |
| print_field("%*cStringLength: 0x%02x", (indent - 8), ' ', len); |
| |
| printf("String: "); |
| for (; len > 0; len--) { |
| uint8_t c; |
| |
| if (!l2cap_frame_get_u8(frame, &c)) |
| return false; |
| |
| printf("%1c", isprint(c) ? c : '.'); |
| } |
| printf("\n"); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_get_player_value_text(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| static uint8_t attr = 0; |
| uint8_t num; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", (indent - 8), ' ', |
| attr, attr2str(attr)); |
| |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| print_field("%*cValueCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (; num > 0; num--) { |
| uint8_t value; |
| |
| if (!l2cap_frame_get_u8(frame, &value)) |
| return false; |
| |
| print_field("%*cValueID: 0x%02x (%s)", (indent - 8), |
| ' ', value, value2str(attr, value)); |
| } |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| print_field("%*cValueCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (; num > 0; num--) { |
| uint8_t value, len; |
| uint16_t charset; |
| |
| if (!l2cap_frame_get_u8(frame, &value)) |
| return false; |
| |
| print_field("%*cValueID: 0x%02x (%s)", (indent - 8), ' ', |
| value, value2str(attr, value)); |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetIDID: 0x%02x (%s)", (indent - 8), ' ', |
| charset, charset2str(charset)); |
| |
| if (!l2cap_frame_get_u8(frame, &len)) |
| return false; |
| |
| print_field("%*cStringLength: 0x%02x", (indent - 8), ' ', len); |
| |
| printf("String: "); |
| for (; len > 0; len--) { |
| uint8_t c; |
| |
| if (!l2cap_frame_get_u8(frame, &c)) |
| return false; |
| |
| printf("%1c", isprint(c) ? c : '.'); |
| } |
| printf("\n"); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_displayable_charset(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t num; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| return true; |
| |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| print_field("%*cCharsetCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (; num > 0; num--) { |
| uint16_t charset; |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", (indent - 8), |
| ' ', charset, charset2str(charset)); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_get_element_attributes(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint64_t id; |
| uint8_t num; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| if (!l2cap_frame_get_be64(frame, &id)) |
| return false; |
| |
| print_field("%*cIdentifier: 0x%jx (%s)", (indent - 8), ' ', |
| id, id ? "Reserved" : "PLAYING"); |
| |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x", (indent - 8), ' ', num); |
| |
| for (; num > 0; num--) { |
| uint32_t attr; |
| |
| if (!l2cap_frame_get_le32(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%08x (%s)", (indent - 8), |
| ' ', attr, mediattr2str(attr)); |
| } |
| |
| return true; |
| |
| response: |
| switch (avctp_frame->pt) { |
| case AVRCP_PACKET_TYPE_SINGLE: |
| case AVRCP_PACKET_TYPE_START: |
| if (!l2cap_frame_get_u8(frame, &num)) |
| return false; |
| |
| avrcp_continuing.num = num; |
| print_field("%*cAttributeCount: 0x%02x", (indent - 8), |
| ' ', num); |
| len--; |
| break; |
| case AVRCP_PACKET_TYPE_CONTINUING: |
| case AVRCP_PACKET_TYPE_END: |
| num = avrcp_continuing.num; |
| |
| if (avrcp_continuing.size > 0) { |
| char attrval[UINT8_MAX] = {0}; |
| uint16_t size; |
| uint8_t idx; |
| |
| if (avrcp_continuing.size > len) { |
| size = len; |
| avrcp_continuing.size -= len; |
| } else { |
| size = avrcp_continuing.size; |
| avrcp_continuing.size = 0; |
| } |
| |
| for (idx = 0; size > 0; idx++, size--) { |
| uint8_t c; |
| |
| if (!l2cap_frame_get_u8(frame, &c)) |
| goto failed; |
| |
| sprintf(&attrval[idx], "%1c", |
| isprint(c) ? c : '.'); |
| } |
| print_field("%*cContinuingAttributeValue: %s", |
| (indent - 8), ' ', attrval); |
| |
| len -= size; |
| } |
| break; |
| default: |
| goto failed; |
| } |
| |
| while (num > 0 && len > 0) { |
| uint32_t attr; |
| uint16_t charset, attrlen; |
| uint8_t idx; |
| char attrval[UINT8_MAX] = {0}; |
| |
| if (!l2cap_frame_get_be32(frame, &attr)) |
| goto failed; |
| |
| print_field("%*cAttribute: 0x%08x (%s)", (indent - 8), |
| ' ', attr, mediattr2str(attr)); |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| goto failed; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", (indent - 8), |
| ' ', charset, charset2str(charset)); |
| |
| if (!l2cap_frame_get_be16(frame, &attrlen)) |
| goto failed; |
| |
| print_field("%*cAttributeValueLength: 0x%04x", |
| (indent - 8), ' ', attrlen); |
| |
| len -= sizeof(attr) + sizeof(charset) + sizeof(attrlen); |
| num--; |
| |
| for (idx = 0; attrlen > 0 && len > 0; idx++, attrlen--, len--) { |
| uint8_t c; |
| |
| if (!l2cap_frame_get_u8(frame, &c)) |
| goto failed; |
| |
| sprintf(&attrval[idx], "%1c", isprint(c) ? c : '.'); |
| } |
| print_field("%*cAttributeValue: %s", (indent - 8), |
| ' ', attrval); |
| |
| if (attrlen > 0) |
| avrcp_continuing.size = attrlen; |
| } |
| |
| avrcp_continuing.num = num; |
| return true; |
| |
| failed: |
| avrcp_continuing.num = 0; |
| avrcp_continuing.size = 0; |
| return false; |
| } |
| |
| static bool avrcp_get_play_status(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint32_t interval; |
| uint8_t status; |
| |
| if (ctype <= AVC_CTYPE_GENERAL_INQUIRY) |
| return true; |
| |
| if (!l2cap_frame_get_be32(frame, &interval)) |
| return false; |
| |
| print_field("%*cSongLength: 0x%08x (%u miliseconds)", |
| (indent - 8), ' ', interval, interval); |
| |
| if (!l2cap_frame_get_be32(frame, &interval)) |
| return false; |
| |
| print_field("%*cSongPosition: 0x%08x (%u miliseconds)", |
| (indent - 8), ' ', interval, interval); |
| |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cPlayStatus: 0x%02x (%s)", (indent - 8), |
| ' ', status, playstatus2str(status)); |
| |
| return true; |
| } |
| |
| static bool avrcp_register_notification(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t event, status; |
| uint16_t uid; |
| uint32_t interval; |
| uint64_t id; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| if (!l2cap_frame_get_u8(frame, &event)) |
| return false; |
| |
| print_field("%*cEventID: 0x%02x (%s)", (indent - 8), |
| ' ', event, event2str(event)); |
| |
| if (!l2cap_frame_get_be32(frame, &interval)) |
| return false; |
| |
| print_field("%*cInterval: 0x%08x (%u seconds)", |
| (indent - 8), ' ', interval, interval); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &event)) |
| return false; |
| |
| print_field("%*cEventID: 0x%02x (%s)", (indent - 8), |
| ' ', event, event2str(event)); |
| |
| switch (event) { |
| case AVRCP_EVENT_PLAYBACK_STATUS_CHANGED: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cPlayStatus: 0x%02x (%s)", (indent - 8), |
| ' ', status, playstatus2str(status)); |
| break; |
| case AVRCP_EVENT_TRACK_CHANGED: |
| if (!l2cap_frame_get_be64(frame, &id)) |
| return false; |
| |
| print_field("%*cIdentifier: 0x%16" PRIx64 " (%" PRIu64 ")", |
| (indent - 8), ' ', id, id); |
| break; |
| case AVRCP_EVENT_PLAYBACK_POS_CHANGED: |
| if (!l2cap_frame_get_be32(frame, &interval)) |
| return false; |
| |
| print_field("%*cPosition: 0x%08x (%u miliseconds)", |
| (indent - 8), ' ', interval, interval); |
| break; |
| case AVRCP_EVENT_BATT_STATUS_CHANGED: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cBatteryStatus: 0x%02x (%s)", (indent - 8), |
| ' ', status, status2str(status)); |
| |
| break; |
| case AVRCP_EVENT_SYSTEM_STATUS_CHANGED: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cSystemStatus: 0x%02x ", (indent - 8), |
| ' ', status); |
| switch (status) { |
| case 0x00: |
| printf("(POWER_ON)\n"); |
| break; |
| case 0x01: |
| printf("(POWER_OFF)\n"); |
| break; |
| case 0x02: |
| printf("(UNPLUGGED)\n"); |
| break; |
| default: |
| printf("(UNKNOWN)\n"); |
| break; |
| } |
| break; |
| case AVRCP_EVENT_PLAYER_APPLICATION_SETTING_CHANGED: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x", (indent - 8), |
| ' ', status); |
| |
| for (; status > 0; status--) { |
| uint8_t attr, value; |
| |
| if (!l2cap_frame_get_u8(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%02x (%s)", |
| (indent - 8), ' ', attr, attr2str(attr)); |
| |
| if (!l2cap_frame_get_u8(frame, &value)) |
| return false; |
| |
| print_field("%*cValueID: 0x%02x (%s)", (indent - 8), |
| ' ', value, value2str(attr, value)); |
| } |
| break; |
| case AVRCP_EVENT_VOLUME_CHANGED: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| status &= 0x7F; |
| |
| print_field("%*cVolume: %.2f%% (%d/127)", (indent - 8), |
| ' ', status/1.27, status); |
| break; |
| case AVRCP_EVENT_ADDRESSED_PLAYER_CHANGED: |
| if (!l2cap_frame_get_be16(frame, &uid)) |
| return false; |
| |
| print_field("%*cPlayerID: 0x%04x (%u)", (indent - 8), |
| ' ', uid, uid); |
| |
| if (!l2cap_frame_get_be16(frame, &uid)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", (indent - 8), |
| ' ', uid, uid); |
| break; |
| case AVRCP_EVENT_UIDS_CHANGED: |
| if (!l2cap_frame_get_be16(frame, &uid)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", (indent - 8), |
| ' ', uid, uid); |
| break; |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_set_absolute_volume(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t value; |
| |
| if (!l2cap_frame_get_u8(frame, &value)) |
| return false; |
| |
| value &= 0x7F; |
| print_field("%*cVolume: %.2f%% (%d/127)", (indent - 8), |
| ' ', value/1.27, value); |
| |
| return true; |
| } |
| |
| static bool avrcp_set_addressed_player(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint16_t id; |
| uint8_t status; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| if (!l2cap_frame_get_be16(frame, &id)) |
| return false; |
| |
| print_field("%*cPlayerID: 0x%04x (%u)", (indent - 8), ' ', id, id); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", (indent - 8), ' ', |
| status, error2str(status)); |
| |
| return true; |
| } |
| |
| static bool avrcp_play_item(struct avctp_frame *avctp_frame, uint8_t ctype, |
| uint8_t len, uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint64_t uid; |
| uint16_t uidcounter; |
| uint8_t scope, status; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| if (!l2cap_frame_get_u8(frame, &scope)) |
| return false; |
| |
| print_field("%*cScope: 0x%02x (%s)", (indent - 8), ' ', |
| scope, scope2str(scope)); |
| |
| if (!l2cap_frame_get_be64(frame, &uid)) |
| return false; |
| |
| print_field("%*cUID: 0x%16" PRIx64 " (%" PRIu64 ")", (indent - 8), |
| ' ', uid, uid); |
| |
| if (!l2cap_frame_get_be16(frame, &uidcounter)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", (indent - 8), ' ', |
| uidcounter, uidcounter); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", (indent - 8), ' ', status, |
| error2str(status)); |
| |
| return true; |
| } |
| |
| static bool avrcp_add_to_now_playing(struct avctp_frame *avctp_frame, |
| uint8_t ctype, uint8_t len, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint64_t uid; |
| uint16_t uidcounter; |
| uint8_t scope, status; |
| |
| if (ctype > AVC_CTYPE_GENERAL_INQUIRY) |
| goto response; |
| |
| if (!l2cap_frame_get_u8(frame, &scope)) |
| return false; |
| |
| print_field("%*cScope: 0x%02x (%s)", (indent - 8), ' ', |
| scope, scope2str(scope)); |
| |
| if (!l2cap_frame_get_be64(frame, &uid)) |
| return false; |
| |
| print_field("%*cUID: 0x%16" PRIx64 " (%" PRIu64 ")", (indent - 8), |
| ' ', uid, uid); |
| |
| if (!l2cap_frame_get_be16(frame, &uidcounter)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", (indent - 8), ' ', |
| uidcounter, uidcounter); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", (indent - 8), ' ', status, |
| error2str(status)); |
| |
| return true; |
| } |
| |
| struct avrcp_ctrl_pdu_data { |
| uint8_t pduid; |
| bool (*func) (struct avctp_frame *avctp_frame, uint8_t ctype, |
| uint8_t len, uint8_t indent); |
| }; |
| |
| static const struct avrcp_ctrl_pdu_data avrcp_ctrl_pdu_table[] = { |
| { 0x10, avrcp_get_capabilities }, |
| { 0x11, avrcp_list_player_attributes }, |
| { 0x12, avrcp_list_player_values }, |
| { 0x13, avrcp_get_current_player_value }, |
| { 0x14, avrcp_set_player_value }, |
| { 0x15, avrcp_get_player_attribute_text }, |
| { 0x16, avrcp_get_player_value_text }, |
| { 0x17, avrcp_displayable_charset }, |
| { 0x20, avrcp_get_element_attributes }, |
| { 0x30, avrcp_get_play_status }, |
| { 0x31, avrcp_register_notification }, |
| { 0x50, avrcp_set_absolute_volume }, |
| { 0x60, avrcp_set_addressed_player }, |
| { 0x74, avrcp_play_item }, |
| { 0x90, avrcp_add_to_now_playing }, |
| { } |
| }; |
| |
| static bool avrcp_rejected_packet(struct l2cap_frame *frame, uint8_t indent) |
| { |
| uint8_t status; |
| |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cError: 0x%02x (%s)", (indent - 8), ' ', status, |
| error2str(status)); |
| |
| return true; |
| } |
| |
| static bool avrcp_pdu_packet(struct avctp_frame *avctp_frame, uint8_t ctype, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t pduid; |
| uint16_t len; |
| int i; |
| const struct avrcp_ctrl_pdu_data *ctrl_pdu_data = NULL; |
| |
| if (!l2cap_frame_get_u8(frame, &pduid)) |
| return false; |
| |
| if (!l2cap_frame_get_u8(frame, &avctp_frame->pt)) |
| return false; |
| |
| if (!l2cap_frame_get_be16(frame, &len)) |
| return false; |
| |
| print_indent(indent, COLOR_OFF, "AVRCP: ", pdu2str(pduid), COLOR_OFF, |
| " pt %s len 0x%04x", pt2str(avctp_frame->pt), len); |
| |
| if (frame->size != len) |
| return false; |
| |
| if (ctype == 0xA) |
| return avrcp_rejected_packet(frame, indent + 2); |
| |
| for (i = 0; avrcp_ctrl_pdu_table[i].func; i++) { |
| if (avrcp_ctrl_pdu_table[i].pduid == pduid) { |
| ctrl_pdu_data = &avrcp_ctrl_pdu_table[i]; |
| break; |
| } |
| } |
| |
| if (!ctrl_pdu_data || !ctrl_pdu_data->func) { |
| packet_hexdump(frame->data, frame->size); |
| return true; |
| } |
| |
| return ctrl_pdu_data->func(avctp_frame, ctype, len, indent + 2); |
| } |
| |
| static bool avrcp_control_packet(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| |
| uint8_t ctype, address, subunit, opcode, company[3], indent = 2; |
| |
| if (!l2cap_frame_get_u8(frame, &ctype) || |
| !l2cap_frame_get_u8(frame, &address) || |
| !l2cap_frame_get_u8(frame, &opcode)) |
| return false; |
| |
| print_field("AV/C: %s: address 0x%02x opcode 0x%02x", |
| ctype2str(ctype), address, opcode); |
| |
| subunit = address >> 3; |
| |
| print_field("%*cSubunit: %s", indent, ' ', subunit2str(subunit)); |
| |
| print_field("%*cOpcode: %s", indent, ' ', opcode2str(opcode)); |
| |
| /* Skip non-panel subunit packets */ |
| if (subunit != 0x09) { |
| packet_hexdump(frame->data, frame->size); |
| return true; |
| } |
| |
| /* Not implemented should not contain any operand */ |
| if (ctype == 0x8) { |
| packet_hexdump(frame->data, frame->size); |
| return true; |
| } |
| |
| switch (opcode) { |
| case 0x7c: |
| return avrcp_passthrough_packet(avctp_frame, 10); |
| case 0x00: |
| if (!l2cap_frame_get_u8(frame, &company[0]) || |
| !l2cap_frame_get_u8(frame, &company[1]) || |
| !l2cap_frame_get_u8(frame, &company[2])) |
| return false; |
| |
| print_field("%*cCompany ID: 0x%02x%02x%02x", indent, ' ', |
| company[0], company[1], company[2]); |
| |
| return avrcp_pdu_packet(avctp_frame, ctype, 10); |
| default: |
| packet_hexdump(frame->data, frame->size); |
| return true; |
| } |
| } |
| |
| static const char *dir2str(uint8_t dir) |
| { |
| switch (dir) { |
| case 0x00: |
| return "Folder Up"; |
| case 0x01: |
| return "Folder Down"; |
| } |
| |
| return "Reserved"; |
| } |
| |
| static bool avrcp_change_path(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint64_t uid; |
| uint32_t items; |
| uint16_t uidcounter; |
| uint8_t dir, status, indent = 2; |
| |
| if (avctp_frame->hdr & 0x02) |
| goto response; |
| |
| if (frame->size < 11) { |
| print_field("%*cPDU Malformed", indent, ' '); |
| packet_hexdump(frame->data, frame->size); |
| return false; |
| } |
| |
| if (!l2cap_frame_get_be16(frame, &uidcounter)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", indent, ' ', |
| uidcounter, uidcounter); |
| |
| if (!l2cap_frame_get_u8(frame, &dir)) |
| return false; |
| |
| print_field("%*cDirection: 0x%02x (%s)", indent, ' ', |
| dir, dir2str(dir)); |
| |
| if (!l2cap_frame_get_be64(frame, &uid)) |
| return false; |
| |
| print_field("%*cFolderUID: 0x%16" PRIx64 " (%" PRIu64 ")", indent, ' ', |
| uid, uid); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", indent, ' ', |
| status, error2str(status)); |
| |
| if (frame->size == 1) |
| return false; |
| |
| if (!l2cap_frame_get_be32(frame, &items)) |
| return false; |
| |
| print_field("%*cNumber of Items: 0x%04x (%u)", indent, ' ', |
| items, items); |
| |
| return true; |
| } |
| |
| |
| static struct { |
| const char *str; |
| bool reserved; |
| } features_table[] = { |
| /* Ignore passthrough bits */ |
| [58] = { "Advanced Control Player" }, |
| [59] = { "Browsing" }, |
| [60] = { "Searching" }, |
| [61] = { "AddToNowPlaying" }, |
| [62] = { "Unique UIDs" }, |
| [63] = { "OnlyBrowsableWhenAddressed" }, |
| [64] = { "OnlySearchableWhenAddressed" }, |
| [65] = { "NowPlaying" }, |
| [66] = { "UIDPersistency" }, |
| /* 67-127 reserved */ |
| [67 ... 127] = { .reserved = true }, |
| }; |
| |
| static void print_features(uint8_t features[16], uint8_t indent) |
| { |
| int i; |
| |
| for (i = 0; i < 127; i++) { |
| if (!(features[i / 8] & (1 << (i % 8)))) |
| continue; |
| |
| if (features_table[i].reserved) { |
| print_text(COLOR_WHITE_BG, "Unknown bit %u", i); |
| continue; |
| } |
| |
| if (!features_table[i].str) |
| continue; |
| |
| print_field("%*c%s", indent, ' ', features_table[i].str); |
| } |
| } |
| |
| static bool avrcp_media_player_item(struct avctp_frame *avctp_frame, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint16_t id, charset, namelen; |
| uint8_t type, status, i; |
| uint32_t subtype; |
| uint8_t features[16]; |
| |
| if (!l2cap_frame_get_be16(frame, &id)) |
| return false; |
| |
| print_field("%*cPlayerID: 0x%04x (%u)", indent, ' ', id, id); |
| |
| if (!l2cap_frame_get_u8(frame, &type)) |
| return false; |
| |
| print_field("%*cPlayerType: 0x%04x (%s)", indent, ' ', |
| type, playertype2str(type)); |
| |
| if (!l2cap_frame_get_be32(frame, &subtype)) |
| return false; |
| |
| print_field("%*cPlayerSubType: 0x%08x (%s)", indent, ' ', |
| subtype, playersubtype2str(subtype)); |
| |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cPlayStatus: 0x%02x (%s)", indent, ' ', |
| status, playstatus2str(status)); |
| |
| printf("%*cFeatures: 0x", indent+8, ' '); |
| |
| for (i = 0; i < 16; i++) { |
| if (!l2cap_frame_get_u8(frame, &features[i])) |
| return false; |
| |
| printf("%02x", features[i]); |
| } |
| |
| printf("\n"); |
| |
| print_features(features, indent + 2); |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", indent, ' ', |
| charset, charset2str(charset)); |
| |
| if (!l2cap_frame_get_be16(frame, &namelen)) |
| return false; |
| |
| print_field("%*cNameLength: 0x%04x (%u)", indent, ' ', |
| namelen, namelen); |
| |
| printf("%*cName: ", indent+8, ' '); |
| for (; namelen > 0; namelen--) { |
| uint8_t c; |
| |
| if (!l2cap_frame_get_u8(frame, &c)) |
| return false; |
| printf("%1c", isprint(c) ? c : '.'); |
| } |
| printf("\n"); |
| |
| return true; |
| } |
| |
| static bool avrcp_folder_item(struct avctp_frame *avctp_frame, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t type, playable; |
| uint16_t charset, namelen; |
| uint64_t uid; |
| |
| if (frame->size < 14) { |
| printf("PDU Malformed\n"); |
| return false; |
| } |
| |
| if (!l2cap_frame_get_be64(frame, &uid)) |
| return false; |
| |
| print_field("%*cFolderUID: 0x%16" PRIx64 " (%" PRIu64 ")", indent, ' ', |
| uid, uid); |
| |
| if (!l2cap_frame_get_u8(frame, &type)) |
| return false; |
| |
| print_field("%*cFolderType: 0x%02x (%s)", indent, ' ', |
| type, foldertype2str(type)); |
| |
| if (!l2cap_frame_get_u8(frame, &playable)) |
| return false; |
| |
| print_field("%*cIsPlayable: 0x%02x (%s)", indent, ' ', playable, |
| playable & 0x01 ? "True" : "False"); |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", indent, ' ', |
| charset, charset2str(charset)); |
| |
| if (!l2cap_frame_get_be16(frame, &namelen)) |
| return false; |
| |
| print_field("%*cNameLength: 0x%04x (%u)", indent, ' ', |
| namelen, namelen); |
| |
| print_field("%*cName: ", indent, ' '); |
| for (; namelen > 0; namelen--) { |
| uint8_t c; |
| if (!l2cap_frame_get_u8(frame, &c)) |
| return false; |
| |
| printf("%1c", isprint(c) ? c : '.'); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_attribute_entry_list(struct avctp_frame *avctp_frame, |
| uint8_t indent, uint8_t count) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| |
| for (; count > 0; count--) { |
| uint32_t attr; |
| uint16_t charset; |
| uint8_t len; |
| |
| if (!l2cap_frame_get_be32(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%08x (%s)", indent, ' ', |
| attr, mediattr2str(attr)); |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", indent, ' ', |
| charset, charset2str(charset)); |
| |
| if (!l2cap_frame_get_u8(frame, &len)) |
| return false; |
| |
| print_field("%*cAttributeLength: 0x%02x (%u)", indent, ' ', |
| len, len); |
| |
| print_field("%*cAttributeValue: ", indent, ' '); |
| for (; len > 0; len--) { |
| uint8_t c; |
| |
| if (!l2cap_frame_get_u8(frame, &c)) |
| return false; |
| |
| printf("%1c", isprint(c) ? c : '.'); |
| } |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_media_element_item(struct avctp_frame *avctp_frame, |
| uint8_t indent) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint64_t uid; |
| uint16_t charset, namelen; |
| uint8_t type, count; |
| |
| if (!l2cap_frame_get_be64(frame, &uid)) |
| return false; |
| |
| print_field("%*cElementUID: 0x%16" PRIx64 " (%" PRIu64 ")", |
| indent, ' ', uid, uid); |
| |
| if (!l2cap_frame_get_u8(frame, &type)) |
| return false; |
| |
| print_field("%*cElementType: 0x%02x (%s)", indent, ' ', |
| type, elementtype2str(type)); |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", indent, ' ', |
| charset, charset2str(charset)); |
| |
| if (!l2cap_frame_get_be16(frame, &namelen)) |
| return false; |
| |
| print_field("%*cNameLength: 0x%04x (%u)", indent, ' ', |
| namelen, namelen); |
| |
| print_field("%*cName: ", indent, ' '); |
| for (; namelen > 0; namelen--) { |
| uint8_t c; |
| if (!l2cap_frame_get_u8(frame, &c)) |
| return false; |
| |
| printf("%1c", isprint(c) ? c : '.'); |
| } |
| |
| if (!l2cap_frame_get_u8(frame, &count)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x (%u)", indent, ' ', |
| count, count); |
| |
| if (!avrcp_attribute_entry_list(avctp_frame, indent, count)) |
| return false; |
| |
| return true; |
| } |
| |
| static bool avrcp_general_reject(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t status, indent = 2; |
| |
| if (avctp_frame->hdr & 0x02) |
| goto response; |
| |
| print_field("%*cPDU Malformed", indent, ' '); |
| packet_hexdump(frame->data, frame->size); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", indent, ' ', |
| status, error2str(status)); |
| |
| return true; |
| } |
| |
| static bool avrcp_get_total_number_of_items(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint32_t num_of_items; |
| uint16_t uidcounter; |
| uint8_t scope, status, indent = 2; |
| |
| if (avctp_frame->hdr & 0x02) |
| goto response; |
| |
| if (frame->size < 4) { |
| printf("PDU Malformed\n"); |
| packet_hexdump(frame->data, frame->size); |
| return false; |
| } |
| |
| if (!l2cap_frame_get_u8(frame, &scope)) |
| return false; |
| |
| print_field("%*cScope: 0x%02x (%s)", (indent - 8), ' ', |
| scope, scope2str(scope)); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", indent, ' ', |
| status, error2str(status)); |
| |
| if (frame->size == 1) |
| return false; |
| |
| if (!l2cap_frame_get_be16(frame, &uidcounter)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", indent, ' ', |
| uidcounter, uidcounter); |
| |
| if (!l2cap_frame_get_be32(frame, &num_of_items)) |
| return false; |
| |
| print_field("%*cNumber of Items: 0x%04x (%u)", indent, ' ', |
| num_of_items, num_of_items); |
| |
| return true; |
| } |
| |
| static bool avrcp_search_item(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint32_t items; |
| uint16_t charset, namelen, uidcounter; |
| uint8_t status, indent = 2; |
| |
| if (avctp_frame->hdr & 0x02) |
| goto response; |
| |
| if (frame->size < 4) { |
| printf("PDU Malformed\n"); |
| packet_hexdump(frame->data, frame->size); |
| return false; |
| } |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", indent, ' ', |
| charset, charset2str(charset)); |
| |
| if (!l2cap_frame_get_be16(frame, &namelen)) |
| return false; |
| |
| print_field("%*cLength: 0x%04x (%u)", indent, ' ', namelen, namelen); |
| |
| printf("%*cString: ", indent+8, ' '); |
| for (; namelen > 0; namelen--) { |
| uint8_t c; |
| |
| if (!l2cap_frame_get_u8(frame, &c)) |
| return false; |
| |
| printf("%1c", isprint(c) ? c : '.'); |
| } |
| |
| printf("\n"); |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", indent, ' ', |
| status, error2str(status)); |
| |
| if (frame->size == 1) |
| return false; |
| |
| if (!l2cap_frame_get_be16(frame, &uidcounter)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", indent, ' ', |
| uidcounter, uidcounter); |
| |
| if (!l2cap_frame_get_be32(frame, &items)) |
| return false; |
| |
| print_field("%*cNumber of Items: 0x%04x (%u)", indent, ' ', |
| items, items); |
| |
| return true; |
| } |
| |
| static bool avrcp_get_item_attributes(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint64_t uid; |
| uint16_t uidcounter; |
| uint8_t scope, count, status, indent = 2; |
| |
| if (avctp_frame->hdr & 0x02) |
| goto response; |
| |
| if (frame->size < 12) { |
| print_field("%*cPDU Malformed", indent, ' '); |
| packet_hexdump(frame->data, frame->size); |
| return false; |
| } |
| |
| if (!l2cap_frame_get_u8(frame, &scope)) |
| return false; |
| |
| print_field("%*cScope: 0x%02x (%s)", indent, ' ', |
| scope, scope2str(scope)); |
| |
| if (!l2cap_frame_get_be64(frame, &uid)) |
| return false; |
| |
| print_field("%*cUID: 0x%016" PRIx64 " (%" PRIu64 ")", indent, |
| ' ', uid, uid); |
| |
| if (!l2cap_frame_get_be16(frame, &uidcounter)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", indent, ' ', |
| uidcounter, uidcounter); |
| |
| if (!l2cap_frame_get_u8(frame, &count)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x (%u)", indent, ' ', |
| count, count); |
| |
| for (; count > 0; count--) { |
| uint32_t attr; |
| |
| if (!l2cap_frame_get_be32(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%08x (%s)", indent, ' ', |
| attr, mediattr2str(attr)); |
| } |
| |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", indent, ' ', |
| status, error2str(status)); |
| |
| if (frame->size == 1) |
| return false; |
| |
| if (!l2cap_frame_get_u8(frame, &count)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x (%u)", indent, ' ', |
| count, count); |
| |
| if (!avrcp_attribute_entry_list(avctp_frame, indent, count)) |
| return false; |
| |
| return true; |
| } |
| |
| static bool avrcp_get_folder_items(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint8_t scope, count, status, indent = 2; |
| uint32_t start, end; |
| uint16_t uid, num; |
| |
| if (avctp_frame->hdr & 0x02) |
| goto response; |
| |
| if (!l2cap_frame_get_u8(frame, &scope)) |
| return false; |
| |
| print_field("%*cScope: 0x%02x (%s)", indent, ' ', |
| scope, scope2str(scope)); |
| |
| if (!l2cap_frame_get_be32(frame, &start)) |
| return false; |
| |
| print_field("%*cStartItem: 0x%08x (%u)", indent, ' ', start, start); |
| |
| if (!l2cap_frame_get_be32(frame, &end)) |
| return false; |
| |
| print_field("%*cEndItem: 0x%08x (%u)", indent, ' ', end, end); |
| |
| if (!l2cap_frame_get_u8(frame, &count)) |
| return false; |
| |
| print_field("%*cAttributeCount: 0x%02x (%u)", indent, ' ', |
| count, count); |
| |
| for (; count > 0; count--) { |
| uint16_t attr; |
| |
| if (!l2cap_frame_get_be16(frame, &attr)) |
| return false; |
| |
| print_field("%*cAttributeID: 0x%08x (%s)", indent, ' ', |
| attr, mediattr2str(attr)); |
| } |
| |
| return false; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", indent, ' ', |
| status, error2str(status)); |
| |
| if (!l2cap_frame_get_be16(frame, &uid)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", indent, ' ', uid, uid); |
| |
| if (!l2cap_frame_get_be16(frame, &num)) |
| return false; |
| |
| print_field("%*cNumOfItems: 0x%04x (%u)", indent, ' ', num, num); |
| |
| for (; num > 0; num--) { |
| uint8_t type; |
| uint16_t len; |
| |
| if (!l2cap_frame_get_u8(frame, &type)) |
| return false; |
| |
| if (!l2cap_frame_get_be16(frame, &len)) |
| return false; |
| |
| print_field("%*cItem: 0x%02x (%s) ", indent, ' ', |
| type, type2str(type)); |
| print_field("%*cLength: 0x%04x (%u)", indent, ' ', len, len); |
| |
| switch (type) { |
| case AVRCP_MEDIA_PLAYER_ITEM_TYPE: |
| avrcp_media_player_item(avctp_frame, indent); |
| break; |
| case AVRCP_FOLDER_ITEM_TYPE: |
| avrcp_folder_item(avctp_frame, indent); |
| break; |
| case AVRCP_MEDIA_ELEMENT_ITEM_TYPE: |
| avrcp_media_element_item(avctp_frame, indent); |
| break; |
| default: |
| print_field("%*cUnknown Media Item type", indent, ' '); |
| packet_hexdump(frame->data, frame->size); |
| break; |
| } |
| } |
| return true; |
| } |
| |
| static bool avrcp_set_browsed_player(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint32_t items; |
| uint16_t id, uids, charset; |
| uint8_t status, folders, indent = 2; |
| |
| if (avctp_frame->hdr & 0x02) |
| goto response; |
| |
| if (!l2cap_frame_get_be16(frame, &id)) |
| return false; |
| |
| print_field("%*cPlayerID: 0x%04x (%u)", indent, ' ', id, id); |
| return true; |
| |
| response: |
| if (!l2cap_frame_get_u8(frame, &status)) |
| return false; |
| |
| print_field("%*cStatus: 0x%02x (%s)", indent, ' ', status, |
| error2str(status)); |
| |
| if (!l2cap_frame_get_be16(frame, &uids)) |
| return false; |
| |
| print_field("%*cUIDCounter: 0x%04x (%u)", indent, ' ', uids, uids); |
| |
| if (!l2cap_frame_get_be32(frame, &items)) |
| return false; |
| |
| print_field("%*cNumber of Items: 0x%08x (%u)", indent, ' ', |
| items, items); |
| |
| if (!l2cap_frame_get_be16(frame, &charset)) |
| return false; |
| |
| print_field("%*cCharsetID: 0x%04x (%s)", indent, ' ', charset, |
| charset2str(charset)); |
| |
| if (!l2cap_frame_get_u8(frame, &folders)) |
| return false; |
| |
| print_field("%*cFolder Depth: 0x%02x (%u)", indent, ' ', folders, |
| folders); |
| |
| for (; folders > 0; folders--) { |
| uint8_t len; |
| |
| if (!l2cap_frame_get_u8(frame, &len)) |
| return false; |
| |
| printf("Folder: "); |
| for (; len > 0; len--) { |
| uint8_t c; |
| |
| if (!l2cap_frame_get_u8(frame, &c)) |
| return false; |
| |
| printf("%1c", isprint(c) ? c : '.'); |
| } |
| printf("\n"); |
| } |
| |
| return true; |
| } |
| |
| static bool avrcp_browsing_packet(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| uint16_t len; |
| uint8_t pduid; |
| |
| if (!l2cap_frame_get_u8(frame, &pduid)) |
| return false; |
| |
| if (!l2cap_frame_get_be16(frame, &len)) |
| return false; |
| |
| print_field("AVRCP: %s: len 0x%04x", pdu2str(pduid), len); |
| |
| switch (pduid) { |
| case AVRCP_SET_BROWSED_PLAYER: |
| avrcp_set_browsed_player(avctp_frame); |
| break; |
| case AVRCP_GET_FOLDER_ITEMS: |
| avrcp_get_folder_items(avctp_frame); |
| break; |
| case AVRCP_CHANGE_PATH: |
| avrcp_change_path(avctp_frame); |
| break; |
| case AVRCP_GET_ITEM_ATTRIBUTES: |
| avrcp_get_item_attributes(avctp_frame); |
| break; |
| case AVRCP_GET_TOTAL_NUMBER_OF_ITEMS: |
| avrcp_get_total_number_of_items(avctp_frame); |
| break; |
| case AVRCP_SEARCH: |
| avrcp_search_item(avctp_frame); |
| break; |
| case AVRCP_GENERAL_REJECT: |
| avrcp_general_reject(avctp_frame); |
| break; |
| default: |
| packet_hexdump(frame->data, frame->size); |
| } |
| |
| return true; |
| } |
| |
| static void avrcp_packet(struct avctp_frame *avctp_frame) |
| { |
| struct l2cap_frame *frame = &avctp_frame->l2cap_frame; |
| bool ret; |
| |
| switch (frame->psm) { |
| case 0x17: |
| ret = avrcp_control_packet(avctp_frame); |
| break; |
| case 0x1B: |
| ret = avrcp_browsing_packet(avctp_frame); |
| break; |
| default: |
| packet_hexdump(frame->data, frame->size); |
| return; |
| } |
| |
| if (!ret) { |
| print_text(COLOR_ERROR, "PDU malformed"); |
| packet_hexdump(frame->data, frame->size); |
| } |
| } |
| |
| void avctp_packet(const struct l2cap_frame *frame) |
| { |
| struct l2cap_frame *l2cap_frame; |
| struct avctp_frame avctp_frame; |
| const char *pdu_color; |
| |
| l2cap_frame_pull(&avctp_frame.l2cap_frame, frame, 0); |
| |
| l2cap_frame = &avctp_frame.l2cap_frame; |
| |
| if (!l2cap_frame_get_u8(l2cap_frame, &avctp_frame.hdr) || |
| !l2cap_frame_get_be16(l2cap_frame, &avctp_frame.pid)) { |
| print_text(COLOR_ERROR, "frame too short"); |
| packet_hexdump(frame->data, frame->size); |
| return; |
| } |
| |
| if (frame->in) |
| pdu_color = COLOR_MAGENTA; |
| else |
| pdu_color = COLOR_BLUE; |
| |
| print_indent(6, pdu_color, "AVCTP", "", COLOR_OFF, |
| " %s: %s: type 0x%02x label %d PID 0x%04x", |
| frame->psm == 23 ? "Control" : "Browsing", |
| avctp_frame.hdr & 0x02 ? "Response" : "Command", |
| avctp_frame.hdr & 0x0c, avctp_frame.hdr >> 4, |
| avctp_frame.pid); |
| |
| if (avctp_frame.pid == 0x110e || avctp_frame.pid == 0x110c) |
| avrcp_packet(&avctp_frame); |
| else |
| packet_hexdump(frame->data, frame->size); |
| } |