262 lines
8.3 KiB
C++
262 lines
8.3 KiB
C++
/*
|
|
* 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 <list>
|
|
#include <optional>
|
|
|
|
#include "hardware/bt_has.h"
|
|
#include "has_preset.h"
|
|
#include "osi/include/alarm.h"
|
|
|
|
namespace 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<std::underlying_type_t<PresetCtpOpcode>>(
|
|
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<HasPreset> preset;
|
|
|
|
static std::optional<HasCtpNtf> 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<RawAddress, int> addr_or_group;
|
|
PresetCtpOpcode opcode;
|
|
uint8_t index;
|
|
uint8_t num_of_indices;
|
|
std::optional<std::string> name;
|
|
uint16_t op_id;
|
|
|
|
HasCtpOp(std::variant<RawAddress, int> addr_or_group_id, PresetCtpOpcode op,
|
|
uint8_t index = bluetooth::has::kHasPresetIndexInvalid,
|
|
uint8_t num_of_indices = 1,
|
|
std::optional<std::string> 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<uint8_t> ToCharacteristicValue(void) const;
|
|
|
|
bool IsGroupRequest() const {
|
|
return std::holds_alternative<int>(addr_or_group);
|
|
}
|
|
|
|
int GetGroupId() const {
|
|
return std::holds_alternative<int>(addr_or_group)
|
|
? std::get<int>(addr_or_group)
|
|
: -1;
|
|
}
|
|
|
|
RawAddress GetDeviceAddr() const {
|
|
return std::holds_alternative<RawAddress>(addr_or_group)
|
|
? std::get<RawAddress>(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<RawAddress> devices;
|
|
HasCtpOp operation;
|
|
std::list<bluetooth::has::PresetInfo> 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)) {
|
|
DLOG(INFO) << __func__ << +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<RawAddress>& targets,
|
|
HasCtpOp operation)
|
|
: operation(operation) {
|
|
LOG_ASSERT(targets.size() != 0) << " Empty device list error.";
|
|
if (targets.size() != 1) {
|
|
LOG_ASSERT(operation.IsGroupRequest()) << " Must be a group operation!";
|
|
LOG_ASSERT(operation.GetGroupId() != -1) << " Must set valid group_id!";
|
|
}
|
|
|
|
devices = std::list<RawAddress>(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(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 le_audio
|