| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "qmi-usb-transport.h" |
| |
| #include <endian.h> |
| #include <lib/fidl/llcpp/coding.h> |
| #include <lib/zx/channel.h> |
| #include <stdio.h> |
| #include <zircon/hw/usb/cdc.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls/port.h> |
| #include <zircon/types.h> |
| |
| #include <future> |
| #include <thread> |
| |
| #include <ddk/debug.h> |
| #include <ddktl/fidl.h> |
| |
| #ifndef _ALL_SOURCE |
| #define _ALL_SOURCE |
| #endif |
| #include <threads.h> |
| |
| #define ETHERNET_MAX_TRANSMIT_DELAY 100 |
| #define ETHERNET_MAX_RECV_DELAY 100 |
| #define ETHERNET_TRANSMIT_DELAY 10 |
| #define ETHERNET_RECV_DELAY 10 |
| #define ETHERNET_INITIAL_TRANSMIT_DELAY 0 |
| #define ETHERNET_INITIAL_RECV_DELAY 0 |
| |
| namespace telephony_transport = ::llcpp::fuchsia::hardware::telephony::transport; |
| namespace telephony_snoop = ::llcpp::fuchsia::telephony::snoop; |
| |
| // TODO (jiamingw): investigate whether it can be replaced by eth::Operation |
| typedef struct txn_info { |
| ethernet_netbuf_t netbuf; |
| ethernet_impl_queue_tx_callback completion_cb; |
| void* cookie; |
| list_node_t node; |
| } txn_info_t; |
| |
| static void complete_txn(txn_info_t* txn, zx_status_t status) { |
| txn->completion_cb(txn->cookie, status, &txn->netbuf); |
| } |
| |
| namespace qmi_usb { |
| constexpr uint32_t kMaxTxBufSz = 32768; |
| constexpr uint32_t kMaxRxBufSz = 32768; |
| |
| static void usb_write_complete(void* ctx, usb_request_t* request); |
| |
| zx_status_t Device::SetChannelToDevice(zx_handle_t channel) { |
| zxlogf(INFO, "qmi-usb-transport: getting channel from transport"); |
| zx_status_t result = ZX_OK; |
| |
| if (qmi_channel_ != ZX_HANDLE_INVALID) { |
| zxlogf(ERROR, "qmi-usb-transport: already bound, failing"); |
| result = ZX_ERR_ALREADY_BOUND; |
| } else if (channel == ZX_HANDLE_INVALID) { |
| zxlogf(ERROR, "qmi-usb-transport: invalid channel handle"); |
| result = ZX_ERR_BAD_HANDLE; |
| } else { |
| qmi_channel_ = channel; |
| } |
| return result; |
| } |
| |
| void Device::SetEthDestMacAddr(const uint8_t* mac_addr_ptr) { |
| if (eth_dst_mac_addr_ == nullptr) { |
| eth_dst_mac_addr_ = std::make_unique<std::array<uint8_t, kMacAddrLen>>(); |
| } else { |
| zxlogf(INFO, "qmi-usb-transport: overwriting eth dest mac addr"); |
| } |
| |
| std::copy(mac_addr_ptr, &mac_addr_ptr[kMacAddrLen], eth_dst_mac_addr_.get()->begin()); |
| } |
| |
| zx_status_t Device::HandleArpReq(const ArpFrame& arp_frame) { |
| if (memcmp(kArpReqHdr.data(), &arp_frame.arp_hdr, sizeof(kArpReqHdr)) != 0) { |
| zxlogf(ERROR, "qmi-usb-transport: invalid arp request"); |
| return ZX_ERR_IO_DATA_INTEGRITY; |
| } |
| |
| SetEthDestMacAddr(arp_frame.src_mac_addr); |
| |
| EthArpFrame eth_arp_resp; |
| GenEthArpResp(arp_frame, ð_arp_resp); |
| |
| // TODO (jiamingw): understand the reason to sleep here. |
| zx_nanosleep(zx_deadline_after(ZX_USEC(tx_endpoint_delay_))); |
| fbl::AutoLock lock(ð_mutex_); |
| if (eth_ifc_ptr_ && eth_ifc_ptr_->is_valid()) { |
| // TODO (jiamingw): Log arp response. |
| zxlogf(INFO, "qmi-usb-transport: Replying Arp Msg"); |
| eth_ifc_ptr_->Recv(reinterpret_cast<const uint8_t*>(ð_arp_resp), kArpSize + kEthFrameHdrSize, |
| 0); |
| } |
| |
| return ZX_OK; |
| } |
| |
| void Device::GenEthArpResp(const ArpFrame& req, EthArpFrame* resp) { |
| // TODO (fxbug.dev/40051): Generate fake mac address to support multiple |
| // cellular devices. |
| // Eth header |
| // eth_dst_mac_addr_ is ensured to be not null when calling this method. |
| std::copy(eth_dst_mac_addr_->begin(), eth_dst_mac_addr_->end(), resp->eth_hdr.dst_mac_addr); |
| std::copy(kFakeMacAddr.begin(), kFakeMacAddr.end(), resp->eth_hdr.src_mac_addr); |
| resp->eth_hdr.ethertype = betoh16(kEthertypeArp); |
| // Eth payload. |
| std::copy(kArpRespHdr.begin(), kArpRespHdr.end(), reinterpret_cast<uint8_t*>(&resp->arp.arp_hdr)); |
| std::copy(kFakeMacAddr.begin(), kFakeMacAddr.end(), resp->arp.src_mac_addr); |
| std::copy(req.dst_ip_addr, &req.dst_ip_addr[kIpv4AddrLen], resp->arp.src_ip_addr); |
| std::copy(eth_dst_mac_addr_->begin(), eth_dst_mac_addr_->end(), resp->arp.dst_mac_addr); |
| std::copy(req.src_ip_addr, &req.src_ip_addr[kIpv4AddrLen], resp->arp.dst_ip_addr); |
| } |
| |
| zx_status_t Device::QueueUsbRequestHandler(const uint8_t* ip, size_t length, usb_request_t* req) { |
| auto ip_hdr = reinterpret_cast<const IpPktHdr*>(ip); |
| size_t ip_length = betoh16(ip_hdr->total_length); |
| zxlogf(INFO, "qmi-usb-transport: ip: x%x x%x", ip[0], ip[1]); |
| |
| if (ip_length != length) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: length of IP packet does not match length in eth " |
| "frame! %zd/%zd \n", |
| ip_length, length); |
| return ZX_ERR_IO; |
| } |
| |
| ssize_t bytes_copied = usb_request_copy_to(req, ip, ip_length, 0); |
| if (bytes_copied < 0) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to copy data into send txn (error %zd)", bytes_copied); |
| return ZX_ERR_IO; |
| } |
| |
| if (static_cast<size_t>(bytes_copied) != ip_length) { |
| zxlogf(ERROR, "qmi-usb-transport: expect to copy %zu bytes but only copied %zd", ip_length, |
| bytes_copied); |
| return ZX_ERR_IO; |
| } |
| |
| usb_request_complete_t complete = { |
| .callback = usb_write_complete, |
| .ctx = this, |
| }; |
| req->header.length = bytes_copied; |
| // TODO (jiamingw): logging IP packet ((uintptr_t)req->virt) + req->offset) with length ip_length. |
| zxlogf(INFO, "qmi-usb-transport: tx IP pkt"); |
| usb_request_queue(&usb_, req, &complete); |
| return ZX_OK; |
| } |
| |
| zx_status_t Device::HandleQueueUsbReq(const uint8_t* data, size_t length, usb_request_t* req) { |
| zxlogf(INFO, "qmi-usb-transport: sending data to modem"); |
| zx_status_t res = QueueUsbRequestHandler(data, length, req); |
| if (res != ZX_OK) { |
| eth_tx_stats_.ipv4_tx_dropped_cnt += 1; |
| return res; |
| } |
| eth_tx_stats_.ipv4_tx_succeed_cnt += 1; |
| return ZX_OK; |
| } |
| |
| zx_status_t inline Device::SendLocked(const uint8_t* byte_data, size_t length) { |
| // Make sure that we can get all of the tx buffers we need to use |
| usb_request_t* tx_req = usb_req_list_remove_head(&tx_txn_bufs_, parent_req_size_); |
| if (tx_req == nullptr) { |
| return ZX_ERR_SHOULD_WAIT; |
| } |
| |
| zx_nanosleep(zx_deadline_after(ZX_USEC(tx_endpoint_delay_))); |
| zx_status_t status; |
| if ((status = HandleQueueUsbReq(byte_data, length, tx_req)) != ZX_OK) { |
| // Add packet back to original place if it is not being sent. |
| zx_status_t add_status = usb_req_list_add_head(&tx_txn_bufs_, tx_req, parent_req_size_); |
| ZX_DEBUG_ASSERT(add_status == ZX_OK); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| void Device::QmiUpdateOnlineStatus(bool is_online) { |
| fbl::AutoLock lock(ð_mutex_); |
| if ((is_online && online_) || (!is_online && !online_)) { |
| return; |
| } |
| |
| if (is_online) { |
| zxlogf(INFO, "qmi-usb-transport: connected to network"); |
| online_ = true; |
| if (eth_ifc_ptr_ && eth_ifc_ptr_->is_valid()) { |
| eth_ifc_ptr_->Status(online_ ? ETHERNET_STATUS_ONLINE : 0); |
| } else { |
| zxlogf(ERROR, "qmi-usb-transport: not connected to ethermac interface"); |
| } |
| } else { |
| zxlogf(INFO, "qmi-usb-transport: no connection to network"); |
| online_ = false; |
| if (eth_ifc_ptr_ && eth_ifc_ptr_->is_valid()) { |
| eth_ifc_ptr_->Status(0); |
| } |
| } |
| } |
| |
| zx_status_t Device::SetAsyncWait() { |
| return zx_object_wait_async(qmi_channel_, qmi_channel_port_, CHANNEL_MSG, |
| ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0); |
| } |
| |
| zx_status_t Device::SetSnoopChannelToDevice(zx_handle_t channel) { |
| zx_port_packet_t packet; |
| zx_status_t status = ZX_OK; |
| // Initialize a port to watch whether the other handle of snoop channel has |
| // closed |
| if (snoop_channel_port_ == ZX_HANDLE_INVALID) { |
| status = zx_port_create(0, &snoop_channel_port_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: failed to create a port to watch snoop channel: " |
| "%s\n", |
| zx_status_get_string(status)); |
| return status; |
| } |
| } else { |
| status = zx_port_wait(snoop_channel_port_, 0, &packet); |
| if (status == ZX_ERR_TIMED_OUT) { |
| zxlogf(ERROR, "qmi-usb-transport: timed out: %s", zx_status_get_string(status)); |
| } else if (packet.signal.observed & ZX_CHANNEL_PEER_CLOSED) { |
| zxlogf(INFO, "qmi-usb-transport: snoop channel peer closed"); |
| snoop_channel_ = ZX_HANDLE_INVALID; |
| } |
| } |
| |
| status = ZX_OK; |
| if (snoop_channel_ != ZX_HANDLE_INVALID) { |
| zxlogf(ERROR, "snoop channel already connected"); |
| status = ZX_ERR_ALREADY_BOUND; |
| } else if (channel == ZX_HANDLE_INVALID) { |
| zxlogf(ERROR, "get invalid snoop channel handle"); |
| status = ZX_ERR_BAD_HANDLE; |
| } else { |
| snoop_channel_ = channel; |
| zx_object_wait_async(snoop_channel_, snoop_channel_port_, 0, ZX_CHANNEL_PEER_CLOSED, 0); |
| } |
| return status; |
| } |
| |
| zx_status_t Device::CloseQmiChannel() { |
| zx_status_t ret_val = zx_handle_close(qmi_channel_); |
| qmi_channel_ = ZX_HANDLE_INVALID; |
| return ret_val; |
| } |
| |
| void Device::SetChannel(::zx::channel transport, SetChannelCompleter::Sync& completer) { |
| zx_status_t set_channel_res = SetChannelToDevice(transport.release()); |
| if (set_channel_res == ZX_OK) { |
| completer.ReplySuccess(); |
| zx_status_t status = SetAsyncWait(); |
| if (status != ZX_OK) { |
| CloseQmiChannel(); |
| } |
| } else { |
| completer.ReplyError(set_channel_res); |
| } |
| } |
| |
| void Device::SetNetwork(bool connected, SetNetworkCompleter::Sync& completer) { |
| QmiUpdateOnlineStatus(connected); |
| completer.Reply(); |
| } |
| |
| void Device::SetSnoopChannel(::zx::channel interface, SetSnoopChannelCompleter::Sync& completer) { |
| zx_status_t set_snoop_res = SetSnoopChannelToDevice(interface.release()); |
| if (set_snoop_res == ZX_OK) { |
| completer.ReplySuccess(); |
| } else { |
| completer.ReplyError(static_cast<uint32_t>(set_snoop_res)); |
| } |
| } |
| |
| zx_status_t Device::DdkMessage(fidl_incoming_msg_t* msg, fidl_txn_t* txn) { |
| DdkTransaction transaction(txn); |
| telephony_transport::Qmi::Dispatch(this, msg, &transaction); |
| return transaction.Status(); |
| } |
| |
| void Device::Release() { delete this; } |
| |
| void Device::DdkRelease() { |
| zxlogf(INFO, "qmi-usb-transport: releasing device"); |
| Release(); |
| } |
| |
| void Device::DdkUnbind(ddk::UnbindTxn txn) { txn.Reply(); } |
| |
| uint32_t Device::GetMacAddr(uint8_t* buffer, uint32_t buffer_length) { |
| if (buffer == nullptr) { |
| return 0; |
| } |
| uint32_t copy_length = std::min((uint32_t)sizeof(kFakeMacAddr), buffer_length); |
| std::copy(kFakeMacAddr.begin(), kFakeMacAddr.end(), buffer); |
| return copy_length; |
| } |
| |
| zx_status_t Device::EthernetImplQuery(uint32_t options, ethernet_info_t* info) { |
| zxlogf(INFO, "qmi-usb-transport: %s called", __FUNCTION__); |
| // No options are supported |
| if (options) { |
| zxlogf(ERROR, "qmi-usb-transport: unexpected options to ethernet_query"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| memset(info, 0, sizeof(*info)); |
| info->mtu = kEthMtu; |
| GetMacAddr(info->mac, sizeof(info->mac)); |
| info->netbuf_size = sizeof(txn_info_t); |
| return ZX_OK; |
| } |
| |
| zx_status_t Device::EthernetImplStart(const ethernet_ifc_protocol_t* ifc) { |
| zxlogf(INFO, "qmi-usb-transport: EthernetImplStart called"); |
| fbl::AutoLock lock(ð_mutex_); |
| if (eth_ifc_ptr_ && eth_ifc_ptr_->is_valid()) { |
| return ZX_ERR_ALREADY_BOUND; |
| } |
| eth_ifc_ptr_ = std::make_unique<ddk::EthernetIfcProtocolClient>(ifc); |
| return ZX_OK; |
| } |
| |
| zx_status_t Device::EthernetImplSetParam(uint32_t param, int32_t value, const void* data, |
| size_t data_size) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| void Device::EthernetImplStop() { |
| zxlogf(INFO, "qmi-usb-transport: %s called", __FUNCTION__); |
| fbl::AutoLock lock(ð_mutex_); |
| eth_ifc_ptr_.reset(); |
| } |
| |
| void Device::EthernetImplQueueTx(uint32_t options, ethernet_netbuf_t* netbuf, |
| ethernet_impl_queue_tx_callback completion_cb, void* cookie) { |
| // TODO (jiamingw): Log netbuf->data_buffer with length netbuf->data_size. |
| zxlogf(INFO, "qmi-usb-transport: transmitting outbound data plane msg:"); |
| |
| size_t length = netbuf->data_size; |
| |
| if (length < kEthFrameHdrSize) { |
| zxlogf(ERROR, "qmi-usb-transport: tx eth frame too short (length: %zx) ", length); |
| eth_tx_stats_.eth_dropped_cnt += 1; |
| } |
| size_t eth_payload_len = length - kEthFrameHdrSize; |
| |
| // Check data type. Only Arp or IPv4 packet will be handled. |
| const EthFrameHdr* eth_hdr = static_cast<const EthFrameHdr*>(netbuf->data_buffer); |
| switch (betoh16(eth_hdr->ethertype)) { |
| // Arp request. Send back Arp response. |
| case kEthertypeArp: { |
| eth_tx_stats_.arp_req_cnt += 1; |
| if (eth_payload_len < kArpSize) { |
| zxlogf(ERROR, "qmi-usb-transport: arp req too short"); |
| return; |
| } |
| |
| auto eth_arp = static_cast<const EthArpFrame*>(netbuf->data_buffer); |
| zx_status_t res = HandleArpReq(eth_arp->arp); |
| if (res != ZX_OK) { |
| eth_tx_stats_.arp_dropped_cnt += 1; |
| } |
| break; |
| } |
| // IPv4 packet. Send to modem. |
| case kEthertypeIpv4: { |
| zx_status_t status = ZX_OK; |
| txn_info_t* txn = containerof(netbuf, txn_info_t, netbuf); |
| txn->completion_cb = completion_cb; |
| txn->cookie = cookie; |
| |
| if (length > kEthMtu || length == 0) { |
| complete_txn(txn, ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| |
| { |
| fbl::AutoLock lock(&tx_mutex_); |
| |
| if (device_unbound_) { |
| status = ZX_ERR_IO_NOT_PRESENT; |
| } else { |
| auto eth_ip = static_cast<const EthFrame*>(netbuf->data_buffer); |
| status = SendLocked(eth_ip->eth_payload, eth_payload_len); |
| if (status == ZX_ERR_SHOULD_WAIT) { |
| // No buffers available, queue it up |
| list_add_tail(&tx_pending_infos_, &txn->node); |
| } |
| } |
| } |
| |
| if (status != ZX_ERR_SHOULD_WAIT) { |
| complete_txn(txn, status); |
| } |
| break; |
| } |
| default: |
| zxlogf(ERROR, "qmi-usb-transport: ethertype 0x%x not supported", eth_hdr->ethertype); |
| eth_tx_stats_.eth_dropped_cnt += 1; |
| break; |
| } |
| return; |
| } |
| |
| #define DEV(c) static_cast<Device*>(c) |
| static ethernet_impl_protocol_ops_t ethernet_impl_ops = { |
| .query = [](void* ctx, uint32_t options, ethernet_info_t* info) -> zx_status_t { |
| return DEV(ctx)->EthernetImplQuery(options, info); |
| }, |
| .stop = [](void* ctx) { DEV(ctx)->EthernetImplStop(); }, |
| .start = [](void* ctx, const ethernet_ifc_protocol_t* ifc) -> zx_status_t { |
| return DEV(ctx)->EthernetImplStart(ifc); |
| }, |
| .queue_tx = |
| [](void* ctx, uint32_t options, ethernet_netbuf_t* netbuf, |
| ethernet_impl_queue_tx_callback completion_cb, |
| void* cookie) { DEV(ctx)->EthernetImplQueueTx(options, netbuf, completion_cb, cookie); }, |
| .set_param = [](void* ctx, uint32_t param, int32_t value, const void* data, size_t data_size) |
| -> zx_status_t { return DEV(ctx)->EthernetImplSetParam(param, value, data, data_size); }, |
| }; |
| #undef DEV |
| |
| static zx_protocol_device_t eth_qmi_ops = { |
| .version = DEVICE_OPS_VERSION, |
| }; |
| |
| void Device::QmiInterruptHandler(usb_request_t* request) { |
| zxlogf(INFO, "request->response.actual: %lu", request->response.actual); |
| if (request->response.actual < sizeof(usb_cdc_notification_t)) { |
| zxlogf(ERROR, "qmi-usb-transport: ignored interrupt (size = %ld)", |
| (long)request->response.actual); |
| return; |
| } |
| |
| usb_cdc_notification_t usb_req = {}; |
| __UNUSED auto copy_length = usb_request_copy_from(request, &usb_req, sizeof(usb_cdc_notification_t), 0); |
| |
| // TODO (jiamingw): confirm this check is unnecessary |
| uint16_t packet_size = max_packet_size_; |
| if (packet_size > kUsbCtrlEpMsgSizeMax) { |
| zxlogf(ERROR, "qmi-usb-transport: packet too big: %d", packet_size); |
| return; |
| } |
| |
| switch (usb_req.bNotification) { |
| case USB_CDC_NC_RESPONSE_AVAILABLE: |
| UsbCdcIntHander(packet_size); |
| break; |
| default: |
| zxlogf(ERROR, "qmi-usb-transport: Unknown Notification Type for QMI: %d", |
| usb_req.bNotification); |
| break; |
| } |
| } |
| |
| void Device::UsbCdcIntHander(uint16_t packet_size) { |
| zx_status_t status; |
| uint8_t buffer[packet_size]; |
| |
| status = usb_control_in(&usb_, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE, |
| USB_CDC_GET_ENCAPSULATED_RESPONSE, 0, QMI_INTERFACE_NUM, ZX_TIME_INFINITE, |
| buffer, packet_size, nullptr); |
| if (!qmi_channel_) { |
| zxlogf(WARNING, "qmi-usb-transport: recieving USB CDC frames without a channel"); |
| return; |
| } |
| status = zx_channel_write(qmi_channel_, 0, buffer, sizeof(buffer), nullptr, 0); |
| if (status < 0) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to write message to channel: %s", |
| zx_status_get_string(status)); |
| } |
| if (snoop_channel_) { |
| SnoopQmiMsgSend(buffer, sizeof(buffer), telephony_snoop::Direction::FROM_MODEM); |
| } |
| return; |
| } |
| |
| zx_handle_t Device::GetQmiChannelPort() { return qmi_channel_port_; } |
| |
| void Device::SnoopQmiMsgSend(uint8_t* msg_arr, uint32_t msg_arr_len, |
| telephony_snoop::Direction direction) { |
| telephony_snoop::QmiMessage qmi_msg; |
| uint32_t current_length = std::min(msg_arr_len, (uint32_t)sizeof(qmi_msg.opaque_bytes)); |
| qmi_msg.is_partial_copy = true; // do not know the real length of QMI message for now |
| qmi_msg.direction = direction; |
| qmi_msg.timestamp = zx_clock_get_monotonic(); |
| memcpy(qmi_msg.opaque_bytes.data_, msg_arr, current_length); |
| telephony_snoop::Message snoop_msg = |
| telephony_snoop::Message::WithQmiMessage(fidl::unowned_ptr(&qmi_msg)); |
| telephony_snoop::Publisher::Call::SendMessage(zx::unowned_channel(snoop_channel_), |
| std::move(snoop_msg)); |
| } |
| |
| static void qmi_interrupt_cb(void* ctx, usb_request_t* req) { |
| Device* device = static_cast<Device*>(ctx); |
| |
| zx_port_packet_t packet = {}; |
| packet.key = INTERRUPT_MSG; |
| zx_port_queue(device->GetQmiChannelPort(), &packet); |
| } |
| |
| int Device::EventLoop(void) { |
| usb_request_t* txn = int_txn_buf_; |
| usb_request_complete_t complete = { |
| .callback = qmi_usb::qmi_interrupt_cb, |
| .ctx = this, |
| }; |
| usb_request_queue(&usb_, txn, &complete); |
| zxlogf(INFO, "successfully queued int req"); |
| if (max_packet_size_ > kUsbCtrlEpMsgSizeMax) { |
| zxlogf(ERROR, "qmi-usb-transport: packet too big: %d", max_packet_size_); |
| return ZX_ERR_IO_REFUSED; |
| } |
| uint8_t buffer[max_packet_size_]; |
| uint32_t length = sizeof(buffer); |
| zx_port_packet_t packet; |
| while (true) { |
| zx_status_t status = zx_port_wait(qmi_channel_port_, ZX_TIME_INFINITE, &packet); |
| if (status == ZX_ERR_TIMED_OUT) { |
| zxlogf(ERROR, "qmi-usb-transport: timed out: %s", zx_status_get_string(status)); |
| continue; |
| } |
| if (packet.key == CHANNEL_MSG) { |
| if (packet.signal.observed & ZX_CHANNEL_PEER_CLOSED) { |
| zxlogf(INFO, "qmi-usb-transport: channel closed"); |
| status = CloseQmiChannel(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to close QMI channel: %s", |
| zx_status_get_string(status)); |
| } |
| continue; |
| } |
| status = |
| zx_channel_read(qmi_channel_, 0, buffer, nullptr, sizeof(buffer), 0, &length, nullptr); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to read channel: %s", |
| zx_status_get_string(status)); |
| return status; |
| } |
| status = usb_control_out(&usb_, USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE, |
| USB_CDC_SEND_ENCAPSULATED_COMMAND, 0, 8, ZX_TIME_INFINITE, buffer, |
| length); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: got an bad status from usb_control_out: %s", |
| zx_status_get_string(status)); |
| return status; |
| } |
| status = SetAsyncWait(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| if (snoop_channel_) { |
| SnoopQmiMsgSend(buffer, sizeof(buffer), telephony_snoop::Direction::TO_MODEM); |
| } |
| } else if (packet.key == INTERRUPT_MSG) { |
| if (txn->response.status == ZX_OK) { |
| QmiInterruptHandler(txn); |
| usb_request_complete_t complete = { |
| .callback = qmi_interrupt_cb, |
| .ctx = this, |
| }; |
| usb_request_queue(&usb_, txn, &complete); |
| } else if (txn->response.status == ZX_ERR_PEER_CLOSED || |
| txn->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| zxlogf(INFO, "qmi-usb-transport: terminating interrupt handling thread"); |
| return txn->response.status; |
| } |
| } else { |
| zxlogf(ERROR, "qmi-usb-transport: invalid pkt key"); |
| } |
| } // while(true) |
| } |
| |
| static int qmi_transport_thread(void* ctx) { |
| Device* device_ptr = static_cast<Device*>(ctx); |
| return device_ptr->EventLoop(); |
| } |
| |
| void Device::GenInboundEthFrameHdr(EthFrameHdr* eth_frame_hdr) { |
| // Dest mac addr. |
| std::copy(eth_dst_mac_addr_->begin(), eth_dst_mac_addr_->end(), eth_frame_hdr->dst_mac_addr); |
| // Src mac addr. |
| std::copy(&kFakeMacAddr[0], &kFakeMacAddr[kMacAddrLen], eth_frame_hdr->src_mac_addr); |
| // Ethertype. |
| eth_frame_hdr->ethertype = betoh16(kEthertypeIpv4); |
| return; |
| } |
| |
| // Note: the assumption made here is that no rx transmissions will be processed |
| // in parallel, so we do not maintain an rx mutex. |
| void Device::UsbRecv(usb_request_t* request) { |
| size_t eth_frame_payload_len = request->response.actual; |
| |
| uint8_t* eth_frame_payload; |
| zx_status_t status = usb_request_mmap(request, reinterpret_cast<void**>(ð_frame_payload)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: usb_request_mmap failed with status %d", status); |
| return; |
| } |
| |
| if (eth_frame_payload_len > kUsbBulkInEpMsgSizeMax) { |
| zxlogf(ERROR, "qmi-usb-transport: recieved usb packet is too large: %zd", |
| eth_frame_payload_len); |
| return; |
| } |
| |
| // TODO (jiamingw): Log message eth_frame_payload. |
| zxlogf(INFO, "qmi-usb-transport: getting inbound data plane msg"); |
| |
| if (eth_dst_mac_addr_ == nullptr) { |
| zxlogf(ERROR, "qmi-usb-transport: no eth mac addr, cannot send eth frame"); |
| return; |
| } |
| |
| uint8_t eth_frame_arr[eth_frame_payload_len + kEthFrameHdrSize]; |
| auto eth_ip = reinterpret_cast<EthFrame*>(eth_frame_arr); |
| GenInboundEthFrameHdr(ð_ip->eth_hdr); |
| |
| // Copy ethernet frame payload. |
| std::copy(eth_frame_payload, ð_frame_payload[eth_frame_payload_len], eth_ip->eth_payload); |
| fbl::AutoLock lock(ð_mutex_); |
| if (eth_ifc_ptr_ && eth_ifc_ptr_->is_valid()) { |
| eth_ifc_ptr_->Recv(eth_frame_arr, eth_frame_payload_len + kEthFrameHdrSize, 0); |
| } |
| } |
| |
| static void usb_read_complete(void* ctx, usb_request_t* request) { |
| Device* device_ptr = static_cast<Device*>(ctx); |
| device_ptr->UsbReadCompleteHandler(request); |
| } |
| |
| void Device::UsbReadCompleteHandler(usb_request_t* request) { |
| if (request->response.status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: usb_read_complete called with status %d", |
| (int)request->response.status); |
| } |
| |
| if (request->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| usb_request_release(request); |
| return; |
| } |
| |
| if (request->response.status == ZX_ERR_IO_REFUSED) { |
| zxlogf(ERROR, "qmi-usb-transport: resetting receive endpoint"); |
| usb_reset_endpoint(&usb_, rx_endpoint_addr_); |
| } else if (request->response.status == ZX_ERR_IO_INVALID) { |
| if (rx_endpoint_delay_ < ETHERNET_MAX_RECV_DELAY) { |
| rx_endpoint_delay_ += ETHERNET_RECV_DELAY; |
| } |
| zxlogf(ERROR, |
| "qmi-usb-transport: slowing down the requests by %d usec." |
| "Resetting the recv endpoint\n", |
| ETHERNET_RECV_DELAY); |
| usb_reset_endpoint(&usb_, rx_endpoint_addr_); |
| } else if (request->response.status == ZX_OK) { |
| UsbRecv(request); |
| } |
| |
| zx_nanosleep(zx_deadline_after(ZX_USEC(rx_endpoint_delay_))); |
| |
| usb_request_complete_t complete = { |
| .callback = qmi_usb::usb_read_complete, |
| .ctx = this, |
| }; |
| usb_request_queue(&usb_, request, &complete); |
| } |
| |
| void Device::UsbWriteCompleteHandler(usb_request_t* request) { |
| if (request->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| usb_request_release(request); |
| return; |
| } |
| |
| // Return transmission buffer to pool |
| fbl::AutoLock tx_lock(&tx_mutex_); |
| |
| zx_status_t status = usb_req_list_add_tail(&tx_txn_bufs_, request, parent_req_size_); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| |
| if (request->response.status == ZX_ERR_IO_REFUSED) { |
| zxlogf(ERROR, "qmi-usb-transport: resetting transmit endpoint"); |
| usb_reset_endpoint(&usb_, tx_endpoint_addr_); |
| } |
| |
| if (request->response.status == ZX_ERR_IO_INVALID) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: slowing down the requests by %d usec." |
| "Resetting the transmit endpoint\n", |
| ETHERNET_TRANSMIT_DELAY); |
| if (tx_endpoint_delay_ < ETHERNET_MAX_TRANSMIT_DELAY) { |
| tx_endpoint_delay_ += ETHERNET_TRANSMIT_DELAY; |
| } |
| usb_reset_endpoint(&usb_, tx_endpoint_addr_); |
| } |
| |
| bool additional_tx_queued = false; |
| txn_info_t* txn; |
| zx_status_t send_status = ZX_OK; |
| if (!list_is_empty(&tx_pending_infos_)) { |
| txn = list_peek_head_type(&tx_pending_infos_, txn_info_t, node); |
| if ((send_status = |
| SendLocked(&static_cast<const uint8_t*>(txn->netbuf.data_buffer)[kEthFrameHdrSize], |
| (txn->netbuf.data_size - kEthFrameHdrSize)) != ZX_ERR_SHOULD_WAIT)) { |
| list_remove_head(&tx_pending_infos_); |
| additional_tx_queued = true; |
| } |
| } |
| |
| tx_lock.release(); |
| |
| fbl::AutoLock eth_lock(ð_mutex_); |
| if (additional_tx_queued) { |
| complete_txn(txn, send_status); |
| } |
| // When the interface is offline, the transaction will complete with status |
| // set to ZX_ERR_IO_NOT_PRESENT. There's not much we can do except ignore it. |
| } |
| |
| static void usb_write_complete(void* ctx, usb_request_t* request) { |
| Device* device_ptr = static_cast<Device*>(ctx); |
| device_ptr->UsbWriteCompleteHandler(request); |
| } |
| |
| Device::Device(zx_device_t* parent) |
| : ddk::Device<Device, ddk::Unbindable, ddk::Messageable>(parent) { |
| usb_device_ = parent; |
| } |
| |
| void Device::QmiBindFailedNoErr(usb_request_t* int_buf) { |
| if (int_buf) { |
| usb_request_release(int_buf); |
| } |
| } |
| |
| void Device::QmiBindFailedErr(zx_status_t status, usb_request_t* int_buf) { |
| zxlogf(ERROR, "qmi-usb-transport: bind failed: %s", zx_status_get_string(status)); |
| QmiBindFailedNoErr(int_buf); |
| } |
| |
| void Device::EthClientInit(const ethernet_ifc_protocol_t* ifc) { |
| eth_ifc_ptr_.reset(new ddk::EthernetIfcProtocolClient(ifc)); |
| } |
| |
| void Device::EthTxListNodeInit() { |
| list_initialize(&tx_txn_bufs_); |
| list_initialize(&tx_pending_infos_); |
| } |
| |
| zx_status_t Device::Bind() __TA_NO_THREAD_SAFETY_ANALYSIS { |
| zx_status_t status; |
| usb_request_t* int_buf = nullptr; |
| |
| // Set up USB stuff |
| usb_protocol_t usb; |
| status = device_get_protocol(usb_device_, ZX_PROTOCOL_USB, &usb); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: get protocol failed: %s", zx_status_get_string(status)); |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| // Initialize context |
| memcpy(&usb_, &usb, sizeof(usb_)); |
| EthTxListNodeInit(); |
| |
| parent_req_size_ = usb_get_request_size(&usb_); |
| uint64_t req_size = parent_req_size_ + sizeof(usb_req_internal_t); |
| ZX_DEBUG_ASSERT(parent_req_size_ != 0); |
| |
| // find our endpoints |
| usb_desc_iter_t iter; |
| zx_status_t result = usb_desc_iter_init(&usb, &iter); |
| if (result < 0) { |
| status = result; |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| // QMI needs to bind to interface QMI_INTERFACE_NUM on current hardware. |
| // Ignore the others for now. |
| // TODO generic way of describing usb interfaces |
| usb_interface_descriptor_t* intf = usb_desc_iter_next_interface(&iter, true); |
| if (!intf || intf->bInterfaceNumber != QMI_INTERFACE_NUM) { |
| usb_desc_iter_release(&iter); |
| status = ZX_ERR_NOT_SUPPORTED; |
| QmiBindFailedNoErr(int_buf); |
| return status; |
| } |
| |
| if (intf->bNumEndpoints != 3) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: interface does not have the required 3 " |
| "endpoints\n"); |
| usb_desc_iter_release(&iter); |
| status = ZX_ERR_NOT_SUPPORTED; |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| uint8_t bulk_in_addr = 0; |
| uint8_t bulk_out_addr = 0; |
| uint8_t intr_addr = 0; |
| uint16_t intr_max_packet = 0; |
| uint16_t bulk_max_packet = 0; |
| |
| usb_descriptor_header_t* desc; |
| while ((desc = usb_desc_iter_peek(&iter)) != NULL) { |
| if (desc->bDescriptorType == USB_DT_ENDPOINT) { |
| usb_endpoint_descriptor_t* endp = reinterpret_cast<usb_endpoint_descriptor_t*>( |
| usb_desc_iter_get_structure(&iter, sizeof(usb_endpoint_descriptor_t))); |
| if (endp == NULL) { |
| break; |
| } |
| if (usb_ep_direction(endp) == USB_ENDPOINT_OUT) { |
| if (usb_ep_type(endp) == USB_ENDPOINT_BULK) { |
| bulk_out_addr = endp->bEndpointAddress; |
| bulk_max_packet = usb_ep_max_packet(endp); |
| } |
| } else { |
| if (usb_ep_type(endp) == USB_ENDPOINT_BULK) { |
| bulk_in_addr = endp->bEndpointAddress; |
| } else if (usb_ep_type(endp) == USB_ENDPOINT_INTERRUPT) { |
| intr_addr = endp->bEndpointAddress; |
| intr_max_packet = usb_ep_max_packet(endp); |
| } |
| } |
| } |
| usb_desc_iter_advance(&iter); |
| } |
| usb_desc_iter_release(&iter); |
| |
| if (bulk_in_addr == 0 || bulk_out_addr == 0 || intr_addr == 0) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to find one of the usb endpoints"); |
| zxlogf(ERROR, "qmi-usb-transport: bulkIn:%u, bulkOut:%u, intr:%u", bulk_in_addr, bulk_out_addr, |
| intr_addr); |
| status = ZX_ERR_INTERNAL; |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| if (intr_max_packet < 1 || bulk_max_packet < 1) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to find reasonable max packet sizes"); |
| zxlogf(ERROR, "qmi-usb-transport: intr_max_packet:%u, bulk_max_packet:%u", intr_max_packet, |
| bulk_max_packet); |
| status = ZX_ERR_INTERNAL; |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| rx_endpoint_delay_ = ETHERNET_INITIAL_RECV_DELAY; |
| tx_endpoint_delay_ = ETHERNET_INITIAL_TRANSMIT_DELAY; |
| |
| // Reset by selecting default interface followed by data interface. We can't |
| // start queueing transactions until this is complete. |
| usb_set_interface(&usb, 8, 0); |
| |
| // set up interrupt |
| status = usb_request_alloc(&int_buf, intr_max_packet, intr_addr, req_size); |
| max_packet_size_ = bulk_max_packet; |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to allocate for usb request: %s", |
| zx_status_get_string(status)); |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| int_txn_buf_ = int_buf; |
| |
| // create port to watch for interrupts and channel messages |
| status = zx_port_create(0, &qmi_channel_port_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to create a port: %s", zx_status_get_string(status)); |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| rx_endpoint_addr_ = bulk_in_addr; |
| tx_endpoint_addr_ = bulk_out_addr; |
| |
| // Allocate tx transaction buffers |
| // TODO uint16_t tx_buf_sz = qmi_ctx->mtu; |
| uint16_t tx_buf_sz = kEthMtu; |
| size_t tx_buf_remain = kMaxTxBufSz; |
| while (tx_buf_remain >= tx_buf_sz) { |
| usb_request_t* tx_buf; |
| zx_status_t alloc_result = usb_request_alloc(&tx_buf, tx_buf_sz, tx_endpoint_addr_, req_size); |
| if (alloc_result != ZX_OK) { |
| result = alloc_result; |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| // As per the CDC-ECM spec, we need to send a zero-length packet to signify |
| // the end of transmission when the endpoint max packet size is a factor of |
| // the total transmission size |
| tx_buf->header.send_zlp = true; |
| |
| zx_status_t status = usb_req_list_add_head(&tx_txn_bufs_, tx_buf, parent_req_size_); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| tx_buf_remain -= tx_buf_sz; |
| } |
| |
| // Allocate rx transaction buffers |
| // TODO(bwb) get correct buffer sizes from usb |
| uint16_t rx_buf_sz = kEthMtu; |
| size_t rx_buf_remain = kMaxRxBufSz; |
| while (rx_buf_remain >= rx_buf_sz) { |
| usb_request_t* rx_buf; |
| zx_status_t alloc_result = usb_request_alloc(&rx_buf, rx_buf_sz, rx_endpoint_addr_, req_size); |
| if (alloc_result != ZX_OK) { |
| result = alloc_result; |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| usb_request_complete_t complete = { |
| .callback = usb_read_complete, |
| .ctx = this, |
| }; |
| usb_request_queue(&usb_, rx_buf, &complete); |
| rx_buf_remain -= rx_buf_sz; |
| } |
| |
| // Kick off the handler thread |
| int thread_result = |
| thrd_create((thrd_t*)&int_thread_, (thrd_start_t)qmi_transport_thread, (void*)this); |
| |
| if (thread_result != thrd_success) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to create transport thread (%d)", thread_result); |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| status = DdkAdd(ddk::DeviceAddArgs("qmi-usb-transport").set_proto_id(ZX_PROTOCOL_QMI_TRANSPORT)); |
| if (status < 0) { |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| device_add_args_t eth_args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "qmi-cdc-ethernet", |
| .ctx = this, |
| // sibling of qmi transport. Cleanup happens when qmi device unbinds |
| .ops = ð_qmi_ops, |
| .proto_id = ZX_PROTOCOL_ETHERNET_IMPL, |
| .proto_ops = ðernet_impl_ops, |
| }; |
| result = device_add(usb_device_, ð_args, ð_zxdev_); |
| if (result < 0) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to add ethernet device: %d", (int)result); |
| QmiBindFailedErr(status, int_buf); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| } // namespace qmi_usb |
| |
| static zx_status_t qmi_bind(void* ctx, zx_device_t* device) { |
| zx_status_t status = ZX_OK; |
| auto dev = std::make_unique<qmi_usb::Device>(device); |
| status = dev->Bind(); |
| std::printf("%s\n", __func__); |
| if (status != ZX_OK) { |
| std::printf("qmi-usb-transport: could not bind: %d\n", status); |
| } else { |
| dev.release(); |
| } |
| return status; |
| } |
| |
| static zx_driver_ops_t qmi_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = qmi_bind, |
| }; |
| |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(qmi_usb, qmi_driver_ops, "zircon", "0.1", 3) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB_INTERFACE), |
| BI_ABORT_IF(NE, BIND_USB_VID, SIERRA_VID), |
| BI_MATCH_IF(EQ, BIND_USB_PID, EM7565_PID), |
| ZIRCON_DRIVER_END(qmi_usb) |