diff --git a/README.md b/README.md index 468d58b..7c18e42 100644 --- a/README.md +++ b/README.md @@ -31,18 +31,44 @@ C libraries of similar functionality. ### Device classes -* Human Interface Device Class (HID) specification version 1.11 (HID over I2C transport also supported) +#### HID - Human Interface Device Class + +HID specification version 1.11 is fully supported, with extensive report descriptor tooling +via [hid-rp][hid-rp] library. +HID has outgrown itself from a pure USB class to a transport-independent protocol, +and so this library also provides alternative transports for HID applications, +which interact with the same high-level application API. +The additional transport layers supported are: +* BLE (HID over GATT Protocol) +* I2C + +### Platforms + +* NXP MCUs supported via `kusb_mac` (see [c2usb/port/nxp](c2usb/port/nxp)) +* Zephyr RTOS supported via `udc_mac` (see [c2usb/port/zephyr](c2usb/port/zephyr)) * support the project to see more! ### Vendor extensions -* Microsoft OS descriptors version 2.0 -* Microsoft XBOX-360 controller interface +#### Microsoft OS descriptors -### Platforms +Microsoft OS descriptors version 2.0 is supported. +The main motivation to support this functionality is because +MS likes to make everybody else's life difficult. +In the case of USB, this means that in many cases the USB standardized device classes +don't get the correct OS driver assigned (even if it's available on the system, such as CDC-NCM on Windows 10), +or get a downgraded driver instead (such as HID gamepads getting DirectInput driver, except if manufactured by MS, see xinputhid.inf), +or no driver at all. +The only possible solution to deal with these is using [Windows Compatible IDs][WCID]. +This is stored in the USB device's descriptors, and tells Windows which driver to load for the given USB function. -* NXP MCUs supported via `kusb_mac` -* Zephyr OS supported via `udc_mac` -* support the project to see more! +#### Microsoft XBOX-360 controller interface + +Microsoft XBOX-360 gamepad controller interface is implemented to leverage XInput driver on Windows +for gamepad applications without any user step. Combining this with Microsoft OS descriptors +makes it possible to have a USB device that either presents an HID or an XInput gamepad interface +towards the host computer, depending on its OS. [project-structure]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1204r0.html +[hid-rp]: https://github.com/IntergatedCircuits/hid-rp +[WCID]: https://github.com/pbatard/libwdi/wiki/WCID-Devices diff --git a/c2usb/CMakeLists.txt b/c2usb/CMakeLists.txt index d66703c..e592271 100644 --- a/c2usb/CMakeLists.txt +++ b/c2usb/CMakeLists.txt @@ -12,10 +12,13 @@ set(C2USB_NXP_PORT_SOURCES set(C2USB_ZEPHYR_PORT_PUBLIC_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/port/zephyr/udc_mac.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/port/zephyr/bluetooth/gatt.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/port/zephyr/bluetooth/hid.hpp ) set(C2USB_ZEPHYR_PORT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/port/zephyr/udc_mac.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/port/zephyr/bluetooth/hid.cpp ) set(C2USB_PUBLIC_HEADERS @@ -50,6 +53,7 @@ set(C2USB_PUBLIC_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/c2usb.hpp ${CMAKE_CURRENT_SOURCE_DIR}/reference_array_view.hpp ${CMAKE_CURRENT_SOURCE_DIR}/single_elem_queue.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/uninit_store.hpp ${C2USB_NXP_PORT_PUBLIC_HEADERS} ${C2USB_ZEPHYR_PORT_PUBLIC_HEADERS} PARENT_SCOPE diff --git a/c2usb/port/zephyr/README.md b/c2usb/port/zephyr/README.md index ec31352..8504684 100644 --- a/c2usb/port/zephyr/README.md +++ b/c2usb/port/zephyr/README.md @@ -17,7 +17,7 @@ set(C2USB_PATH "c2usb") add_subdirectory(${C2USB_PATH}) # link c2usb to the abstract Zephyr interface to inherit the build flags -target_link_libraries(c2usb zephyr_interface) +target_link_libraries(c2usb PUBLIC zephyr_interface) # link the application to c2usb target_link_libraries(app PRIVATE @@ -39,7 +39,7 @@ CONFIG_UDC_DRIVER=y # RAM optimization: # the buffer pool size can be cut down, as it's only used for control transfers -# CONFIG_UDC_BUF_POOL_SIZE=256 +# CONFIG_UDC_BUF_POOL_SIZE=optimize based on your application (and check asserts) # CONFIG_UDC_BUF_COUNT=3 + maximal used endpoint count in a configuration ``` diff --git a/c2usb/port/zephyr/bluetooth/gatt.hpp b/c2usb/port/zephyr/bluetooth/gatt.hpp new file mode 100644 index 0000000..dd69285 --- /dev/null +++ b/c2usb/port/zephyr/bluetooth/gatt.hpp @@ -0,0 +1,347 @@ +/// @file +/// +/// @author Benedek Kupper +/// @date 2023 +/// +/// @copyright +/// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +/// If a copy of the MPL was not distributed with this file, You can obtain one at +/// https://mozilla.org/MPL/2.0/. +/// +#ifndef __PORT_ZEPHYR_BLUETOOTH_GATT_HPP_ +#define __PORT_ZEPHYR_BLUETOOTH_GATT_HPP_ + +#define C2USB_HAS_ZEPHYR_BT_GATT_HEADERS __has_include("zephyr/bluetooth/gatt.h") +#if C2USB_HAS_ZEPHYR_BT_GATT_HEADERS + +#include +#include +#include + +#include "c2usb.hpp" + +namespace bluetooth::zephyr +{ +using uuid = ::bt_uuid; + +/// @brief 16-bit UUID structure definition +/// @tparam UUID_CODE the uuid code, usually zephyr's _VAL suffixed macro, e.g. BT_UUID_BAS_VAL +/// @return reference to the full uuid16 structure in rodata +template +inline const uuid& uuid16() +{ + static const ::bt_uuid_16 u BT_UUID_INIT_16(UUID_CODE); + return u.uuid; +} + +} // namespace bluetooth::zephyr + +// https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/gatt.html +namespace bluetooth::zephyr::gatt +{ +enum class permissions : uint16_t +{ + NONE = ::bt_gatt_perm::BT_GATT_PERM_NONE, + READ = ::bt_gatt_perm::BT_GATT_PERM_READ, + WRITE = ::bt_gatt_perm::BT_GATT_PERM_WRITE, + READ_ENCRYPT = ::bt_gatt_perm::BT_GATT_PERM_READ_ENCRYPT, + WRITE_ENCRYPT = ::bt_gatt_perm::BT_GATT_PERM_WRITE_ENCRYPT, + READ_AUTHEN = ::bt_gatt_perm::BT_GATT_PERM_READ_AUTHEN, + WRITE_AUTHEN = ::bt_gatt_perm::BT_GATT_PERM_WRITE_AUTHEN, + PREPARE_WRITE = ::bt_gatt_perm::BT_GATT_PERM_PREPARE_WRITE, + READ_LESC = ::bt_gatt_perm::BT_GATT_PERM_READ_LESC, + WRITE_LESC = ::bt_gatt_perm::BT_GATT_PERM_WRITE_LESC, +}; + +enum class properties : uint8_t +{ + NONE = 0, + BROADCAST = BT_GATT_CHRC_BROADCAST, + READ = BT_GATT_CHRC_READ, + WRITE_WITHOUT_RESP = BT_GATT_CHRC_WRITE_WITHOUT_RESP, + WRITE = BT_GATT_CHRC_WRITE, + NOTIFY = BT_GATT_CHRC_NOTIFY, + INDICATE = BT_GATT_CHRC_INDICATE, + AUTH = BT_GATT_CHRC_AUTH, + EXT_PROP = BT_GATT_CHRC_EXT_PROP, +}; + +enum class write_flags : uint8_t +{ + NONE = 0, + PREPARE = BT_GATT_WRITE_FLAG_PREPARE, + COMMAND = BT_GATT_WRITE_FLAG_CMD, + EXECUTE = BT_GATT_WRITE_FLAG_EXECUTE, +}; + +class attribute; + +enum class ccc_flags : uint16_t +{ + NONE = 0, + NOTIFY = BT_GATT_CCC_NOTIFY, + INDICATE = BT_GATT_CCC_INDICATE, +}; + +/// @brief This class stores the context information for GATT CCC descriptors, +/// which have different value per connected client. +class ccc_store : public ::_bt_gatt_ccc +{ + public: + constexpr ccc_store() + : _bt_gatt_ccc() + {} + + ccc_store(void (*changed)(const attribute*, ccc_flags), + ssize_t (*cfg_write)(::bt_conn*, const attribute*, ccc_flags), + bool (*cfg_match)(::bt_conn*, const attribute*) = nullptr) + : _bt_gatt_ccc{ + .cfg_changed = reinterpret_cast(changed), + .cfg_write = + reinterpret_cast( + cfg_write), + .cfg_match = reinterpret_cast(cfg_match)} + {} + + protected: + constexpr size_t cfg_size() const { return sizeof(cfg) / sizeof(cfg[0]); } +}; + +/// @brief This class stores the GATT characteristic declaration information. +class char_decl : public ::bt_gatt_chrc +{ + public: + constexpr char_decl(const bt::uuid& id, gatt::properties props, uint16_t handle = 0) + : bt_gatt_chrc{.uuid = &id, + .value_handle = handle, + .properties = + static_cast().properties)>(props)} + {} +}; + +/// @brief This class encapsulates the generic GATT attribute functionality. +class attribute : public ::bt_gatt_attr +{ + public: + using read_fn = ssize_t (*)(::bt_conn*, const attribute*, uint8_t*, uint16_t, uint16_t); + using write_fn = ssize_t (*)(::bt_conn*, const attribute*, const uint8_t*, uint16_t, uint16_t, + write_flags); + + attribute() + : bt_gatt_attr() + {} + + template + attribute(const bt::uuid& uuid, gatt::permissions perm, ::bt_gatt_attr_read_func_t read, + ::bt_gatt_attr_write_func_t write, T* user_data = nullptr) + : bt_gatt_attr{.uuid = &uuid, + .read = read, + .write = write, + .user_data = + reinterpret_cast(const_cast*>(user_data)), + .handle = 0, + .perm = static_cast>(perm)} + {} + + template + attribute(const bt::uuid& uuid, gatt::permissions perm, read_fn read, write_fn write, + T* user_data = nullptr) + : bt_gatt_attr{.uuid = &uuid, + .read = reinterpret_cast<::bt_gatt_attr_read_func_t>(read), + .write = reinterpret_cast<::bt_gatt_attr_write_func_t>(write), + .user_data = + reinterpret_cast(const_cast*>(user_data)), + .handle = 0, + .perm = static_cast>(perm)} + {} + + template + attribute(const bt::uuid& uuid, gatt::permissions perm, T user_value) + : bt_gatt_attr{.uuid = &uuid, + .read = + reinterpret_cast<::bt_gatt_attr_read_func_t>(&attribute::read_value), + .write = nullptr, + .user_data = reinterpret_cast( + std::bit_cast>(user_value)), + .handle = 0, + .perm = static_cast>(perm)} + {} + + template + T user_value() const + { + return std::bit_cast(reinterpret_cast>(user_data)); + } + + template + static ssize_t read_value(::bt_conn* conn, const attribute* attr, uint8_t* buf, uint16_t len, + uint16_t offset) + { + auto value = attr->user_value(); + return bt_gatt_attr_read(conn, attr, static_cast(buf), len, offset, &value, + sizeof(T)); + } + + template + static ssize_t read_range(::bt_conn* conn, const attribute* attr, uint8_t* buf, uint16_t len, + uint16_t offset) + { + auto* range = reinterpret_cast(attr->user_data); + + return bt_gatt_attr_read(conn, attr, static_cast(buf), len, offset, range->data(), + range->size()); + } + + template + static ssize_t read_static_data(::bt_conn* conn, const attribute* attr, uint8_t* buf, + uint16_t len, uint16_t offset) + { + return bt_gatt_attr_read(conn, attr, static_cast(buf), len, offset, attr->user_data, + SIZE); + } + + bool ccc_active(::bt_conn* conn, ccc_flags flag) const + { + return bt_gatt_is_subscribed(conn, this, static_cast(flag)); + } + + template + int notify(const std::span& data, ::bt_gatt_complete_func_t cb, + T* user_data = nullptr, ::bt_conn* conn = nullptr) const + { + ::bt_gatt_notify_params params{.attr = this, + .data = reinterpret_cast(data.data()), + .len = data.size(), + .func = cb, + .user_data = reinterpret_cast(user_data)}; + return bt_gatt_notify_cb(conn, ¶ms); + } + + int notify(const std::span& data, ::bt_conn* conn = nullptr) const + { + return notify(data, nullptr, nullptr, conn); + } + + /// @brief This class provides a convenience to initialize a GATT attributes array. + class builder + { + public: + explicit builder(attribute* attr) + : attr_(attr) + {} + + attribute* data() { return attr_; } + + /// @brief Creates a primary service attribute. + /// @param uuid service type UUID + /// @return builder to continue setting the attribute array + builder primary_service(const bt::uuid& uuid) + { + *attr_ = attribute(uuid16(), permissions::READ, + bt_gatt_attr_read_service, nullptr, &uuid); + return builder(attr_ + 1); + } + + /// @brief Creates a characteristic using two attributes. + /// @tparam T user data's deduced type + /// @tparam TRead read method's deduced type + /// @tparam TWrite write method's deduced type + /// @param info characteristic declaration + /// @param perm characteristic value attribute access permissions + /// @param read the reader method gets called by GATT to get the characteristic value + /// @param write the writer method gets called by GATT to set the characteristic value + /// @param user_data context pointer + /// @return builder to continue setting the attribute array + template + builder characteristic(const char_decl& info, permissions perm, TRead read, TWrite write, + T* user_data = nullptr) + { + *attr_ = attribute(uuid16(), permissions::READ, + bt_gatt_attr_read_chrc, nullptr, &info); + *(attr_ + 1) = attribute(*info.uuid, perm, read, write, user_data); + return builder(attr_ + 2); + } + + /// @brief Creates a characteristic with a fixed value, using two attributes. + /// @tparam T the fixed value's deduced type + /// @param info characteristic declaration + /// @param perm characteristic value attribute access permissions + /// @param value fixed value + /// @return builder to continue setting the attribute array + template + builder characteristic(const char_decl& info, permissions perm, T value) + { + *attr_ = attribute(uuid16(), permissions::READ, + bt_gatt_attr_read_chrc, nullptr, &info); + *(attr_ + 1) = attribute(*info.uuid, perm, value); + return builder(attr_ + 2); + } + + /// @brief Creates a descriptor with a fixed value. + /// @tparam T the fixed value's deduced type + /// @param uuid descriptor type UUID + /// @param perm descriptor attribute access permissions + /// @param value fixed value + /// @return builder to continue setting the attribute array + template + builder descriptor(const bt::uuid& uuid, permissions perm, T value) + { + *attr_ = attribute(uuid, perm, value); + return builder(attr_ + 1); + } + + /// @brief Creates a Client Characteristic Configuration descriptor. + /// @param ccc the CCC context object + /// @param perm descriptor attribute access permissions + /// @return builder to continue setting the attribute array + builder ccc_descriptor(ccc_store& ccc, permissions perm) + { + *attr_ = attribute(uuid16(), perm, bt_gatt_attr_read_ccc, + bt_gatt_attr_write_ccc, &ccc); + return builder(attr_ + 1); + } + + private: + attribute* attr_; + }; +}; + +/// @brief This class manages the registration of GATT services. +/// Requires the CONFIG_BT_GATT_DYNAMIC_DB to be set. +class service : public ::bt_gatt_service +{ + public: + service(const std::span& attrs) + : bt_gatt_service{.attrs = attrs.data(), .attr_count = attrs.size()} + { + [[maybe_unused]] auto ret = bt_gatt_service_register(this); + } + ~service() { [[maybe_unused]] auto ret = bt_gatt_service_unregister(this); } +}; + +} // namespace bluetooth::zephyr::gatt + +template <> +struct magic_enum::customize::enum_range +{ + static constexpr bool is_flags = true; +}; +template <> +struct magic_enum::customize::enum_range +{ + static constexpr bool is_flags = true; +}; +template <> +struct magic_enum::customize::enum_range +{ + static constexpr bool is_flags = true; +}; +template <> +struct magic_enum::customize::enum_range +{ + static constexpr bool is_flags = true; +}; + +#endif // C2USB_HAS_ZEPHYR_BT_GATT_HEADERS + +#endif // __PORT_ZEPHYR_BLUETOOTH_GATT_HPP_ diff --git a/c2usb/port/zephyr/bluetooth/hid.cpp b/c2usb/port/zephyr/bluetooth/hid.cpp new file mode 100644 index 0000000..3d64e12 --- /dev/null +++ b/c2usb/port/zephyr/bluetooth/hid.cpp @@ -0,0 +1,515 @@ +/// @file +/// +/// @author Benedek Kupper +/// @date 2023 +/// +/// @copyright +/// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +/// If a copy of the MPL was not distributed with this file, You can obtain one at +/// https://mozilla.org/MPL/2.0/. +/// +#include "port/zephyr/bluetooth/hid.hpp" +#if C2USB_HAS_ZEPHYR_BT_GATT_HEADERS +#include + +LOG_MODULE_REGISTER(hogp, 0); + +using namespace magic_enum::bitwise_operators; +using namespace ::hid; +using namespace bluetooth::zephyr; +using namespace bluetooth::zephyr::hid; + +#define HOGP_ALREADY_CONNECTED_ERROR BT_ATT_ERR_PROCEDURE_IN_PROGRESS + +gatt::permissions service::get_access(security sec) +{ + using namespace bt::gatt; + switch (sec) + { + case security::AUTH_ENCRYPT: + return permissions::READ_AUTHEN | permissions::WRITE_AUTHEN; + case security::ENCRYPT: + return permissions::READ_ENCRYPT | permissions::WRITE_ENCRYPT; + default: + return permissions::READ | permissions::WRITE; + } +} +gatt::permissions service::access() const +{ + return access_; +} +gatt::permissions service::read_access() const +{ + using namespace bt::gatt; + return access() & (permissions::READ | permissions::READ_AUTHEN | permissions::READ_ENCRYPT | + permissions::READ_LESC); +} +gatt::permissions service::write_access() const +{ + using namespace bt::gatt; + return access() & (permissions::WRITE | permissions::WRITE_AUTHEN | permissions::WRITE_ENCRYPT | + permissions::WRITE_LESC); +} + +const gatt::char_decl& service::report_map_info() +{ + using namespace bt::gatt; + static const char_decl info{uuid16(), properties::READ}; + return info; +} +const gatt::char_decl& service::hid_info() +{ + using namespace bt::gatt; + static const char_decl info{uuid16(), properties::READ}; + return info; +} +const gatt::char_decl& service::protocol_mode_info() +{ + using namespace bt::gatt; + static const char_decl info{uuid16(), + properties::READ | properties::WRITE_WITHOUT_RESP}; + return info; +} +const gatt::char_decl& service::control_point_info() +{ + using namespace bt::gatt; + static const char_decl info{uuid16(), + properties::WRITE_WITHOUT_RESP}; + return info; +} +const gatt::char_decl& service::input_report_info() +{ + using namespace bt::gatt; + static const char_decl info{uuid16(), + properties::READ | properties::NOTIFY}; + return info; +} +const gatt::char_decl& service::output_report_info() +{ + using namespace bt::gatt; + static const char_decl info{uuid16(), + properties::READ | properties::WRITE | + properties::WRITE_WITHOUT_RESP}; + return info; +} +const gatt::char_decl& service::feature_report_info() +{ + return output_report_info(); +} + +ssize_t service::get_report_map(::bt_conn* conn, const ::bt_gatt_attr* attr, void* buf, + uint16_t len, uint16_t offset) +{ + auto* this_ = reinterpret_cast(attr->user_data); + auto& desc = this_->app_.report_info().descriptor; + return bt_gatt_attr_read(conn, attr, buf, len, offset, const_cast(desc.data()), + desc.size()); +} + +ssize_t service::get_protocol_mode(::bt_conn* conn, const ::bt_gatt_attr* attr, void* buf, + uint16_t len, uint16_t offset) +{ + auto* this_ = reinterpret_cast(attr->user_data); + + auto protocol = this_->app_.get_protocol(); + return bt_gatt_attr_read(conn, attr, buf, len, offset, reinterpret_cast(&protocol), + sizeof(protocol)); +} + +ssize_t service::set_protocol_mode(::bt_conn* conn, const ::bt_gatt_attr* attr, void const* buf, + uint16_t len, uint16_t offset, uint8_t flags) +{ + auto* this_ = reinterpret_cast(attr->user_data); + + if (offset > 0) + { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + if (len > sizeof(::hid::protocol)) + { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + auto protocol = *(const ::hid::protocol*)buf; + if (!magic_enum::enum_contains<::hid::protocol>(protocol)) + { + return BT_GATT_ERR(BT_ATT_ERR_NOT_SUPPORTED); + } + + LOG_INF("set protocol: %u", static_cast(protocol)); + if (!this_->start_app(conn, protocol)) + { + return BT_GATT_ERR(HOGP_ALREADY_CONNECTED_ERROR); + } + return len; +} + +ssize_t service::control_point_request(::bt_conn* conn, const ::bt_gatt_attr* attr, void const* buf, + uint16_t len, uint16_t offset, uint8_t flags) +{ + auto* this_ = reinterpret_cast(attr->user_data); + + if (offset > 0) + { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + if (len > sizeof(uint8_t)) + { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + auto ev = *(const bt::hid::event*)buf; + switch (ev) + { + case event::SUSPEND: + case event::EXIT_SUSPEND: + LOG_INF("control point set: %u", static_cast(ev)); + if (this_->power_event_delegate_) + { + this_->power_event_delegate_(*this_, ev); + } + return len; + default: + return BT_GATT_ERR(BT_ATT_ERR_NOT_SUPPORTED); + } +} + +void service::request_get_report(::hid::report::selector sel, const std::span& buffer) +{ + get_report_ = sel; + get_report_buffer_ = {}; + app_.get_report(sel, buffer); + get_report_ = {}; +} + +ssize_t service::get_report(::bt_conn* conn, const gatt::attribute* attr, uint8_t* buf, + uint16_t len, uint16_t offset) +{ + LOG_DBG("get report, size:%u, offset:%u", len, offset); + + auto* this_ = reinterpret_cast(attr->user_data); + if (!this_->start_app(conn)) + { + return BT_GATT_ERR(HOGP_ALREADY_CONNECTED_ERROR); + } + + this_->request_get_report(report_attr_selector(attr), {buf, len}); + auto data = this_->get_report_buffer_; + if (data.data() == nullptr) + { + LOG_WRN("get report failed"); + return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); + } + return bt_gatt_attr_read(conn, attr, buf, len, offset, data.data(), data.size()); +} + +ssize_t service::set_report(::bt_conn* conn, const gatt::attribute* attr, const uint8_t* buf, + uint16_t len, uint16_t offset, gatt::write_flags flag) +{ + LOG_DBG("set report, size:%u, offset:%u", len, offset); + + auto* this_ = reinterpret_cast(attr->user_data); + if (!this_->start_app(conn)) + { + return BT_GATT_ERR(HOGP_ALREADY_CONNECTED_ERROR); + } + + auto sel = report_attr_selector(attr); + if (sel.type() == report::type::INPUT) + { + return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); + } + // split writes are not supported by the HID application design + if ((offset > 0) or ((offset + len) > this_->app_.report_info().max_report_size(sel.type()))) + { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + // copy to application buffer to extend data lifetime + auto buffer = this_->rx_buffers_[sel.type()]; + if (buffer.size() < (offset + len)) + { + return BT_GATT_ERR(BT_ATT_ERR_WRITE_REQ_REJECTED); + } + buffer = buffer.subspan(0, len); + ::memcpy(buffer.data(), buf, len); + + this_->rx_buffers_[sel.type()] = {}; + this_->app_.set_report(sel.type(), buffer); + return len; +} + +::hid::result service::send_report(const std::span& data, report::type type) +{ + // reroute report when in a querying context + if ((get_report_.type() == type) and + ((get_report_.id() == 0) or (get_report_.id() == data.front()))) + { + get_report_buffer_ = data; + get_report_ = {}; + return result::OK; + } + + // feature reports can only be sent if a GET_REPORT command is pending + // output reports cannot be sent + if (type != report::type::INPUT) + { + return result::INVALID; + } + + const gatt::attribute* attr = input_report_attr(report::id(data.front())); + if (attr == nullptr) + { + return result::INVALID; + } + + // HOGP would allow parallel report notifications (with different report IDs), + // but rather keep the application compatible with other transports + if (input_buffer_.size() > 0) + { + return result::BUSY; + } + + auto ret = attr->notify( + data, + [](::bt_conn*, void* user_data) + { + auto* this_ = reinterpret_cast(user_data); + auto buf = this_->input_buffer_; + this_->input_buffer_ = {}; + LOG_DBG("input report sent (size %u)", buf.size()); + this_->app_.in_report_sent(buf); + }, + this); + switch (ret) + { + case 0: + input_buffer_ = data; + return result::OK; + case -ENOENT: + case -EINVAL: + return result::INVALID; + default: + return result::NO_CONNECTION; + } +} + +::hid::result service::receive_report(const std::span& data, ::hid::report::type type) +{ + // the transfer is initiated by the other end + rx_buffers_[type] = data; + return result::OK; +} + +gatt::attribute::builder service::add_input_report(gatt::attribute::builder attr_tail, + gatt::ccc_store* ccc, ::hid::report::id::type id) +{ + using namespace ::hid::report; + + *ccc = gatt::ccc_store( + nullptr, + // a single connection is allowed to interact with HOGP at a time + // to avoid conflicting protocol modes + [](::bt_conn* conn, const gatt::attribute* attr, gatt::ccc_flags flags) -> ssize_t + { + attr -= 2; // distance between characteristic value and ccc descriptor + auto* this_ = reinterpret_cast(attr->user_data); + + LOG_DBG("report CCC set: %u", static_cast(flags)); + if (flags != gatt::ccc_flags::NONE) + { + return this_->start_app(conn) ? sizeof(flags) + : BT_GATT_ERR(HOGP_ALREADY_CONNECTED_ERROR); + } + return sizeof(flags); + }, + nullptr); + + return attr_tail + .characteristic(input_report_info(), read_access(), &service::get_report, nullptr, this) + .descriptor(uuid16(), read_access(), selector(type::INPUT, id)) + .ccc_descriptor(*ccc, access()); +} + +gatt::attribute::builder service::add_output_report(gatt::attribute::builder attr_tail, + ::hid::report::id::type id) +{ + using namespace ::hid::report; + + return attr_tail + .characteristic(output_report_info(), write_access(), nullptr, &service::set_report, this) + .descriptor(uuid16(), read_access(), + selector(type::OUTPUT, id)); +} + +gatt::attribute::builder service::add_feature_report(gatt::attribute::builder attr_tail, + ::hid::report::id::type id) +{ + using namespace ::hid::report; + + return attr_tail + .characteristic(feature_report_info(), access(), &service::get_report, &service::set_report, + this) + .descriptor(uuid16(), read_access(), + selector(type::FEATURE, id)); +} + +void service::connected(::bt_conn* conn) {} + +void service::disconnected(::bt_conn* conn) +{ + stop_app(conn); +} + +bool service::start_app(::bt_conn* conn, ::hid::protocol protocol) +{ + ::bt_conn* expected = nullptr; + if (!active_conn_.compare_exchange_strong(expected, conn)) + { + return expected == conn; + } + + auto result = app_.setup(this, protocol); + LOG_INF("starting HID app %u", result); + if (!result) + { + active_conn_.compare_exchange_strong(conn, nullptr); + } + return result; +} + +void service::stop_app(::bt_conn* conn) +{ + if (!active_conn_.compare_exchange_strong(conn, nullptr)) + { + return; + } + + auto result = app_.teardown(this); + LOG_INF("stopping HID app %u", result); +} + +std::span service::fill_attributes(const std::span& attrs, + const std::span& cccs, flags f) +{ + auto* ccc_ptr = cccs.data(); + + // base service attributes + auto attr_tail = + gatt::attribute::builder(attrs.data()) + .primary_service(uuid16()) + .characteristic(report_map_info(), read_access(), + //&gatt::attribute::read_range, + // nullptr, &app_.report_info().descriptor) + &service::get_report_map, nullptr, this) + .characteristic(hid_info(), read_access(), hid::info(f)) + .characteristic(protocol_mode_info(), access(), &service::get_protocol_mode, + &service::set_protocol_mode, + this) // keep "this" here as long as it's used by for_each() + .characteristic(control_point_info(), write_access(), nullptr, + &service::control_point_request, this); + + assert(attr_tail.data() == (attrs.data() + base_attribute_count())); + + // TODO add support for boot attributes + + // report protocol attributes + if (app_.report_info().max_input_size > 0) + { + if (!app_.report_info().uses_report_ids()) + { + attr_tail = add_input_report(attr_tail, ccc_ptr); + ccc_ptr++; + } + else + { + for (auto id = ::hid::report::id::min(); id <= app_.report_info().max_input_id; id++) + { + attr_tail = add_input_report(attr_tail, ccc_ptr, id); + ccc_ptr++; + } + } + } + if (app_.report_info().max_output_size > 0) + { + if (!app_.report_info().uses_report_ids()) + { + attr_tail = add_output_report(attr_tail); + } + else + { + for (auto id = ::hid::report::id::min(); id <= app_.report_info().max_output_id; id++) + { + attr_tail = add_output_report(attr_tail, id); + } + } + } + if (app_.report_info().max_feature_size > 0) + { + if (!app_.report_info().uses_report_ids()) + { + attr_tail = add_feature_report(attr_tail); + } + else + { + for (auto id = ::hid::report::id::min(); id <= app_.report_info().max_feature_id; id++) + { + attr_tail = add_feature_report(attr_tail, id); + } + } + } + assert(attr_tail.data() == (&attrs.back() + 1)); + assert(attr_tail.data() == (attrs.data() + attrs.size())); + + return attrs; +} + +const gatt::attribute* service::input_report_attr(::hid::report::id::type id) const +{ + if (app_.report_info().max_input_size == 0) + { + return nullptr; + } + + const gatt::attribute* attr = &attributes()[base_attribute_count()]; + if (app_.report_info().uses_report_ids()) + { + if ((id > app_.report_info().max_input_id) or (id < report::id::min())) + { + return nullptr; + } + attr += input_report_attribute_count() * (id - report::id::min()); + } + return attr; +} + +service* service::base_from_input_report_attr(const gatt::attribute* attr) +{ + auto sel = attr[report_reference_offset()].user_value(); + assert(sel.type() == report::type::INPUT); + uint8_t offset = 0; + if (sel.id() > report::id::min()) + { + offset = sel.id() - report::id::min(); + attr -= input_report_attribute_count() * offset; + } + attr -= base_attribute_count(); + return base_from_service_attr(attr); +} + +service* service::base_from_service_attr(const gatt::attribute* attr) +{ + struct base_finder : public service + { + using service::service; + gatt::attribute field; + }; + return CONTAINER_OF(attr, base_finder, field); +} + +BT_CONN_CB_DEFINE(hid_service_conn_callbacks) = { + .disconnected = [](::bt_conn* conn, uint8_t reason) + { service::for_each<::bt_conn*, &service::disconnected>(conn); }, +}; + +#endif // C2USB_HAS_ZEPHYR_BT_HEADERS diff --git a/c2usb/port/zephyr/bluetooth/hid.hpp b/c2usb/port/zephyr/bluetooth/hid.hpp new file mode 100644 index 0000000..6fa33c7 --- /dev/null +++ b/c2usb/port/zephyr/bluetooth/hid.hpp @@ -0,0 +1,257 @@ +/// @file +/// +/// @author Benedek Kupper +/// @date 2023 +/// +/// @copyright +/// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +/// If a copy of the MPL was not distributed with this file, You can obtain one at +/// https://mozilla.org/MPL/2.0/. +/// +#ifndef __PORT_ZEPHYR_BLUETOOTH_HID_HPP_ +#define __PORT_ZEPHYR_BLUETOOTH_HID_HPP_ + +#include "port/zephyr/bluetooth/gatt.hpp" +#if C2USB_HAS_ZEPHYR_BT_GATT_HEADERS + +#include +#include + +#include "hid/application.hpp" +#include "hid/report_protocol.hpp" +#include "uninit_store.hpp" +#include "usb/class/hid.hpp" + +namespace bluetooth::zephyr::hid +{ +/// @brief HID over GATT feature flags. +enum class flags : uint8_t +{ + NONE = 0, + REMOTE_WAKE = 1, + NORMALLY_CONNECTABLE = 2, +}; + +/// @brief HID over GATT general descriptor. +struct info +{ + usb::version bcdHID{usb::hid::SPEC_VERSION}; + usb::hid::country_code bCountryCode{}; + flags bFlags{}; + + info(flags flag = {}, usb::hid::country_code country_code = {}) + : bCountryCode(country_code), bFlags(flag) + {} +}; + +/// @brief HID over GATT power event. +enum class event : uint8_t +{ + SUSPEND = 0, + EXIT_SUSPEND = 1, +}; + +/// @brief HID over GATT security access control levels. +enum class security : uint8_t +{ + NONE = 0, + ENCRYPT = 1, + AUTH_ENCRYPT = 2, // encryption using authenticated link-key +}; + +/// @brief This class implements the HID over GATT protocol service. +/// To instantiate an object of this class, see @ref service_instance below. +class service : public ::hid::transport +{ + public: + using power_event_delegate = etl::delegate; + + void set_power_event_delegate(const power_event_delegate& delegate) + { + power_event_delegate_ = delegate; + } + + template + static void for_each(T data) + { + bt_gatt_foreach_attr_type( + std::numeric_limits::min(), std::numeric_limits::max(), + protocol_mode_info().uuid, // this attribute is unique, and uses this as user_data + nullptr, 0, + [](const ::bt_gatt_attr* attr, uint16_t, void* user_data) + { + auto* this_ = reinterpret_cast(attr->user_data); + (this_->*FUNC)(reinterpret_cast(user_data)); + return (uint8_t)BT_GATT_ITER_CONTINUE; + }, + reinterpret_cast(data)); + } + + void connected(::bt_conn* conn); + void disconnected(::bt_conn* conn); + + private: + static const gatt::char_decl& report_map_info(); + static const gatt::char_decl& hid_info(); + static const gatt::char_decl& protocol_mode_info(); + static const gatt::char_decl& control_point_info(); + static const gatt::char_decl& input_report_info(); + static const gatt::char_decl& output_report_info(); + static const gatt::char_decl& feature_report_info(); + + static gatt::permissions get_access(security sec); + gatt::permissions access() const; + gatt::permissions read_access() const; + gatt::permissions write_access() const; + + ::hid::result send_report(const std::span& data, + ::hid::report::type type) override; + ::hid::result receive_report(const std::span& data, ::hid::report::type type) override; + + static ssize_t get_report_map(::bt_conn* conn, const ::bt_gatt_attr* attr, void* buf, + uint16_t len, uint16_t offset); + + static ssize_t get_protocol_mode(::bt_conn* conn, const ::bt_gatt_attr* attr, void* buf, + uint16_t len, uint16_t offset); + + static ssize_t set_protocol_mode(::bt_conn* conn, const ::bt_gatt_attr* attr, void const* buf, + uint16_t len, uint16_t offset, uint8_t flags); + + static ssize_t control_point_request(::bt_conn* conn, const ::bt_gatt_attr* attr, + void const* buf, uint16_t len, uint16_t offset, + uint8_t flags); + + static ssize_t get_report(::bt_conn* conn, const gatt::attribute* attr, uint8_t* buf, + uint16_t len, uint16_t offset); + + static ssize_t set_report(::bt_conn* conn, const gatt::attribute* attr, const uint8_t* buf, + uint16_t len, uint16_t offset, gatt::write_flags flag); + + void request_get_report(::hid::report::selector sel, const std::span& buffer); + + static ::hid::report::selector report_attr_selector(const gatt::attribute* attr) + { + return (attr + 1)->user_value<::hid::report::selector>(); + } + + gatt::attribute::builder add_input_report(gatt::attribute::builder attr_tail, + gatt::ccc_store* ccc, ::hid::report::id::type id = 0); + gatt::attribute::builder add_output_report(gatt::attribute::builder attr_tail, + ::hid::report::id::type id = 0); + gatt::attribute::builder add_feature_report(gatt::attribute::builder attr_tail, + ::hid::report::id::type id = 0); + + bool start_app(::bt_conn* conn, ::hid::protocol protocol = ::hid::protocol::REPORT); + void stop_app(::bt_conn* conn); + + const gatt::attribute* attributes() const + { + return reinterpret_cast(gatt_service_.attrs); + } + + std::span fill_attributes(const std::span& attrs, + const std::span& cccs, flags f); + + static service* base_from_input_report_attr(const gatt::attribute* attr); + static service* base_from_service_attr(const gatt::attribute* attr); + const gatt::attribute* input_report_attr(::hid::report::id::type id) const; + + ::hid::application& app_; + gatt::attribute* attrs_; + gatt::permissions access_; + ::hid::report::selector get_report_{}; + std::atomic<::bt_conn*> active_conn_{}; + ::hid::reports_receiver rx_buffers_{}; + std::span get_report_buffer_{}; + std::span input_buffer_{}; + power_event_delegate power_event_delegate_{}; + usb::hid::boot_protocol_mode boot_mode_{}; + gatt::service gatt_service_; // leave as last + + protected: + service(::hid::application& app, flags f, security sec, const std::span& attrs, + const std::span& cccs) + : app_(app), + attrs_(attrs.data()), + access_(get_access(sec)), + gatt_service_(fill_attributes(attrs, cccs, f)) + {} + + static constexpr size_t base_attribute_count() + { + return 1 // service + + 2 // report map + + 2 // HID info + + 2 // protocol mode + + 2 // control point + ; + } + static constexpr size_t input_report_attribute_count() + { + return 2 // report characteristic + + 1 // report reference + + 1 // CCC + ; + } + static constexpr size_t report_reference_offset() { return 2; } + static constexpr size_t input_report_count(const ::hid::report_protocol_properties& props) + { + if (props.max_input_size > 0) + { + return std::max(1, static_cast(props.max_input_id) - 1); + } + return 0; + } + static constexpr size_t attribute_count(const ::hid::report_protocol_properties& props) + { + size_t size = base_attribute_count(); + // note that these numbers are an upper bound only, + // attributes will be wasted if the report ID numbering is not contiguous + size += input_report_attribute_count() * input_report_count(props); + if (props.max_output_size > 0) + { + // output report characteristic + report reference + size += 3 * std::max(1, static_cast(props.max_output_id) - 1); + } + if (props.max_feature_size > 0) + { + // feature report characteristic + report reference + size += 3 * std::max(1, static_cast(props.max_feature_id) - 1); + } + return size; + } + static constexpr size_t ccc_count(const ::hid::report_protocol_properties& props) + { + return input_report_count(props); + } +}; + +/// @brief This class creates the HID over GATT service with the necessary storage. +/// @tparam REPORT_PROPS the report properties that are created out of the HID report descriptor +template <::hid::report_protocol_properties REPORT_PROPS> +class service_instance : public service +{ + public: + service_instance(::hid::application& app, security sec, + flags f = (flags)((uint8_t)flags::REMOTE_WAKE | + (uint8_t)flags::NORMALLY_CONNECTABLE)) + : service(app, f, sec, attributes_.span(), ccc_stores_.span()) + {} + + private: + // update @ref base_from_service_attr if this layout changes! + c2usb::uninit_store attributes_; + c2usb::uninit_store ccc_stores_; +}; + +} // namespace bluetooth::zephyr::hid + +template <> +struct magic_enum::customize::enum_range +{ + static constexpr bool is_flags = true; +}; + +#endif // C2USB_HAS_ZEPHYR_BT_GATT_HEADERS + +#endif // __PORT_ZEPHYR_BLUETOOTH_HID_HPP_ diff --git a/c2usb/uninit_store.hpp b/c2usb/uninit_store.hpp new file mode 100644 index 0000000..37d7e3c --- /dev/null +++ b/c2usb/uninit_store.hpp @@ -0,0 +1,41 @@ +/// @file +/// +/// @author Benedek Kupper +/// @date 2023 +/// +/// @copyright +/// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +/// If a copy of the MPL was not distributed with this file, You can obtain one at +/// https://mozilla.org/MPL/2.0/. +/// +#ifndef __UNINIT_STORE_HPP_ +#define __UNINIT_STORE_HPP_ + +#include + +namespace c2usb +{ +/// @brief This class provides storage space for types without initialization / construction. +/// @tparam T The type to provide storage for +/// @tparam SIZE Optional size property allows for creating contiguous array storage. +template +class alignas(alignof(T)) uninit_store +{ + public: + uninit_store() {} + + T& ref() { return *reinterpret_cast(this); } + const T& ref() const { return *reinterpret_cast(this); } + std::span span() { return {&ref(), SIZE}; } + std::span span() const { return {&ref(), SIZE}; } + + private: + struct alignas(alignof(T)) store + { + std::array bytes_; + }; + std::array items_; +}; +} // namespace c2usb + +#endif // __UNINIT_STORE_HPP_