From 8de07fe8fac23d508ae64dfd6ffb332f568f4e45 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 6 Jun 2024 04:52:00 +0000 Subject: [PATCH] daniel/bluetooth-avrc (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Have a squizzy and lemme know if any issues @cooljqln 🐝 Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/80 Co-authored-by: ailurux Co-committed-by: ailurux --- src/drivers/bluetooth.cpp | 143 ++++++++++++++++-- src/drivers/include/drivers/bluetooth.hpp | 6 + .../include/drivers/bluetooth_types.hpp | 19 ++- src/tangara/audio/audio_events.hpp | 6 + src/tangara/audio/audio_fsm.cpp | 34 +++-- src/tangara/ui/ui_fsm.cpp | 70 ++++++--- src/tangara/ui/ui_fsm.hpp | 1 + 7 files changed, 239 insertions(+), 40 deletions(-) diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 8cf2b477..412cba1f 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -73,6 +73,16 @@ auto avrcp_cb(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t* param) } } +auto avrcp_tg_cb(esp_avrc_tg_cb_event_t event, esp_avrc_tg_cb_param_t* param) + -> void { + esp_avrc_tg_cb_param_t copy = *param; + sBgWorker->Dispatch([=]() { + auto lock = bluetooth::BluetoothState::lock(); + tinyfsm::FsmList::dispatch( + bluetooth::events::internal::Avrctg{.type = event, .param = copy}); + }); +} + auto a2dp_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t* param) -> void { esp_a2d_cb_param_t copy = *param; sBgWorker->Dispatch([=]() { @@ -229,7 +239,6 @@ auto Scanner::ScanOnce() -> void { } auto Scanner::StopScanning() -> void { - ESP_LOGI(kTag, "stopping scan"); enabled_ = false; } @@ -263,7 +272,7 @@ auto Scanner::HandleGapEvent(const events::internal::Gap& ev) -> void { break; case ESP_BT_GAP_MODE_CHG_EVT: // todo: mode change. is this important? - ESP_LOGI(kTag, "GAP mode changed"); + ESP_LOGI(kTag, "GAP mode changed %d", ev.param.mode_chg.mode); break; default: ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type); @@ -390,7 +399,7 @@ auto BluetoothState::react(const events::DeviceDiscovered& ev) -> void { } if (sEventHandler_ && !already_known) { - std::invoke(sEventHandler_, Event::kKnownDevicesChanged); + std::invoke(sEventHandler_, SimpleEvent::kKnownDevicesChanged); } if (is_preferred && sPreferredDevice_) { @@ -439,6 +448,7 @@ void Disabled::entry() { esp_a2d_source_deinit(); esp_avrc_ct_deinit(); + esp_avrc_tg_deinit(); esp_bluedroid_disable(); esp_bluedroid_deinit(); esp_bt_controller_disable(); @@ -480,11 +490,50 @@ void Disabled::react(const events::Enable&) { // Initialise GAP. This controls advertising our device, and scanning for // other devices. - esp_bt_gap_register_callback(gap_cb); + err = esp_bt_gap_register_callback(gap_cb); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error initialising GAP: %s %d", esp_err_to_name(err), err); + } // Initialise AVRCP. This handles playback controls; play/pause/volume/etc. - esp_avrc_ct_register_callback(avrcp_cb); - esp_avrc_ct_init(); + err = esp_avrc_ct_init(); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error initialising AVRC: %s %d", esp_err_to_name(err), err); + } + err = esp_avrc_ct_register_callback(avrcp_cb); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error registering AVRC: %s %d", esp_err_to_name(err), err); + } + + // AVRCP Target + err = esp_avrc_tg_init(); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error during target init: %s %d", esp_err_to_name(err), err); + } + err = esp_avrc_tg_register_callback(avrcp_tg_cb); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error registering AVRC tg callback: %s %d", esp_err_to_name(err), err); + } + + // Set the supported passthrough commands on the tg + esp_avrc_psth_bit_mask_t psth; + // Retry this until successful + // this indicates that the bt stack is ready + do { + // Sleep for a bit + vTaskDelay(pdMS_TO_TICKS(10)); + err = esp_avrc_tg_get_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_ALLOWED_CMD, &psth); + } while (err != ESP_OK); + + err = esp_avrc_tg_set_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_SUPPORTED_CMD, &psth); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); + } + esp_avrc_rn_evt_cap_mask_t evt_set = {0}; + esp_avrc_rn_evt_bit_mask_operation(ESP_AVRC_BIT_MASK_OP_SET, &evt_set, + ESP_AVRC_RN_VOLUME_CHANGE); + assert(esp_avrc_tg_set_rn_evt_cap(&evt_set) == ESP_OK); + // Initialise A2DP. This handles streaming audio. Currently ESP-IDF's SBC // encoder only supports 2 channels of interleaved 16 bit samples, at @@ -552,7 +601,7 @@ void Connecting::entry() { sScanner_->StopScanning(); if (sEventHandler_) { - std::invoke(sEventHandler_, Event::kConnectionStateChanged); + std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); } } @@ -560,7 +609,7 @@ void Connecting::exit() { xTimerDelete(sTimeoutTimer, portMAX_DELAY); if (sEventHandler_) { - std::invoke(sEventHandler_, Event::kConnectionStateChanged); + std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); } } @@ -726,7 +775,10 @@ void Connected::react(events::internal::Avrc ev) { switch (ev.type) { case ESP_AVRC_CT_CONNECTION_STATE_EVT: if (ev.param.conn_stat.connected) { - // TODO: tell the target about our capabilities + auto err = esp_avrc_ct_send_register_notification_cmd(4, ESP_AVRC_RN_VOLUME_CHANGE, 0); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); + } } // Don't worry about disconnect events; if there's a serious problem // then the entire bluetooth connection will drop out, which is handled @@ -735,9 +787,80 @@ void Connected::react(events::internal::Avrc ev) { case ESP_AVRC_CT_REMOTE_FEATURES_EVT: // The remote device is telling us about its capabilities! We don't // currently care about any of them. + ESP_LOGI(kTag, "Recieved capabilitites: %lu", ev.param.rmt_feats.feat_mask); break; + case ESP_AVRC_CT_CHANGE_NOTIFY_EVT: + if (ev.param.change_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) { + if (sEventHandler_) { + std::invoke(sEventHandler_, bluetooth::RemoteVolumeChanged{.new_vol = ev.param.change_ntf.event_parameter.volume}); + } + // Resubscribe to volume facts + auto err = esp_avrc_ct_send_register_notification_cmd(4, ESP_AVRC_RN_VOLUME_CHANGE, 0); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); + } + } + break; + default: + ESP_LOGI(kTag, "unhandled AVRC event: %u", ev.type); + } +} + +void Connected::react(const events::internal::Avrctg ev) { + switch (ev.type) { + case ESP_AVRC_TG_CONNECTION_STATE_EVT: + ESP_LOGI(kTag, "Got connection event. Connected: %s", ev.param.conn_stat.connected ? "true" : "false"); + if (ev.param.conn_stat.connected) { + } + break; + case ESP_AVRC_TG_REMOTE_FEATURES_EVT: + ESP_LOGI(kTag, "Got remote features feat flag %d", ev.param.rmt_feats.ct_feat_flag); + ESP_LOGI(kTag, "Got remote features feat mask %lu", ev.param.rmt_feats.feat_mask); + break; + case ESP_AVRC_TG_PASSTHROUGH_CMD_EVT: + ESP_LOGI(kTag, "Got passthrough event keycode: %x, %d", ev.param.psth_cmd.key_code, ev.param.psth_cmd.key_state); + if (ev.param.psth_cmd.key_state == 1 && sEventHandler_) { + switch (ev.param.psth_cmd.key_code) { + case ESP_AVRC_PT_CMD_PLAY: + std::invoke(sEventHandler_, bluetooth::SimpleEvent::kPlayPause); + break; + case ESP_AVRC_PT_CMD_PAUSE: + std::invoke(sEventHandler_, bluetooth::SimpleEvent::kPlayPause); + break; + case ESP_AVRC_PT_CMD_STOP: + std::invoke(sEventHandler_, bluetooth::SimpleEvent::kStop); + break; + case ESP_AVRC_PT_CMD_MUTE: + std::invoke(sEventHandler_, bluetooth::SimpleEvent::kMute); + break; + case ESP_AVRC_PT_CMD_FORWARD: + std::invoke(sEventHandler_, bluetooth::SimpleEvent::kForward); + break; + case ESP_AVRC_PT_CMD_BACKWARD: + std::invoke(sEventHandler_, bluetooth::SimpleEvent::kBackward); + break; + default: + ESP_LOGI(kTag, "Unhandled passthrough cmd. Key code: %d", ev.param.psth_cmd.key_code); + } + } + break; + case ESP_AVRC_TG_REGISTER_NOTIFICATION_EVT: { + if (ev.param.reg_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) { + // TODO: actually do this lol + esp_avrc_rn_param_t rn_param; + rn_param.volume = 64; + auto err = esp_avrc_tg_send_rn_rsp(ESP_AVRC_RN_VOLUME_CHANGE, + ESP_AVRC_RN_RSP_INTERIM, &rn_param); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); + } + } else { + ESP_LOGW(kTag, "unhandled AVRC TG Register Notification event: %u", ev.param.reg_ntf.event_id); + } + break; + } default: - ESP_LOGW(kTag, "unhandled AVRC event: %u", ev.type); + ESP_LOGW(kTag, "unhandled AVRC TG event: %u", ev.type); } } diff --git a/src/drivers/include/drivers/bluetooth.hpp b/src/drivers/include/drivers/bluetooth.hpp index 6deeca05..94a85263 100644 --- a/src/drivers/include/drivers/bluetooth.hpp +++ b/src/drivers/include/drivers/bluetooth.hpp @@ -75,6 +75,10 @@ struct Avrc : public tinyfsm::Event { esp_avrc_ct_cb_event_t type; esp_avrc_ct_cb_param_t param; }; +struct Avrctg : public tinyfsm::Event { + esp_avrc_tg_cb_event_t type; + esp_avrc_tg_cb_param_t param; +}; } // namespace internal } // namespace events @@ -135,6 +139,7 @@ class BluetoothState : public tinyfsm::Fsm { virtual void react(events::internal::Gap ev) = 0; virtual void react(events::internal::A2dp ev){}; virtual void react(events::internal::Avrc ev){}; + virtual void react(events::internal::Avrctg ev){}; protected: static NvsStorage* sStorage_; @@ -206,6 +211,7 @@ class Connected : public BluetoothState { void react(events::internal::Gap ev) override; void react(events::internal::A2dp ev) override; void react(events::internal::Avrc ev) override; + void react(const events::internal::Avrctg ev) override; using BluetoothState::react; diff --git a/src/drivers/include/drivers/bluetooth_types.hpp b/src/drivers/include/drivers/bluetooth_types.hpp index 7cfb236f..d2e55ee5 100644 --- a/src/drivers/include/drivers/bluetooth_types.hpp +++ b/src/drivers/include/drivers/bluetooth_types.hpp @@ -5,6 +5,7 @@ #include #include "memory_resource.hpp" +#include namespace drivers { namespace bluetooth { @@ -25,11 +26,27 @@ struct Device { int8_t signal_strength; }; -enum class Event { +enum class SimpleEvent { kKnownDevicesChanged, kConnectionStateChanged, kPreferredDeviceChanged, + // Passthrough events + kPlayPause, + kStop, + kMute, + kVolUp, + kVolDown, + kForward, + kBackward, + kRewind, + kFastForward, }; +struct RemoteVolumeChanged { + uint8_t new_vol; +}; + +using Event = std::variant; + } // namespace bluetooth } // namespace drivers diff --git a/src/tangara/audio/audio_events.hpp b/src/tangara/audio/audio_events.hpp index 55095d85..503664cc 100644 --- a/src/tangara/audio/audio_events.hpp +++ b/src/tangara/audio/audio_events.hpp @@ -116,6 +116,12 @@ struct SetVolumeBalance : tinyfsm::Event { int left_bias; }; +/* + Event emitted when the hardware volume for a connected Bluetooth device has changed. +*/ +struct RemoteVolumeChanged : tinyfsm::Event { + uint_fast8_t value; // 0..127 +}; struct VolumeChanged : tinyfsm::Event { uint_fast8_t percent; int db; diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index d6ddff8d..80611082 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -217,18 +217,32 @@ void AudioState::react(const internal::StreamEnded& ev) { } void AudioState::react(const system_fsm::BluetoothEvent& ev) { - if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { - return; + using drivers::bluetooth::SimpleEvent; + if (std::holds_alternative(ev.event)) { + auto simpleEvent = std::get(ev.event); + switch (simpleEvent) { + case SimpleEvent::kConnectionStateChanged: { + auto dev = sServices->bluetooth().ConnectedDevice(); + if (!dev) { + return; + } + sBtOutput->SetVolume(sServices->nvs().BluetoothVolume(dev->mac)); + events::Ui().Dispatch(VolumeChanged{ + .percent = sOutput->GetVolumePct(), + .db = sOutput->GetVolumeDb(), + }); + break; + } + default: + break; + } } - auto dev = sServices->bluetooth().ConnectedDevice(); - if (!dev) { - return; + if (std::holds_alternative(ev.event)) { + auto volume_chg = std::get(ev.event).new_vol; + events::Ui().Dispatch(RemoteVolumeChanged{ + .value = volume_chg + }); } - sBtOutput->SetVolume(sServices->nvs().BluetoothVolume(dev->mac)); - events::Ui().Dispatch(VolumeChanged{ - .percent = sOutput->GetVolumePct(), - .db = sOutput->GetVolumeDb(), - }); } void AudioState::react(const StepUpVolume& ev) { diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 17d6c511..e367586b 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -387,6 +387,10 @@ void UiState::react(const audio::VolumeChanged& ev) { sVolumeCurrentDb.setDirect(static_cast(ev.db)); } +void UiState::react(const audio::RemoteVolumeChanged& ev) { + // TODO: Show dialog +} + void UiState::react(const audio::VolumeBalanceChanged& ev) { sVolumeLeftBias.setDirect(ev.left_bias); } @@ -396,27 +400,55 @@ void UiState::react(const audio::VolumeLimitChanged& ev) { } void UiState::react(const system_fsm::BluetoothEvent& ev) { + using drivers::bluetooth::SimpleEvent; auto bt = sServices->bluetooth(); auto dev = bt.ConnectedDevice(); - switch (ev.event) { - case drivers::bluetooth::Event::kKnownDevicesChanged: - sBluetoothDevices.setDirect(bt.KnownDevices()); - break; - case drivers::bluetooth::Event::kConnectionStateChanged: - sBluetoothConnected.setDirect(bt.IsConnected()); - if (dev) { - sBluetoothPairedDevice.setDirect(drivers::bluetooth::Device{ - .address = dev->mac, - .name = {dev->name.data(), dev->name.size()}, - .class_of_device = 0, - .signal_strength = 0, - }); - } else { - sBluetoothPairedDevice.setDirect(std::monostate{}); - } - break; - case drivers::bluetooth::Event::kPreferredDeviceChanged: - break; + if (std::holds_alternative(ev.event)) { + switch (std::get(ev.event)) { + case SimpleEvent::kPlayPause: + events::Audio().Dispatch(audio::TogglePlayPause{}); + break; + case SimpleEvent::kStop: + events::Audio().Dispatch(audio::TogglePlayPause{.set_to = false}); + break; + case SimpleEvent::kMute: + break; + case SimpleEvent::kVolUp: + break; + case SimpleEvent::kVolDown: + break; + case SimpleEvent::kForward: + break; + case SimpleEvent::kBackward: + break; + case SimpleEvent::kRewind: + break; + case SimpleEvent::kFastForward: + break; + case SimpleEvent::kKnownDevicesChanged: + sBluetoothDevices.setDirect(bt.KnownDevices()); + break; + case SimpleEvent::kConnectionStateChanged: + sBluetoothConnected.setDirect(bt.IsConnected()); + if (dev) { + sBluetoothPairedDevice.setDirect(drivers::bluetooth::Device{ + .address = dev->mac, + .name = {dev->name.data(), dev->name.size()}, + .class_of_device = 0, + .signal_strength = 0, + }); + } else { + sBluetoothPairedDevice.setDirect(std::monostate{}); + } + break; + case SimpleEvent::kPreferredDeviceChanged: + break; + default: + break; + } + } else if (std::holds_alternative(ev.event)) { + // Todo: Do something with this (ie, bt volume alert) + ESP_LOGI(kTag, "Recieved volume changed event with new volume: %d", std::get(ev.event).new_vol); } } diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index af8d75fb..aea1eb36 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -67,6 +67,7 @@ class UiState : public tinyfsm::Fsm { void react(const audio::VolumeChanged&); void react(const audio::VolumeBalanceChanged&); void react(const audio::VolumeLimitChanged&); + void react(const audio::RemoteVolumeChanged& ev); void react(const system_fsm::KeyLockChanged&); void react(const system_fsm::SamdUsbStatusChanged&);