/* * Copyright 2021 HIMSA II K/S - www.himsa.com. * Represented by EHIMA - www.ehima.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "hardware/bt_has.h" #include "has_preset.h" #include "osi/include/alarm.h" namespace bluetooth::le_audio { namespace has { /* HAS control point Change Id */ enum class PresetCtpChangeId : uint8_t { PRESET_GENERIC_UPDATE = 0, PRESET_DELETED, PRESET_AVAILABLE, PRESET_UNAVAILABLE, /* NOTICE: Values below are for internal use only of this particular * implementation, and do not correspond to any bluetooth specification. */ CHANGE_ID_MAX_ = PRESET_UNAVAILABLE, }; std::ostream& operator<<(std::ostream& out, const PresetCtpChangeId value); /* HAS control point Opcodes */ enum class PresetCtpOpcode : uint8_t { READ_PRESETS = 1, READ_PRESET_RESPONSE, PRESET_CHANGED, WRITE_PRESET_NAME, SET_ACTIVE_PRESET, SET_NEXT_PRESET, SET_PREV_PRESET, SET_ACTIVE_PRESET_SYNC, SET_NEXT_PRESET_SYNC, SET_PREV_PRESET_SYNC, /* NOTICE: Values below are for internal use only of this particular * implementation, and do not correspond to any bluetooth specification. */ OP_MAX_ = SET_PREV_PRESET_SYNC, OP_NONE_ = OP_MAX_ + 1, }; std::ostream& operator<<(std::ostream& out, const PresetCtpOpcode value); static constexpr uint16_t PresetCtpOpcode2Bitmask(PresetCtpOpcode op) { return ((uint16_t)0b1 << static_cast>( op)); } /* Mandatory opcodes if control point characteristic exists */ static constexpr uint16_t kControlPointMandatoryOpcodesBitmask = PresetCtpOpcode2Bitmask(PresetCtpOpcode::READ_PRESETS) | PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_ACTIVE_PRESET) | PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_NEXT_PRESET) | PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_PREV_PRESET); /* Optional coordinated operation opcodes */ static constexpr uint16_t kControlPointSynchronizedOpcodesBitmask = PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC) | PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_NEXT_PRESET_SYNC) | PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_PREV_PRESET_SYNC); /* Represents HAS Control Point value notification */ struct HasCtpNtf { PresetCtpOpcode opcode; PresetCtpChangeId change_id; bool is_last; union { uint8_t index; uint8_t prev_index; }; std::optional preset; static std::optional FromCharacteristicValue(uint16_t len, const uint8_t* value); }; std::ostream& operator<<(std::ostream& out, const HasCtpNtf& value); /* Represents HAS Control Point operation request */ struct HasCtpOp { std::variant addr_or_group; PresetCtpOpcode opcode; uint8_t index; uint8_t num_of_indices; std::optional name; uint16_t op_id; HasCtpOp(std::variant addr_or_group_id, PresetCtpOpcode op, uint8_t index = bluetooth::has::kHasPresetIndexInvalid, uint8_t num_of_indices = 1, std::optional name = std::nullopt) : addr_or_group(addr_or_group_id), opcode(op), index(index), num_of_indices(num_of_indices), name(name) { /* Skip 0 on roll-over */ last_op_id_ += 1; if (last_op_id_ == 0) last_op_id_ = 1; op_id = last_op_id_; } std::vector ToCharacteristicValue(void) const; bool IsGroupRequest() const { return std::holds_alternative(addr_or_group); } int GetGroupId() const { return std::holds_alternative(addr_or_group) ? std::get(addr_or_group) : -1; } RawAddress GetDeviceAddr() const { return std::holds_alternative(addr_or_group) ? std::get(addr_or_group) : RawAddress::kEmpty; } bool IsSyncedOperation() const { return (opcode == PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC) || (opcode == PresetCtpOpcode::SET_NEXT_PRESET_SYNC) || (opcode == PresetCtpOpcode::SET_PREV_PRESET_SYNC); } private: /* It's fine for this to roll-over eventually */ static uint16_t last_op_id_; }; std::ostream& operator<<(std::ostream& out, const HasCtpOp& value); /* Used to track group operations. SetCompleted() allows to mark * a single device as operation-completed when notification is received. * When all the devices are SetComplete'd, timeout timer is being canceled and * a group operation can be considered completed (IsFullyCompleted() == true). * * NOTICE: A single callback and reference counter is being used for all the * coordinator instances, therefore creating more instances result * in timeout timer being rescheduled. User should remove all the * pending op. coordinators in the timer timeout callback. */ struct HasCtpGroupOpCoordinator { std::list devices; HasCtpOp operation; std::list preset_info_verification_list; static size_t ref_cnt; static alarm_t* operation_timeout_timer; static constexpr uint16_t kOperationTimeoutMs = 10000u; static alarm_callback_t cb; static void Initialize(alarm_callback_t c = nullptr) { operation_timeout_timer = nullptr; ref_cnt = 0; cb = c; } static void Cleanup() { if (operation_timeout_timer != nullptr) { if (alarm_is_scheduled(operation_timeout_timer)) { log::verbose("{}", ref_cnt); alarm_cancel(operation_timeout_timer); } alarm_free(operation_timeout_timer); operation_timeout_timer = nullptr; } ref_cnt = 0; } static bool IsFullyCompleted() { return ref_cnt == 0; } static bool IsPending() { return ref_cnt != 0; } HasCtpGroupOpCoordinator() = delete; HasCtpGroupOpCoordinator& operator=(const HasCtpGroupOpCoordinator&) = delete; /* NOTICE: It cannot be non-copyable if we want to put it into the std::map. * The default copy constructor and copy assignment operator would break the * reference counting, so we must increment ref_cnt for all the temporary * copies. */ HasCtpGroupOpCoordinator(const HasCtpGroupOpCoordinator& other) : devices(other.devices), operation(other.operation), preset_info_verification_list(other.preset_info_verification_list) { ref_cnt += other.devices.size(); } HasCtpGroupOpCoordinator(const std::vector& targets, HasCtpOp operation) : operation(operation) { log::assert_that(targets.size() != 0, "Empty device list error."); if (targets.size() != 1) { log::assert_that(operation.IsGroupRequest(), "Must be a group operation!"); log::assert_that(operation.GetGroupId() != -1, "Must set valid group_id!"); } devices = std::list(targets.cbegin(), targets.cend()); ref_cnt += devices.size(); if (operation_timeout_timer == nullptr) { operation_timeout_timer = alarm_new("GroupOpTimer"); } if (alarm_is_scheduled(operation_timeout_timer)) alarm_cancel(operation_timeout_timer); log::assert_that(cb != nullptr, "Timeout timer callback not set!"); alarm_set_on_mloop(operation_timeout_timer, kOperationTimeoutMs, cb, nullptr); } ~HasCtpGroupOpCoordinator() { /* Check if cleanup wasn't already called */ if (ref_cnt != 0) { ref_cnt -= devices.size(); if (ref_cnt == 0) { Cleanup(); } } } bool SetCompleted(RawAddress addr) { auto result = false; auto it = std::find(devices.begin(), devices.end(), addr); if (it != devices.end()) { devices.erase(it); --ref_cnt; result = true; } if (ref_cnt == 0) { alarm_cancel(operation_timeout_timer); alarm_free(operation_timeout_timer); operation_timeout_timer = nullptr; } return result; } }; } // namespace has } // namespace bluetooth::le_audio namespace fmt { template <> struct formatter : ostream_formatter {}; template <> struct formatter : ostream_formatter {}; } // namespace fmt