diff --git a/v5/include/libhal-util/mock/usb.hpp b/v5/include/libhal-util/mock/usb.hpp new file mode 100644 index 0000000..db6ded0 --- /dev/null +++ b/v5/include/libhal-util/mock/usb.hpp @@ -0,0 +1,520 @@ +// Copyright 2024 - 2025 Khalil Estell and the libhal contributors +// +// 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../usb/utils.hpp" + +namespace hal::v5::usb { +constexpr u8 interface_description_length = 9; +constexpr u8 interface_description_type = 0x4; +constexpr u8 string_description_type = 0x3; +constexpr u8 interface_association_descriptor_length = 0x08; +constexpr u8 interface_association_descriptor_type = 0x0B; + +constexpr std::vector pkt_to_scatter(setup_packet const& req) +{ + std::vector vec; + vec.push_back(req.bm_request_type()); + vec.push_back(req.request()); + vec.append_range(setup_packet::to_le_u16(req.value())); + vec.append_range(setup_packet::to_le_u16(req.index())); + vec.append_range(setup_packet::to_le_u16(req.length())); + + return vec; +} + +class mock_usb_endpoint : public usb::endpoint +{ +public: + usb::endpoint_info m_info{}; + bool m_stall_called{ false }; + bool m_should_stall{ false }; + bool m_reset_called{ false }; + +protected: + [[nodiscard]] usb::endpoint_info driver_info() const override + { + return m_info; + } + + void driver_stall(bool p_should_stall) override + { + m_stall_called = true; + m_should_stall = p_should_stall; + } + + void driver_reset() override + { + m_reset_called = true; + } +}; + +class mock_usb_control_endpoint : public control_endpoint +{ +public: + mock_usb_endpoint m_endpoint; + bool m_is_connected{ false }; + u8 m_address{ 0 }; + std::vector m_out_buf; + std::array m_req_buf; + usize m_read_result{ 0 }; + callback m_receive_callback{}; + + void write_request(scatter_span p_data) + { + size_t total_len = 0; + for (std::span const& s : p_data) { + for (byte const el : s) { + m_req_buf[total_len] = el; + total_len++; + } + } + } + + void simulate_interrupt() + { + m_receive_callback(on_receive_tag{}); + } + +private: + [[nodiscard]] endpoint_info driver_info() const override + { + return m_endpoint.info(); + } + + void driver_stall(bool p_should_stall) override + { + m_endpoint.stall(p_should_stall); + } + + void driver_connect(bool p_should_connect) override + { + m_is_connected = p_should_connect; + } + + void driver_set_address(u8 p_address) override + { + m_address = p_address; + } + + void driver_write(scatter_span p_data) override + { + // Ignore ZLPs + if (p_data.empty() || + (scatter_span_size(p_data) == 1 && p_data[0][0] == 0)) { + return; + } + for (std::span const& s : p_data) { + for (byte const el : s) { + m_out_buf.push_back(el); + } + } + } + + usize driver_read(scatter_span p_buffer) override + { + size_t src_idx = 0; + for (auto s : p_buffer) { + for (unsigned char& el : s) { + el = m_req_buf[src_idx]; + src_idx++; + } + } + + return src_idx; + } + + void driver_on_receive( + callback const& p_callback) override + { + m_receive_callback = p_callback; + } + + void driver_reset() override + { + m_endpoint.reset(); + } +}; + +struct mock : public interface +{ + + constexpr mock(std::u16string_view p_name) + : name(p_name) + { + } + + [[nodiscard]] descriptor_count driver_write_descriptors( + descriptor_start p_start, + endpoint_writer const& p_callback) override + { + byte res_iface_idx = 0; + if (p_start.interface.has_value()) { + interface_number() = p_start.interface.value(); + res_iface_idx = 1; + } + + byte res_str_idx = 0; + if (p_start.string.has_value()) { + interface_name_string_idx() = p_start.string.value(); + res_str_idx = 1; + } + + p_callback(make_scatter_bytes(m_packed_array)); + + return { .interface = res_iface_idx, .string = res_str_idx }; + } + + [[nodiscard]] bool driver_write_string_descriptor( + u8 p_index, + endpoint_writer const& p_callback) override + { + if (p_index != interface_name_string_idx()) { + return false; + } + std::array str_hdr( + { static_cast(descriptor_type::string), + static_cast(name.length()) }); + + auto scatter_arr = make_scatter_bytes( + str_hdr, + std::span(reinterpret_cast(name.data()), name.length())); + + p_callback(scatter_span(scatter_arr)); + + return true; + } + + bool driver_handle_request(setup_packet const& p_setup, + endpoint_writer const& p_callback) override + { + std::ignore = p_setup; + std::ignore = p_callback; + return true; + } + + constexpr byte& interface_number() + { + return m_packed_array[2]; + } + + constexpr byte& alt_settings() + { + return m_packed_array[3]; + } + + constexpr byte& interface_name_string_idx() + { + return m_packed_array[8]; + } + + std::array m_packed_array = { + sizeof(m_packed_array), + interface_description_type, + 0, // interface_number + 0, // alternate_setting + 1, // num_endpoints + 2, // interface_class + 3, // interface_sub_class + 4, // interface_protocol + 0 // interface name index + }; + std::u16string_view name; +}; + +class iad_mock : public interface +{ +public: + ~iad_mock() override = default; + + iad_mock(std::u16string_view p_iface_name_one, // NOLINT + std::u16string_view p_iface_name_two) + : m_name_one(p_iface_name_one) + , m_name_two(p_iface_name_two) {}; + + struct mock_iface_descriptor + { + byte num; + byte alt_settings; + byte str_idx; + bool feature; + }; + +private: + [[nodiscard]] descriptor_count driver_write_descriptors( + descriptor_start p_start, + endpoint_writer const& p_callback) override + { + if (!p_start.interface.has_value() || !p_start.string.has_value()) { + throw hal::argument_out_of_domain(this); + } + + auto iface_idx = p_start.interface.value(); + auto str_idx = p_start.string.value(); + std::array iad_buf{ + interface_association_descriptor_length, + interface_association_descriptor_type, + 0, // first interface + 2, // iface count + 0, // class + 0, // subclass + 0, // proto + str_idx++ // string idx + }; + + std::array iface_header = { interface_description_length, + interface_description_type }; + std::array static_desc_vars = { + 0, // altsettings + 1, // num endpoints + 0, // class + 0, // subclass + 0, // protocol + }; + + m_iface_one = { .num = iface_idx++, + .alt_settings = 0, + .str_idx = str_idx++, + .feature = false }; + m_iface_two = { .num = iface_idx++, + .alt_settings = 0, + .str_idx = str_idx++, + .feature = false }; + + std::array iface_one_arr({ m_iface_one.num }); + std::array iface_one_str_arr({ m_iface_one.str_idx }); + + std::array iface_two_arr({ m_iface_two.num }); + std::array iface_two_str_arr({ m_iface_two.str_idx }); + + auto span_arr = make_scatter_bytes(iad_buf, + + // iface one + iface_header, + std::span(iface_one_arr), + static_desc_vars, + std::span(iface_one_str_arr), + + // iface two + iface_header, + std::span(iface_two_arr), + static_desc_vars, + std::span(iface_two_str_arr)); + + p_callback(span_arr); + m_wrote_descriptors = true; + return { .interface = 2, .string = 3 }; + } + + [[nodiscard]] bool driver_write_string_descriptor( + u8 p_index, + endpoint_writer const& p_callback) override + { + if (!m_wrote_descriptors) { + safe_throw(hal::operation_not_permitted(this)); + } + std::array header{ 0, string_description_type }; + if (p_index == m_iface_one.str_idx) { + header[0] = m_name_one.length() + 2; + auto arr = make_scatter_bytes( + header, + std::span(reinterpret_cast(m_name_one.data()), + m_name_one.length())); + p_callback(arr); + return true; + } else if (p_index == m_iface_two.str_idx) { + header[0] = m_iface_two.str_idx + 2; + auto arr = make_scatter_bytes( + header, + std::span(reinterpret_cast(m_name_two.data()), + m_name_two.length())); + + p_callback(arr); + return true; + } + + return false; + } + + u16 driver_get_status(setup_packet p_pkt) + { + if (p_pkt.get_recipient() != setup_packet::request_recipient::interface) { + safe_throw(hal::operation_not_supported(this)); + } + + auto iface_idx = p_pkt.index() & 0xFF; + + if (iface_idx == m_iface_one.num) { + return m_iface_one.num; + } else if (iface_idx == m_iface_two.num) { + return m_iface_two.num; + } + + safe_throw(hal::operation_not_supported(this)); + } + + void manage_features(setup_packet p_pkt, bool p_set, u16 p_selector) + { + std::ignore = p_selector; + if (p_pkt.get_recipient() != setup_packet::request_recipient::interface) { + safe_throw(hal::operation_not_supported(this)); + } + + auto iface_idx = p_pkt.index() & 0xFF; + + if (iface_idx == m_iface_one.num) { + m_iface_one.feature = p_set; + } else if (iface_idx == m_iface_two.num) { + m_iface_two.feature = p_set; + } else { + safe_throw(hal::operation_not_supported(this)); + } + } + + u8 driver_get_interface(setup_packet p_pkt) + { + auto iface_idx = p_pkt.index() & 0xFF; + + if (iface_idx == m_iface_one.num) { + return m_iface_one.alt_settings; + } else if (iface_idx == m_iface_two.num) { + return m_iface_two.alt_settings; + } else { + safe_throw(hal::operation_not_supported(this)); + } + } + + void driver_set_interface(setup_packet p_pkt) + { + auto iface_idx = p_pkt.index() & 0xFF; + auto alt_setting = p_pkt.value(); + if (iface_idx == m_iface_one.num) { + m_iface_one.alt_settings = alt_setting; + } else if (iface_idx == m_iface_two.num) { + m_iface_two.alt_settings = alt_setting; + } else { + safe_throw(hal::operation_not_supported(this)); + } + } + + bool driver_handle_request(setup_packet const& p_setup, + endpoint_writer const& p_callback) override + { + std::ignore = p_setup; + std::ignore = p_callback; + return false; + } + +public: + mock_iface_descriptor m_iface_one; + std::u16string_view m_name_one; + mock_iface_descriptor m_iface_two; + std::u16string_view m_name_two; + bool m_wrote_descriptors = false; +}; + +void simulate_sending_payload( + strong_ptr const& ctrl_ptr, + setup_packet& req) +{ + auto vec = pkt_to_scatter(req); + auto scatter_arr = make_scatter_bytes(vec); + scatter_span ss(scatter_arr); + + ctrl_ptr->write_request(ss); +} + +// u8 calculate_conf_desc_recursive(std::span p_conf_arr) +// { +// u8 total_len = 0; +// for (configuration& conf : p_conf_arr) { +// total_len += 9; +// for (auto& iface : conf.interfaces) { +// auto real_iface = dynamic_cast(&(*iface)); +// std::ignore = iface->write_descriptors( +// { .interface = 0, .string = real_iface->interface_name_string_idx() +// }, +// [&total_len](scatter_span p_data) { +// total_len += scatter_span_size(p_data); +// }); +// } +// } + +// return total_len; +// } + +// std::vector generate_conf_descriptors(std::span +// p_conf_arr) +// { +// std::vector vec; +// for (configuration const& conf : p_conf_arr) { +// vec.append_range(std::span(conf)); +// for (auto const& iface : conf.interfaces) { +// std::ignore = +// iface->write_descriptors({ .interface = 0, .string = 0 }, +// [&vec](scatter_span p_dat) { +// for (auto const& s : p_dat) { +// for (auto const& el : s) { +// vec.push_back(el); +// } +// } +// }); +// } +// } + +// return vec; +// } + +class mock_serial : public hal::serial +{ + void driver_configure(settings const& p_settings) override + { + std::ignore = p_settings; + } + + write_t driver_write(std::span p_data) override + { + std::string_view sv(reinterpret_cast(p_data.data()), + p_data.size()); + return { .data = p_data }; + } + + read_t driver_read(std::span p_data) override + { + std::ignore = p_data; + return { .data = {}, .available = 0, .capacity = 0 }; + } + + void driver_flush() override + { + } +}; + +} // namespace hal::v5::usb diff --git a/v5/include/libhal-util/usb.hpp b/v5/include/libhal-util/usb.hpp index 0bfa259..abfbfb3 100644 --- a/v5/include/libhal-util/usb.hpp +++ b/v5/include/libhal-util/usb.hpp @@ -14,110 +14,7 @@ #pragma once -#include -#include -#include -#include - -namespace hal::v5 { -// TODO(#79): Add doxygen docs to USB APIs -inline void write(usb::control_endpoint& p_endpoint, - scatter_span p_data_out) -{ - p_endpoint.write(p_data_out); -} - -inline void write_and_flush(usb::control_endpoint& p_endpoint, - scatter_span p_data_out) -{ - p_endpoint.write(p_data_out); - p_endpoint.write({}); -} - -inline void write(usb::control_endpoint& p_endpoint, - std::span p_data_out) -{ - p_endpoint.write(make_scatter_bytes(p_data_out)); -} - -inline void write_and_flush(usb::control_endpoint& p_endpoint, - std::span p_data_out) -{ - p_endpoint.write(make_scatter_bytes(p_data_out)); - p_endpoint.write({}); -} - -inline void write(usb::in_endpoint& p_endpoint, - scatter_span p_data_out) -{ - p_endpoint.write(p_data_out); -} - -inline void write_and_flush(usb::in_endpoint& p_endpoint, - scatter_span p_data_out) -{ - p_endpoint.write(p_data_out); - p_endpoint.write({}); -} - -inline void write(usb::in_endpoint& p_endpoint, - std::span p_data_out) -{ - p_endpoint.write(make_scatter_bytes(p_data_out)); -} - -inline void write_and_flush(usb::in_endpoint& p_endpoint, - std::span p_data_out) -{ - p_endpoint.write(make_scatter_bytes(p_data_out)); - p_endpoint.write({}); -} - -inline void write(usb::in_endpoint& p_endpoint, - spanable_bytes auto... p_data_out) -{ - p_endpoint.write(make_scatter_bytes(p_data_out...)); -} - -inline void write_and_flush(usb::in_endpoint& p_endpoint, - spanable_bytes auto... p_data_out) -{ - p_endpoint.write(make_scatter_bytes(p_data_out...)); - p_endpoint.write({}); -} - -inline auto read(usb::out_endpoint& p_endpoint, - scatter_span p_data_out) -{ - return p_endpoint.read(p_data_out); -} - -inline auto read(usb::out_endpoint& p_endpoint, std::span p_data_out) -{ - return p_endpoint.read(make_writable_scatter_bytes(p_data_out)); -} - -inline auto read(usb::out_endpoint& p_endpoint, - spanable_writable_bytes auto... p_data_out) -{ - return p_endpoint.read(make_writable_scatter_bytes(p_data_out...)); -} - -inline auto read(usb::control_endpoint& p_endpoint, - scatter_span p_data_out) -{ - return p_endpoint.read(p_data_out); -} - -inline auto read(usb::control_endpoint& p_endpoint, - std::span p_data_out) -{ - return p_endpoint.read(make_writable_scatter_bytes(p_data_out)); -} - -inline auto read(usb::control_endpoint& p_endpoint, - spanable_writable_bytes auto... p_data_out) -{ - return p_endpoint.read(make_writable_scatter_bytes(p_data_out...)); -} -} // namespace hal::v5 +#include "usb/descriptors.hpp" +#include "usb/endpoints.hpp" +#include "usb/enumerator.hpp" +#include "usb/utils.hpp" diff --git a/v5/include/libhal-util/usb/descriptors.hpp b/v5/include/libhal-util/usb/descriptors.hpp new file mode 100644 index 0000000..73d3b9c --- /dev/null +++ b/v5/include/libhal-util/usb/descriptors.hpp @@ -0,0 +1,325 @@ +// Copyright 2024 - 2025 Khalil Estell and the libhal contributors +// +// 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 +#include + +#include +#include +#include +#include +#include + +#include "utils.hpp" + +/* TODO (PhazonicRidley): + Class, subclass, proto validator + Device qualifer descriptor (happens between device and config) + Other speed descriptor (happens with configuration) +*/ + +namespace hal::v5::usb { + +struct device +{ + template + friend class enumerator; + + struct device_arguments + { + u16 bcd_usb; + class_code device_class; + u8 device_subclass; + u8 device_protocol; + u16 id_vendor; + u16 id_product; + u16 bcd_device; + std::u16string_view p_manufacturer; + std::u16string_view p_product; + std::u16string_view p_serial_number_str; + }; + + constexpr device(device_arguments&& p_args) + : manufacturer_str(p_args.p_manufacturer) + , product_str(p_args.p_product) + , serial_number_str(p_args.p_serial_number_str) + { + u8 idx = 0; + auto bcd_usb_bytes = setup_packet::to_le_u16(p_args.bcd_usb); + for (auto& bcd_usb_byte : bcd_usb_bytes) { + m_packed_arr[idx++] = bcd_usb_byte; + } + m_packed_arr[idx++] = static_cast(p_args.device_class); + m_packed_arr[idx++] = p_args.device_subclass; + m_packed_arr[idx++] = p_args.device_protocol; + + m_packed_arr[idx++] = 0; // Max Packet length handled by the enumerator + auto id_vendor_bytes = setup_packet::to_le_u16(p_args.id_vendor); + for (auto& id_vendor_byte : id_vendor_bytes) { + m_packed_arr[idx++] = id_vendor_byte; + } + + auto id_product_bytes = setup_packet::to_le_u16(p_args.id_product); + for (auto& id_product_byte : id_product_bytes) { + m_packed_arr[idx++] = id_product_byte; + } + + auto bcd_device_bytes = setup_packet::to_le_u16(p_args.bcd_device); + for (auto& bcd_device_byte : bcd_device_bytes) { + m_packed_arr[idx++] = bcd_device_byte; + } + + // Default string indexes, assuming enumeration will use 4 onward for string + // indexes are these are required (can be modified by enumerator if desired) + m_packed_arr[idx++] = 1; // string idx of manufacturer + m_packed_arr[idx++] = 2; // string idx of product + m_packed_arr[idx++] = 3; // string idx of serial number + + // Evaluated during enumeration + m_packed_arr[idx++] = 0; // Number of possible configurations + }; + + [[nodiscard]] constexpr u16 bcd_usb() const + { + return setup_packet::from_le_bytes(m_packed_arr[0], m_packed_arr[1]); + } + + [[nodiscard]] constexpr u8 device_class() const + { + return m_packed_arr[2]; + } + + [[nodiscard]] constexpr u8 device_sub_class() const + { + return m_packed_arr[3]; + } + + [[nodiscard]] constexpr u8 device_protocol() const + { + return m_packed_arr[4]; + } + + [[nodiscard]] constexpr u16 id_vendor() const + { + return setup_packet::from_le_bytes(m_packed_arr[6], m_packed_arr[7]); + } + + [[nodiscard]] constexpr u16 id_product() const + { + return setup_packet::from_le_bytes(m_packed_arr[8], m_packed_arr[9]); + } + + [[nodiscard]] constexpr u16 bcd_device() const + { + return setup_packet::from_le_bytes(m_packed_arr[10], m_packed_arr[11]); + } + + operator std::span() const + { + return m_packed_arr; + } + + std::u16string_view manufacturer_str; + std::u16string_view product_str; + std::u16string_view serial_number_str; + +private: + [[nodiscard]] constexpr u8 max_packet_size() const + { + return m_packed_arr[5]; + } + constexpr void set_max_packet_size(u8 p_size) + { + m_packed_arr[5] = p_size; + } + + [[nodiscard]] constexpr u8 manufacturer_index() const + { + return m_packed_arr[12]; + } + + [[nodiscard]] constexpr u8 product_index() const + { + return m_packed_arr[13]; + } + + [[nodiscard]] constexpr u8 serial_number_index() const + { + return m_packed_arr[14]; + } + + [[nodiscard]] constexpr u8 num_configurations() const + { + return m_packed_arr[15]; + } + constexpr void set_num_configurations(u8 p_count) + { + m_packed_arr[15] = p_count; + } + + std::array m_packed_arr; +}; + +// https://www.beyondlogic.org/usbnutshell/usb5.shtml#ConfigurationDescriptors + +template +concept usb_interface_concept = std::derived_from; + +// Calculate: total length, number of interfaces, configuration value +struct configuration +{ + + template + friend class enumerator; + + struct bitmap + { + + constexpr bitmap(u8 p_bitmap) + : m_bitmap(p_bitmap) + { + } + + constexpr bitmap(bool p_self_powered, bool p_remote_wakeup) + { + m_bitmap = (1 << 7) | (p_self_powered << 6) | (p_remote_wakeup << 5); + } + + constexpr u8 to_byte() + { + return m_bitmap; + } + + [[nodiscard]] constexpr bool self_powered() const + { + return static_cast(m_bitmap & 1 << 6); + } + + [[nodiscard]] constexpr bool remote_wakeup() const + { + return static_cast(m_bitmap & 1 << 5); + } + + private: + u8 m_bitmap; + }; + + /** Configuration descriptor info. + * @note The name string_view must outlive the configuration object. + */ + struct configuration_info + { + std::u16string_view name; + bitmap attributes; + u8 max_power; + std::pmr::polymorphic_allocator<> allocator; + }; + + template + constexpr configuration(configuration_info p_info, + strong_ptr... p_interfaces) + : name(p_info.name) + , interfaces(p_info.allocator) + { + interfaces.reserve(sizeof...(p_interfaces)); + (interfaces.push_back(p_interfaces), ...); + u8 idx = 0; + + // Anything marked with 0 is to be populated at enumeration time + m_packed_arr[idx++] = 0; // 0 Total Length + m_packed_arr[idx++] = 0; + m_packed_arr[idx++] = interfaces.size(); // 2 number of interfaces + m_packed_arr[idx++] = 0; // 3 Config number + m_packed_arr[idx++] = 0; // 4 Configuration name string index + + m_packed_arr[idx++] = p_info.attributes.to_byte(); // 5 + m_packed_arr[idx++] = p_info.max_power; // 6 + } + + operator std::span() const + { + return m_packed_arr; + } + + constexpr bitmap attributes() + { + return { m_packed_arr[5] }; + } + [[nodiscard]] constexpr u8 attributes_byte() const + { + return m_packed_arr[5]; + } + + [[nodiscard]] constexpr u8 max_power() const + { + return m_packed_arr[6]; + } + + std::u16string_view name; + std::pmr::vector> interfaces; + +private: + [[nodiscard]] constexpr u16 total_length() const + { + return setup_packet::from_le_bytes(m_packed_arr[0], m_packed_arr[1]); + } + + constexpr void set_total_length(u16 p_length) + { + auto bytes = setup_packet::to_le_u16(p_length); + m_packed_arr[0] = bytes[0]; + m_packed_arr[1] = bytes[1]; + } + [[nodiscard]] constexpr u8 num_interfaces() const + { + return m_packed_arr[2]; + } + constexpr void set_num_interfaces(u8 p_count) + { + m_packed_arr[2] = p_count; + } + + [[nodiscard]] constexpr u8 configuration_value() const + { + return m_packed_arr[3]; + } + constexpr void set_configuration_value(u8 p_value) + { + m_packed_arr[3] = p_value; + } + + [[nodiscard]] constexpr u8 configuration_index() const + { + return m_packed_arr[4]; + } + constexpr void set_configuration_index(u8 p_index) + { + m_packed_arr[4] = p_index; + } + + std::array m_packed_arr; +}; +} // namespace hal::v5::usb + +namespace hal::usb { +using v5::usb::configuration; +using v5::usb::device; +using v5::usb::usb_interface_concept; +} // namespace hal::usb diff --git a/v5/include/libhal-util/usb/endpoints.hpp b/v5/include/libhal-util/usb/endpoints.hpp new file mode 100644 index 0000000..0bfa259 --- /dev/null +++ b/v5/include/libhal-util/usb/endpoints.hpp @@ -0,0 +1,123 @@ +// Copyright 2024 - 2025 Khalil Estell and the libhal contributors +// +// 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 + +namespace hal::v5 { +// TODO(#79): Add doxygen docs to USB APIs +inline void write(usb::control_endpoint& p_endpoint, + scatter_span p_data_out) +{ + p_endpoint.write(p_data_out); +} + +inline void write_and_flush(usb::control_endpoint& p_endpoint, + scatter_span p_data_out) +{ + p_endpoint.write(p_data_out); + p_endpoint.write({}); +} + +inline void write(usb::control_endpoint& p_endpoint, + std::span p_data_out) +{ + p_endpoint.write(make_scatter_bytes(p_data_out)); +} + +inline void write_and_flush(usb::control_endpoint& p_endpoint, + std::span p_data_out) +{ + p_endpoint.write(make_scatter_bytes(p_data_out)); + p_endpoint.write({}); +} + +inline void write(usb::in_endpoint& p_endpoint, + scatter_span p_data_out) +{ + p_endpoint.write(p_data_out); +} + +inline void write_and_flush(usb::in_endpoint& p_endpoint, + scatter_span p_data_out) +{ + p_endpoint.write(p_data_out); + p_endpoint.write({}); +} + +inline void write(usb::in_endpoint& p_endpoint, + std::span p_data_out) +{ + p_endpoint.write(make_scatter_bytes(p_data_out)); +} + +inline void write_and_flush(usb::in_endpoint& p_endpoint, + std::span p_data_out) +{ + p_endpoint.write(make_scatter_bytes(p_data_out)); + p_endpoint.write({}); +} + +inline void write(usb::in_endpoint& p_endpoint, + spanable_bytes auto... p_data_out) +{ + p_endpoint.write(make_scatter_bytes(p_data_out...)); +} + +inline void write_and_flush(usb::in_endpoint& p_endpoint, + spanable_bytes auto... p_data_out) +{ + p_endpoint.write(make_scatter_bytes(p_data_out...)); + p_endpoint.write({}); +} + +inline auto read(usb::out_endpoint& p_endpoint, + scatter_span p_data_out) +{ + return p_endpoint.read(p_data_out); +} + +inline auto read(usb::out_endpoint& p_endpoint, std::span p_data_out) +{ + return p_endpoint.read(make_writable_scatter_bytes(p_data_out)); +} + +inline auto read(usb::out_endpoint& p_endpoint, + spanable_writable_bytes auto... p_data_out) +{ + return p_endpoint.read(make_writable_scatter_bytes(p_data_out...)); +} + +inline auto read(usb::control_endpoint& p_endpoint, + scatter_span p_data_out) +{ + return p_endpoint.read(p_data_out); +} + +inline auto read(usb::control_endpoint& p_endpoint, + std::span p_data_out) +{ + return p_endpoint.read(make_writable_scatter_bytes(p_data_out)); +} + +inline auto read(usb::control_endpoint& p_endpoint, + spanable_writable_bytes auto... p_data_out) +{ + return p_endpoint.read(make_writable_scatter_bytes(p_data_out...)); +} +} // namespace hal::v5 diff --git a/v5/include/libhal-util/usb/enumerator.hpp b/v5/include/libhal-util/usb/enumerator.hpp new file mode 100644 index 0000000..4077ee9 --- /dev/null +++ b/v5/include/libhal-util/usb/enumerator.hpp @@ -0,0 +1,599 @@ +// Copyright 2024 - 2025 Khalil Estell and the libhal contributors +// +// 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "descriptors.hpp" +#include "libhal-util/as_bytes.hpp" +#include "libhal-util/usb/endpoints.hpp" +#include "utils.hpp" + +namespace hal::v5::usb { + +template +constexpr size_t scatter_span_size(scatter_span ss) +{ + size_t res = 0; + for (auto const& s : ss) { + res += s.size(); + } + + return res; +} + +/** + * @brief Result of make_sub_scatter_array containing the scatter span array + * and the number of valid spans within it. + */ +template +struct sub_scatter_result +{ + // Array of spans composing the sub scatter span + std::array, N> spans; + // Number of valid spans in the array (may be less than N if truncated) + size_t count; +}; + +// TODO: Create proper scatter span data structure and remove this +/** + * @brief Create a sub scatter span from span fragments. Meaning only create a + * composite scatter span of a desired length instead of being composed of every + * span. + * + * eg: + * first_span = {1, 2, 3} + * second_span = {4, 5, 6, 7} + * new_sub_span = make_scatter_span_array(5, first_span, second_span) => {1, 2, + * 3, 4, 5} + * + * Unfortunately, it is not as clean as the above psuedo code. In reality + * this function returns the required spans of a given sub scatter span and the + * number of required spans. As of this time starting location is always the + * start of the first span given + * + * Usage: + * @code{.cpp} + * std::array first = {1, 2, 3}; + * std::array second = {4, 5, 6, 7}; + * std::array third = {8, 9}; + * + * // Request only 5 bytes from 9 total + * auto result = make_sub_scatter_bytes(5, first, second, third); + * + * // result.spans -> Spans in sub scatter span + * // result.count -> Number of scatter spans needed to account for desired + * elements + * + * // Use count to limit the scatter_span to valid spans only + * auto ss = scatter_span(result.spans).first(result.count); + * @endcode + * + * @tparam T - The type each span contains + * @tparam Args... - The spans to compose the scatter span + * + * @param p_count - Number of elements (of type T) desired for scatter span + * @param p_spans - The spans that will be used to compose the new sub scatter + * span. + * + * @return A sub_scatter_result containing the spans used within the + * scatter_span and the number of spans in the sub scatter span. + */ +template +constexpr sub_scatter_result make_sub_scatter_array( + size_t p_count, + Args&&... p_spans) +{ + std::array, sizeof...(Args)> full_ss{ std::span( + std::forward(p_spans))... }; + + size_t total_span_len = scatter_span_size(scatter_span(full_ss)); + std::array, sizeof...(Args)> res; + std::array lens{ std::span(p_spans).size()... }; + + if (total_span_len <= p_count) { + return { .spans = full_ss, .count = full_ss.size() }; + } + size_t cur_len = 0; + size_t i = 0; + for (; i < lens.size(); i++) { + auto ith_span_length = lens[i]; + + if (p_count >= (cur_len + ith_span_length)) { + res[i] = full_ss[i]; + cur_len += ith_span_length; + continue; + } + + if (cur_len >= p_count) { + return { .spans = res, .count = i }; + } + + auto delta = p_count - cur_len; + std::span subspan = std::span(full_ss[i]).first(delta); + res[i] = subspan; + break; + } + + return { .spans = res, .count = i + 1 }; +} + +/** + * @brief Convenience wrapper for make_sub_scatter_array with byte const type. + * @see make_sub_scatter_array + */ +template +constexpr sub_scatter_result +make_sub_scatter_bytes(size_t p_count, Args&&... p_spans) +{ + return make_sub_scatter_array(p_count, + std::forward(p_spans)...); +} + +template +class enumerator +{ + +public: + struct args + { + strong_ptr ctrl_ep; + strong_ptr device; + strong_ptr> configs; + u16 lang_str; + }; + + enumerator(args p_args) + : m_ctrl_ep(p_args.ctrl_ep) + , m_device(p_args.device) + , m_configs(p_args.configs) + , m_lang_str(p_args.lang_str) + { + } + + void enumerate() + { + // Renumerate, a config will only be set if + if (m_active_conf != nullptr) { + m_active_conf = nullptr; + m_ctrl_ep->connect(false); + } + + // String indexes 1-3 are reserved for device descriptor strings + // (manufacturer, product, serial number). Configuration strings start at 4. + u8 cur_str_idx = 4; + byte cur_iface_idx = 0; + // Phase one: Preperation + + // Device + m_device->set_num_configurations(num_configs); + + // Configurations + for (size_t i = 0; i < num_configs; i++) { + configuration& config = m_configs->at(i); + config.set_configuration_index(cur_str_idx++); + config.set_configuration_value(i + 1); + } + + for (configuration& config : *m_configs) { + auto total_length = + static_cast(constants::configuration_descriptor_size); + for (auto const& iface : config.interfaces) { + auto deltas = iface->write_descriptors( + { .interface = cur_iface_idx, .string = cur_str_idx }, + [&total_length](scatter_span p_data) { + total_length += scatter_span_size(p_data); + }); + + cur_iface_idx += deltas.interface; + cur_str_idx += deltas.string; + } + config.set_num_interfaces(cur_iface_idx); + config.set_total_length(total_length); + } + + // Phase two: Writing + + // TODO: Make async + bool finished_enumeration = false; + bool volatile waiting_for_data = true; + + using on_receive_tag = control_endpoint::on_receive_tag; + + m_ctrl_ep->on_receive( + [&waiting_for_data](on_receive_tag) { waiting_for_data = false; }); + m_ctrl_ep->connect(true); + + // std::array raw_req; + setup_packet req; + do { + // Seriously, make this async + while (waiting_for_data) { + continue; + } + waiting_for_data = true; + + auto scatter_raw_req = make_writable_scatter_bytes(req.raw_request_bytes); + auto num_bytes_read = m_ctrl_ep->read(scatter_raw_req); + + if (num_bytes_read == 0) { + continue; + } + + if (num_bytes_read != constants::standard_request_size) { + + safe_throw(hal::message_size(num_bytes_read, this)); + } + // + // for (auto const& el : raw_req) { + // + // } + // + + if (req.get_recipient() != setup_packet::request_recipient::device) { + safe_throw(hal::not_connected(this)); + } + + // TODO: Handle exception + handle_standard_device_request(req); + // m_ctrl_ep->write({}); // Send ZLP to complete Data + // Transaction + if (static_cast(req.request()) == + standard_request_types::set_configuration) { + finished_enumeration = true; + m_ctrl_ep->on_receive( + [this](on_receive_tag) { m_has_setup_packet = true; }); + } + } while (!finished_enumeration); + } + + [[nodiscard]] configuration& get_active_configuration() + { + if (m_active_conf == nullptr) { + safe_throw(hal::operation_not_permitted(this)); + } + + return *m_active_conf; + } + + void resume_ctrl_transaction() + { + while (!m_has_setup_packet) { + continue; + } + + m_has_setup_packet = false; + + setup_packet req; + auto& read_buf = req.raw_request_bytes; + auto scatter_read_buf = make_writable_scatter_bytes(read_buf); + auto bytes_read = m_ctrl_ep->read(scatter_read_buf); + std::span payload(read_buf.data(), bytes_read); + + if (req.get_type() == setup_packet::request_type::invalid) { + return; + } + + if (determine_standard_request(req) == + standard_request_types::get_descriptor && + static_cast(req.value() >> 8 & 0xFF) == + descriptor_type::string) { + handle_str_descriptors(req.value() & 0xFF, req.length() > 2); + + } else if (req.get_recipient() == setup_packet::request_recipient::device) { + handle_standard_device_request(req); + } else { + // Handle iface level requests + interface::endpoint_writer f; + if (req.is_device_to_host()) { + f = [this](scatter_span resp) { + m_ctrl_ep->write(resp); + }; + } else { + f = [this](scatter_span resp) { + std::ignore = m_ctrl_ep->read( + resp); // Can't use this... TODO: Maybe add a return for callbacks + // for "bytes processed" + }; + } + bool req_handled = false; + for (auto const& iface : get_active_configuration()) { + req_handled = iface->handle_request(req, f); + if (req_handled) { + break; + } + } + m_ctrl_ep->write( + {}); // A ZLP to terminate Data Transaction just to be safe + + if (!req_handled) { + safe_throw(hal::argument_out_of_domain(this)); + } + } + } + +private: + void handle_standard_device_request(setup_packet& req) + { + switch (determine_standard_request(req)) { + case standard_request_types::set_address: { + m_ctrl_ep->write({}); + m_ctrl_ep->set_address(req.value()); + + break; + } + + case standard_request_types::get_descriptor: { + process_get_descriptor(req); + break; + } + + case standard_request_types::get_configuration: { + if (m_active_conf == nullptr) { + safe_throw(hal::operation_not_permitted(this)); + } + auto conf_value = m_active_conf->configuration_value(); + auto scatter_conf = make_scatter_bytes(std::span(&conf_value, 1)); + m_ctrl_ep->write(scatter_conf); + break; + } + + case standard_request_types::set_configuration: { + m_active_conf = &(m_configs->at(req.value() - 1)); + break; + } + + case standard_request_types::invalid: + default: + safe_throw(hal::not_connected(this)); + } + } + + void process_get_descriptor(setup_packet& req) + { + hal::byte desc_type = req.value() >> 8 & 0xFF; + [[maybe_unused]] hal::byte desc_idx = req.value() & 0xFF; + + switch (static_cast(desc_type)) { + case descriptor_type::device: { + auto header = + std::to_array({ constants::device_descriptor_size, + static_cast(descriptor_type::device) }); + m_device->set_max_packet_size( + static_cast(m_ctrl_ep->info().size)); + auto scatter_arr_pair = + make_sub_scatter_bytes(req.length(), header, *m_device); + hal::v5::write_and_flush( + *m_ctrl_ep, + scatter_span(scatter_arr_pair.spans) + .first(scatter_arr_pair.count)); + + break; + } + + case descriptor_type::configuration: { + configuration& conf = m_configs->at(desc_idx); + auto conf_hdr = + std::to_array({ constants::configuration_descriptor_size, + static_cast(descriptor_type::configuration) }); + auto scatter_conf_pair = make_sub_scatter_bytes( + req.length(), conf_hdr, static_cast>(conf)); + + m_ctrl_ep->write(scatter_span(scatter_conf_pair.spans) + .first(scatter_conf_pair.count)); + + // Return early if the only thing requested was the config descriptor + if (req.length() <= constants::configuration_descriptor_size) { + m_ctrl_ep->write({}); + return; + } + + u16 total_size = constants::configuration_descriptor_size; + for (auto const& iface : conf.interfaces) { + std::ignore = iface->write_descriptors( + { .interface = std::nullopt, .string = std::nullopt }, + [this, &total_size](scatter_span byte_stream) { + m_ctrl_ep->write(byte_stream); + total_size += scatter_span_size(byte_stream); + }); + } + + m_ctrl_ep->write({}); + // if (total_size != req.length()) { + // safe_throw(hal::operation_not_supported( + // this)); // TODO: Make specific exception for this + // } + + break; + } + + case descriptor_type::string: { + if (desc_idx == 0) { + + auto s_hdr = + std::to_array({ static_cast(4), + static_cast(descriptor_type::string) }); + auto lang = setup_packet::to_le_u16(m_lang_str); + auto scatter_arr_pair = make_scatter_bytes(s_hdr, lang); + // auto p = scatter_span(scatter_arr_pair.spans) + // .first(scatter_arr_pair.count); + m_ctrl_ep->write(scatter_arr_pair); + m_ctrl_ep->write({}); + break; + } + handle_str_descriptors(desc_idx, req.length()); // Can throw + break; + } + + // TODO: Interface, endpoint, device_qualifier, interface_power, + // OTHER_SPEED_CONFIGURATION + + default: + safe_throw(hal::operation_not_supported(this)); + } + } + + void handle_str_descriptors(u8 const target_idx, u16 p_len) + { + // Device strings at indexes 1-3, configuration strings start at 4 + constexpr u8 device_str_start = 1; + u8 cfg_string_end = device_str_start + 3 + num_configs; + if (target_idx <= cfg_string_end) { + auto r = write_cfg_str_descriptor(target_idx, p_len); + if (!r) { + safe_throw(hal::argument_out_of_domain(this)); + } + m_iface_for_str_desc = std::nullopt; + return; + } + + if (m_iface_for_str_desc.has_value() && + m_iface_for_str_desc->first == target_idx) { + bool success = m_iface_for_str_desc->second->write_string_descriptor( + target_idx, [this](scatter_span desc) { + hal::v5::write_and_flush(*m_ctrl_ep, desc); + }); + if (success) { + return; + } + } + + // Iterate through every interface now to find a match + auto f = [this, p_len](scatter_span desc) { + if (p_len > 2) { + hal::v5::write_and_flush(*m_ctrl_ep, desc); + } else { + std::array desc_type{ static_cast( + descriptor_type::string) }; + auto scatter_str_hdr = + make_scatter_bytes(std::span(&desc[0][0], 1), desc_type); + hal::v5::write_and_flush(*m_ctrl_ep, scatter_str_hdr); + } + }; + + if (m_active_conf != nullptr) { + for (auto const& iface : m_active_conf->interfaces) { + auto res = iface->write_string_descriptor(target_idx, f); + if (res) { + return; + } + } + } + + for (configuration const& conf : *m_configs) { + for (auto const& iface : conf.interfaces) { + auto res = iface->write_string_descriptor(target_idx, f); + if (res) { + break; + } + } + } + } + + bool write_cfg_str_descriptor(u8 const target_idx, u16 p_len) + { + // Device strings are at fixed indexes: 1=manufacturer, 2=product, 3=serial + constexpr u8 manufacturer_idx = 1; + constexpr u8 product_idx = 2; + constexpr u8 serial_number_idx = 3; + constexpr u8 config_start_idx = 4; + + std::optional opt_conf_sv; + if (target_idx == manufacturer_idx) { + opt_conf_sv = m_device->manufacturer_str; + + } else if (target_idx == product_idx) { + opt_conf_sv = m_device->product_str; + + } else if (target_idx == serial_number_idx) { + opt_conf_sv = m_device->serial_number_str; + + } else { + for (size_t i = 0; i < m_configs->size(); i++) { + configuration const& conf = m_configs->at(i); + if (target_idx == (config_start_idx + i)) { + opt_conf_sv = conf.name; + } + } + } + + if (opt_conf_sv == std::nullopt) { + return false; + } + + // Acceptable to access without checking because guaranteed to be Some, + // there is no pattern matching in C++ yet so unable to do this cleanly + // (would require a check on every single one) + + auto sv = opt_conf_sv.value(); + std::mbstate_t state{}; + for (wchar_t const wc : sv) { + std::array tmp; + size_t len = std::wcrtomb(tmp.data(), wc, &state); + if (len == static_cast(-1)) { + + continue; + } + + for (size_t i = 0; i < len; i++) { + } + } + + auto const conf_sv_span = hal::as_bytes(opt_conf_sv.value()); + auto desc_len = static_cast((conf_sv_span.size() + 2)); + + auto hdr_arr = std::to_array( + { desc_len, static_cast(descriptor_type::string) }); + + auto scatter_arr_pair = + make_sub_scatter_bytes(p_len, hdr_arr, conf_sv_span); + + auto p = scatter_span(scatter_arr_pair.spans) + .first(scatter_arr_pair.count); + hal::v5::write_and_flush(*m_ctrl_ep, p); + + return true; + } + + strong_ptr m_ctrl_ep; + strong_ptr m_device; + strong_ptr> m_configs; + u16 m_lang_str; + + std::optional>> m_iface_for_str_desc; + configuration* m_active_conf = nullptr; + bool m_has_setup_packet = false; +}; +} // namespace hal::v5::usb + +namespace hal::usb { +using v5::usb::enumerator; +} diff --git a/v5/include/libhal-util/usb/utils.hpp b/v5/include/libhal-util/usb/utils.hpp new file mode 100644 index 0000000..74a739c --- /dev/null +++ b/v5/include/libhal-util/usb/utils.hpp @@ -0,0 +1,98 @@ +// Copyright 2024 - 2025 Khalil Estell and the libhal contributors +// +// 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 + +namespace hal::v5::usb { + +namespace constants { + +constexpr byte device_descriptor_size = 18; +constexpr byte configuration_descriptor_size = 9; +constexpr byte inferface_descriptor_size = 9; +constexpr byte endpoint_descriptor_size = 7; +constexpr byte interface_association_descriptor_size = 0x08; + +constexpr byte standard_request_size = 8; + +} // namespace constants + +// Maybe move these enum classes into the constants namespace +// Assigned by USB-IF +enum class class_code : hal::byte +{ + use_interface_descriptor = + 0x00, // Use class information in the Interface Descriptors + audio = 0x01, // Audio device class + cdc_control = 0x02, // Communications and CDC Control + hid = 0x03, // Human Interface Device + physical = 0x05, // Physical device class + image = 0x06, // Still Imaging device + printer = 0x07, // Printer device + mass_storage = 0x08, // Mass Storage device + hub = 0x09, // Hub device + cdc_data = 0x0A, // CDC-Data device + smart_card = 0x0B, // Smart Card device + content_security = 0x0D, // Content Security device + video = 0x0E, // Video device + personal_healthcare = 0x0F, // Personal Healthcare device + audio_video = 0x10, // Audio/Video Devices + billboard = 0x11, // Billboard Device Class + usb_c_bridge = 0x12, // USB Type-C Bridge Class + bulk_display = 0x13, // USB Bulk Display Protocol Device Class + mctp = 0x14, // MCTP over USB Protocol Endpoint Device Class + i3c = 0x3C, // I3C Device Class + diagnostic = 0xDC, // Diagnostic Device + wireless_controller = 0xE0, // Wireless Controller + misc = 0xEF, // Miscellaneous + application_specific = 0xFE, // Application Specific + vendor_specific = 0xFF // Vendor Specific +}; + +// Default types +enum class descriptor_type : hal::byte +{ + device = 0x1, + configuration = 0x2, + string = 0x3, + interface = 0x4, + endpoint = 0x5, + device_qualifier = 0x6, + other_speed_configuration = 0x7, + interface_power = 0x8, + otg = 0x9, + debug = 0xA, + interface_association = 0xB, + security = 0xC, + key = 0xD, + encryption_type = 0xE, + bos = 0xF, + device_capability = 0x10, + wireless_endpoint_companion = 0x11, + superspeed_endpoint_companion = 0x30, + superspeed_endpoint_isochronous_companion = 0x31, +}; + +} // namespace hal::v5::usb + +namespace hal::usb { +namespace constants = v5::usb::constants; +using v5::usb::class_code; +using v5::usb::descriptor_type; + +} // namespace hal::usb diff --git a/v5/tests/usb.test.cpp b/v5/tests/usb.test.cpp index 5206c46..55e3987 100644 --- a/v5/tests/usb.test.cpp +++ b/v5/tests/usb.test.cpp @@ -12,12 +12,267 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include -namespace hal { +namespace hal::v5::usb { + boost::ut::suite<"usb_test"> usb_test = [] { // TODO(#78): Add usb utility tests }; -} // namespace hal + +boost::ut::suite<"enumeration_test"> enumeration_test = [] { + using namespace boost::ut; + using namespace hal::literals; + namespace pmr = std::pmr; + static std::array iface_buf; + + iface_buf.fill(0); + device dev({ .bcd_usb = 0x0002, + .device_class = class_code::application_specific, + .device_subclass = 0, + .device_protocol = 0, + .id_vendor = 0xffff, + .id_product = 0x0000, + .bcd_device = 0x0001, + .p_manufacturer = u"libhal", + .p_product = u"Unit Test", + .p_serial_number_str = u"123456789" }); + + pmr::monotonic_buffer_resource pool(iface_buf.data(), std::size(iface_buf)); + std::array conf_arr{ configuration{ + { .name = u"Test Config", + .attributes = configuration::bitmap(true, false), + .max_power = 1, + .allocator = &pool }, + make_strong_ptr(&pool, mock(u"Mock Iface")) } }; + + mock_usb_control_endpoint ctrl_ep; + ctrl_ep.m_endpoint.m_info = { .size = 8, .number = 0, .stalled = false }; + auto ctrl_ptr = make_strong_ptr(&pool, ctrl_ep); + auto const conf_arr_ptr = + make_strong_ptr>(&pool, conf_arr); + auto dev_ptr = make_strong_ptr(&pool, dev); + auto console_ptr = make_strong_ptr(&pool, mock_serial()); + + "basic usb enumeration test"_test = [&ctrl_ptr, &dev_ptr, &conf_arr_ptr] { + // Start enumeration process and verify connection + std::optional> en; + auto f = [&en, &ctrl_ptr, &dev_ptr, &conf_arr_ptr]() { + en.emplace(enumerator<1>::args{ .ctrl_ep = ctrl_ptr, + .device = dev_ptr, + .configs = conf_arr_ptr, + .lang_str = 0x0409 }); + en->enumerate(); + }; + constexpr byte delay_time_ms = 1000; + auto& ctrl_buf = ctrl_ptr->m_out_buf; + std::thread ejh(f); + std::this_thread::sleep_for(std::chrono::milliseconds( + delay_time_ms)); // Should be enough time to connect + expect(that % true == ctrl_ptr->m_is_connected); + ctrl_buf.clear(); + + u16 expected_addr = 0x30; + setup_packet set_addr{ + { .device_to_host = false, + .type = setup_packet::request_type::standard, + .recipient = setup_packet::request_recipient::device, + .request = static_cast(standard_request_types::set_address), + .value = 0x30, + .index = 0, // a + .length = 0 } + }; + + simulate_sending_payload(ctrl_ptr, set_addr); + ctrl_ptr->simulate_interrupt(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); + expect(that % expected_addr == ctrl_ptr->m_address); + ctrl_buf.clear(); + + // Get device descriptor + u16 desc_t_idx = static_cast(descriptor_type::device) << 8; + setup_packet get_desc( + { .device_to_host = true, + .type = setup_packet::request_type::standard, + .recipient = setup_packet::request_recipient::device, + .request = static_cast(standard_request_types::get_descriptor), + .value = desc_t_idx, + .index = 0, + .length = 18 }); + simulate_sending_payload(ctrl_ptr, get_desc); + ctrl_ptr->simulate_interrupt(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); + std::span dev_actual(ctrl_buf.data(), 18); + std::array dev_expected{ + 0x12, // length + static_cast(descriptor_type::device), // type + 0x02, // usb bcd + 0x00, + static_cast(class_code::application_specific), + 0, // subclass + 0, // protocol + 8, // max packet size + 0xff, // vendor id + 0xff, + 0, // product id + 0, + 0x1, // bcd device + 0x0, + 1, // manufactures index + 2, // product index + 3, // product index + 1 // num configuration + }; + + expect(std::ranges::equal(std::span(dev_expected), dev_actual)); + ctrl_buf.clear(); + + // Get a string descriptor header from device + // where 1 is the manufacture string index + constexpr u16 str_desc_t_idx = + static_cast(descriptor_type::string) << 8 | 1; + setup_packet str_hdr_req( + { .device_to_host = true, + .type = setup_packet::request_type::standard, + .recipient = setup_packet::request_recipient::device, + .request = static_cast(standard_request_types::get_descriptor), + .value = str_desc_t_idx, + .index = 0, + .length = 2 }); + + simulate_sending_payload(ctrl_ptr, str_hdr_req); + ctrl_ptr->simulate_interrupt(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); + std::array expected_manu_str_hdr{ + static_cast(14), // string is "libhal" + static_cast(descriptor_type::string) + }; + std::span actual_dev_str_hdr(ctrl_buf.data(), 2); + expect(std::ranges::equal(std::span(expected_manu_str_hdr), + actual_dev_str_hdr)); + ctrl_buf.clear(); + + // Get a string descriptor from device + setup_packet str_req(str_hdr_req); + str_req.length(expected_manu_str_hdr[0]); + simulate_sending_payload(ctrl_ptr, str_req); + ctrl_ptr->simulate_interrupt(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); + std::u16string_view expected_manu_str = u"libhal"; + + auto expected_manu_str_scatter = + make_scatter_bytes(expected_manu_str_hdr, + std::span(reinterpret_cast( + expected_manu_str.data()), + expected_manu_str.length() * 2)); + auto actual_manu_str_scatter = make_scatter_bytes(ctrl_buf); + expect(that % (scatter_span(expected_manu_str_scatter) == + scatter_span(actual_manu_str_scatter))); + ctrl_buf.clear(); + + // Get Configuration length + setup_packet conf_hdr_req( + { .device_to_host = true, + .type = setup_packet::request_type::standard, + .recipient = setup_packet::request_recipient::device, + .request = static_cast(standard_request_types::get_descriptor), + .value = static_cast(descriptor_type::configuration) << 8, + .index = 0, + .length = 9 }); + + // Expected Config + interface descriptor + std::array expected_conf_iface_desc{ + // config descriptor + 0x9, // len + static_cast(descriptor_type::configuration), // type + 0x12, // HH: total length + 0x0, // LL: tl + 0x1, // number of interfaces + 0x1, // config value + 0x4, // name string index + 0xc0, // bmattributes (selfpowered = true) + 0x1, // max power + + // Interface descriptor + interface_description_length, + interface_description_type, + 0x0, // iface number + 0x0, // alt settings + 0x1, // number of endpoints + 0x2, // class + 0x3, // subclass + 0x4, // protocol + 0x5 // iface name string index + }; + + // Get Configuration descriptor header + simulate_sending_payload(ctrl_ptr, conf_hdr_req); + ctrl_ptr->simulate_interrupt(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); + + auto expected_tl_hh = expected_conf_iface_desc[2]; + auto expected_tl_ll = expected_conf_iface_desc[3]; + auto expected_total_len = + setup_packet::from_le_bytes(expected_tl_hh, expected_tl_ll); + + auto actual_tl_hh = ctrl_buf[2]; + auto actual_tl_ll = ctrl_buf[3]; + auto actual_total_len = + setup_packet::from_le_bytes(actual_tl_hh, actual_tl_ll); + expect(that % expected_total_len == actual_total_len); + ctrl_buf.clear(); + + // Get Configuration Descriptor + interface descriptor + endpoint descriptor + setup_packet conf_req(conf_hdr_req); + conf_req.length(expected_total_len); + simulate_sending_payload(ctrl_ptr, conf_req); + ctrl_ptr->simulate_interrupt(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); + expect(std::ranges::equal(std::span(expected_conf_iface_desc), + std::span(ctrl_buf))); + + // Set configuration + setup_packet set_conf_req( + { .device_to_host = false, + .type = setup_packet::request_type::standard, + .recipient = setup_packet::request_recipient::device, + .request = + static_cast(standard_request_types::set_configuration), + .value = 1, + .index = 0, + .length = 0 }); + + simulate_sending_payload(ctrl_ptr, set_conf_req); + ctrl_ptr->simulate_interrupt(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); + + ejh.join(); + // Verify active config + expect(std::ranges::equal( + std::span(expected_conf_iface_desc.data() + 2, 7), + std::span(en->get_active_configuration()))); + }; +}; + +} // namespace hal::v5::usb