// MT76 chip protocol (MediaTek MT76xx radio in the dongle) // Port of medusalix/xone transport/mt76.c (register + EFUSE layer). #include "mt76/mt76.hpp" #include #include #include #include #include #include #include #include "common/log.hpp" #include "common/types.hpp" #include "mt76/mt76_defs.hpp" #include "usb/usb_transport.hpp" namespace xone::mt76 { // Poll retry count (port of XONE_MT_POLL_RETRIES). constexpr int poll_retries = 50; // Build a command message (port of xone_mt76_prep_message): // [u32 header][payload rounded up to u32][zero pad + 4-byte trailer] auto build_message(std::uint32_t info, void const *payload, std::size_t payload_len) -> std::vector { auto rounded = (payload_len + 3) & ~std::size_t{3}; auto buf = std::vector(rounded + 2 * cmd_hdr_len, 0); xone::store_le32(buf.data(), info | field_prep(mt_mcu_msg_len, rounded)); if (payload_len > 0) std::memcpy(buf.data() + cmd_hdr_len, payload, payload_len); return buf; } // Read a firmware file into memory (port of request_firmware). auto read_firmware_file(char const *path) -> std::optional> { std::ifstream in(path, std::ios::binary | std::ios::ate); if (!in) return std::nullopt; auto size = in.tellg(); if (size <= 0) return std::nullopt; in.seekg(0); std::vector buf(static_cast(size)); if (!in.read(reinterpret_cast(buf.data()), size)) return std::nullopt; return buf; } chip::chip(usb::transport &transport) : transport_(transport) {} auto chip::read_register(std::uint32_t addr) -> std::uint32_t { auto req = usb::vendor_request::multi_read; if (addr & mt_vend_type_cfg) { req = usb::vendor_request::read_cfg; addr &= ~mt_vend_type_cfg; } std::uint8_t buf[4] = {}; int ret = transport_.send_vendor_request(req, true, static_cast(addr >> 16), static_cast(addr & 0xFFFF), buf, sizeof(buf)); if (ret != static_cast(sizeof(buf))) { xone::log_msg(log_level::error, "mt76: register read 0x%04x failed (%d)", addr, ret); return 0; } return xone::load_le32(buf); } auto chip::write_register(std::uint32_t addr, std::uint32_t val) -> void { auto req = usb::vendor_request::multi_write; if (addr & mt_vend_type_cfg) { req = usb::vendor_request::write_cfg; addr &= ~mt_vend_type_cfg; } std::uint8_t buf[4]; xone::store_le32(buf, val); int ret = transport_.send_vendor_request(req, false, static_cast(addr >> 16), static_cast(addr & 0xFFFF), buf, sizeof(buf)); if (ret != static_cast(sizeof(buf))) xone::log_msg(log_level::error, "mt76: register write 0x%04x failed (%d)", addr, ret); } auto chip::poll(std::uint32_t offset, std::uint32_t mask, std::uint32_t val) -> bool { for (int i = 0; i < poll_retries; i++) { auto reg = read_register(offset); if ((reg & mask) == val) return true; usleep(10000); // upstream: usleep_range(10000, 20000) } return false; } auto chip::read_efuse(std::uint16_t addr, void *data, std::size_t len) -> int { auto ctrl = read_register(mt_efuse_ctrl); ctrl &= ~(mt_efuse_ctrl_ain | mt_efuse_ctrl_mode); ctrl |= mt_efuse_ctrl_kick; ctrl |= field_prep(mt_efuse_ctrl_ain, static_cast(addr) & ~0x0Fu); // efuse_read (mode 0) leaves the MODE bits cleared. write_register(mt_efuse_ctrl, ctrl); if (!poll(mt_efuse_ctrl, mt_efuse_ctrl_kick, 0)) return -ETIMEDOUT; for (std::size_t i = 0; i < len; i += sizeof(std::uint32_t)) { // Block data offset (multiple of 32 bits) auto offset = static_cast((addr & genmask(3, 2)) + i); std::uint8_t buf[4]; xone::store_le32(buf, read_register(mt_efuse_data_base + offset)); std::memcpy(static_cast(data) + i, buf, std::min(len - i, sizeof(std::uint32_t))); } return 0; } auto chip::chip_id() -> std::uint16_t { std::uint8_t id[4] = {}; if (read_efuse(mt_ee_chip_id, id, sizeof(id)) != 0) return 0; return static_cast((id[1] << 8) | id[2]); } auto chip::mac_address() -> std::array { auto address = std::array{}; if (read_efuse(mt_ee_mac_addr, address.data(), address.size()) != 0) return address; // Some addresses start with 6c:5d:3a. // Clients only connect to 62:45:bd:xx:xx:xx. if (address[0] != 0x62) { address[0] = 0x62; address[1] = 0x45; address[2] = 0xbd; } return address; } auto chip::send_command(std::uint32_t cmd, void const *payload, std::size_t payload_len) -> int { auto info = mt_mcu_msg_type_cmd | field_prep(mt_mcu_msg_port, dma_msg_port::cpu_tx_port) | field_prep(mt_mcu_msg_cmd_type, cmd); auto buf = build_message(info, payload, payload_len); auto ret = transport_.bulk_write(buf.data(), buf.size()); return ret < 0 ? ret : 0; } auto chip::load_ivb() -> int { return transport_.send_vendor_request(usb::vendor_request::dev_mode, false, static_cast(fw_load_ivb), 0, nullptr, 0); } auto chip::send_firmware_part(std::uint32_t offset, void const *data, std::size_t len) -> int { for (std::size_t pos = 0; pos < len; pos += fw_chunk_size) { auto chunk_len = std::min(len - pos, fw_chunk_size); auto rounded = (chunk_len + 3) & ~std::size_t{3}; write_register(mt_fce_dma_addr | mt_vend_type_cfg, offset + pos); write_register(mt_fce_dma_len | mt_vend_type_cfg, static_cast(rounded) << 16); if (auto ret = send_command(0, static_cast(data) + pos, chunk_len); ret < 0) return ret; auto complete = 0xc0000000u | (static_cast(rounded) << 16); if (!poll(mt_fce_dma_len | mt_vend_type_cfg, 0xFFFFFFFFu, complete)) return -ETIMEDOUT; } return 0; } auto chip::send_firmware(void const *fw_data, std::size_t fw_size) -> int { if (fw_size < sizeof(fw_header)) return -EINVAL; auto header = static_cast(fw_data); auto ilm_len = xone::load_le32(&header->ilm_len); auto dlm_len = xone::load_le32(&header->dlm_len); if (fw_size != sizeof(fw_header) + ilm_len + dlm_len) return -EINVAL; std::memcpy(build_time_, header->build_time, sizeof(header->build_time)); xone::log_msg(log_level::info, "mt76: firmware build %s", build_time_); // Configure the DMA, enable FCE and packet DMA. write_register(mt_usb_u3dma_cfg | mt_vend_type_cfg, mt_usb_dma_cfg_tx_bulk_en | mt_usb_dma_cfg_rx_bulk_en); write_register(mt_fce_pse_ctrl, 0x01); write_register(mt_tx_cpu_from_fce_base_ptr, 0x00400230); write_register(mt_tx_cpu_from_fce_max_count, 0x01); write_register(mt_tx_cpu_from_fce_cpu_desc_idx, 0x01); write_register(mt_fce_pdma_global_conf, 0x44); write_register(mt_fce_skip_fs, 0x03); auto base = static_cast(fw_data) + sizeof(fw_header); if (auto ret = send_firmware_part(fw_ilm_offset, base, ilm_len); ret != 0) return ret; return send_firmware_part(fw_dlm_offset, base + ilm_len, dlm_len); } auto chip::reset_firmware() -> int { // Apply power-on RF patch. auto val = read_register(xone_mt_rf_patch | mt_vend_type_cfg); write_register(xone_mt_rf_patch | mt_vend_type_cfg, val & ~bit(19)); if (auto err = load_ivb(); err != 0) return err; // Wait for the reset. if (!poll(mt_fce_dma_addr | mt_vend_type_cfg, 0x80000000u, 0x80000000u)) return -ETIMEDOUT; return 0; } auto chip::firmware_build() const -> char const * { return build_time_; } auto chip::load_firmware(char const *path) -> int { // If firmware is already loaded, reset the MCU. if (read_register(mt_fce_dma_addr | mt_vend_type_cfg)) { int err = reset_firmware(); if (err == 0) return 0; // The chip keeps its firmware across a USB reset and does not set // the reset-complete bit on this hardware. If it is still alive // with the firmware running, use the running firmware as-is. std::uint8_t id[4] = {}; if (read_register(mt_fce_dma_addr | mt_vend_type_cfg) & 0x01u && read_efuse(mt_ee_chip_id, id, sizeof(id)) == 0) { xone::log_msg(log_level::warn, "mt76: firmware reset incomplete; using running firmware"); return 0; } return err; } auto fw = read_firmware_file(path); if (!fw.has_value()) { xone::log_msg(log_level::error, "mt76: firmware not found: %s", path); return -ENOENT; } int ret = send_firmware(fw->data(), fw->size()); if (ret != 0) return ret; write_register(mt_fce_dma_addr | mt_vend_type_cfg, 0); // Apply power-on RF patch. auto val = read_register(xone_mt_rf_patch | mt_vend_type_cfg); write_register(xone_mt_rf_patch | mt_vend_type_cfg, val & ~bit(19)); if (auto err = load_ivb(); err != 0) return err; // Wait for the firmware to start. if (!poll(mt_fce_dma_addr | mt_vend_type_cfg, 0x01u, 0x01u)) return -ETIMEDOUT; return 0; } auto chip::set_led_mode(std::uint32_t mode) -> int { std::uint8_t payload[4]; xone::store_le32(payload, mode); return send_command(mcu_cmd::cmd_led_mode_op, payload, sizeof(payload)); } auto chip::select_function(std::uint32_t func, std::uint32_t val) -> int { std::uint8_t payload[8]; xone::store_le32(payload + 0, func); xone::store_le32(payload + 4, val); return send_command(mcu_cmd::cmd_fun_set_op, payload, sizeof(payload)); } auto chip::set_power_mode(power_mode mode) -> int { std::uint8_t payload[4]; xone::store_le32(payload, static_cast(mode)); return send_command(mcu_cmd::cmd_power_saving_op, payload, sizeof(payload)); } auto chip::load_cr(cr_mode mode) -> int { // struct xone_mt76_msg_load_cr {mode, temperature, channel, padding}. std::uint8_t payload[4] = {}; payload[0] = static_cast(mode); return send_command(mcu_cmd::cmd_load_cr, payload, sizeof(payload)); } auto chip::write_burst(std::uint32_t idx, void *data, std::size_t len) -> int { std::vector buf(4 + len); xone::store_le32(buf.data(), idx + mt_mcu_memmap_wlan); // Register offset in memory. std::memcpy(buf.data() + 4, data, len); return send_command(mcu_cmd::cmd_burst_write, buf.data(), buf.size()); } auto chip::send_ms_command(ms_command cmd, void *data, std::size_t len) -> int { std::vector buf(4 + len); xone::store_le32(buf.data(), static_cast(cmd)); std::memcpy(buf.data() + 4, data, len); return send_command(mcu_cmd::cmd_init_gain_op, buf.data(), buf.size()); } auto chip::calibrate(calibration calib, std::uint32_t val) -> int { std::uint8_t payload[8]; xone::store_le32(payload + 0, static_cast(calib)); xone::store_le32(payload + 4, val); return send_command(mcu_cmd::cmd_calibration_op, payload, sizeof(payload)); } auto chip::init_registers() -> void { // Port of xone_mt76_init_registers (transport/mt76.c). write_register(mt_mac_sys_ctrl, mt_mac_sys_ctrl_reset_bbp | mt_mac_sys_ctrl_reset_csr); write_register(mt_usb_dma_cfg, 0); write_register(mt_mac_sys_ctrl, 0); write_register(mt_pwr_pin_cfg, 0); write_register(mt_ldo_ctrl_1, 0x6b006464); write_register(mt_wpdma_glo_cfg, 0x70); write_register(mt_wmm_aifsn, 0x2273); write_register(mt_wmm_cwmin, 0x2344); write_register(mt_wmm_cwmax, 0x34aa); write_register(mt_fce_dma_addr, 0x041200); write_register(mt_tso_ctrl, 0); write_register(mt_pbf_sys_ctrl, 0x080c00); write_register(mt_pbf_tx_max_pcnt, 0x1fbf1f1f); write_register(mt_fce_pse_ctrl, 0x01); write_register(mt_mac_sys_ctrl, mt_mac_sys_ctrl_enable_rx | mt_mac_sys_ctrl_enable_tx); write_register(mt_auto_rsp_cfg, 0x13); write_register(mt_max_len_cfg, 0x3e3fff); write_register(mt_ampdu_max_len_20m1s, 0xfffc9855); write_register(mt_ampdu_max_len_20m2s, 0xff); write_register(mt_bkoff_slot_cfg, 0x0109); write_register(mt_pwr_pin_cfg, 0); write_register(mt_edca_cfg_ac(0), 0x064320); write_register(mt_edca_cfg_ac(1), 0x0a4700); write_register(mt_edca_cfg_ac(2), 0x043238); write_register(mt_edca_cfg_ac(3), 0x03212f); write_register(mt_tx_pin_cfg, 0x150f0f); write_register(mt_tx_sw_cfg0, 0x101001); write_register(mt_tx_sw_cfg1, 0x010000); write_register(mt_txop_ctrl_cfg, 0x10583f); write_register(mt_tx_timeout_cfg, 0x0a0f90); write_register(mt_tx_retry_cfg, 0x47d01f0f); write_register(mt_cck_prot_cfg, 0x03f40003); write_register(mt_ofdm_prot_cfg, 0x03f40003); write_register(mt_mm20_prot_cfg, 0x01742004); write_register(mt_gf20_prot_cfg, 0x01742004); write_register(mt_gf40_prot_cfg, 0x03f42084); write_register(mt_exp_ack_time, 0x2c00dc); write_register(mt_tx_alc_cfg_2, 0x22160a00); write_register(mt_tx_alc_cfg_3, 0x22160a76); write_register(mt_tx_alc_cfg_0, 0x3f3f1818); write_register(mt_tx_alc_cfg_4, 0x0606); write_register(mt_pifs_tx_cfg, 0x060fff); write_register(mt_rx_filtr_cfg, 0x017f17); write_register(mt_legacy_basic_rate, 0x017f); write_register(mt_ht_basic_rate, 0x8003); write_register(mt_pn_pad_mode, 0x02); write_register(mt_txop_hldr_et, 0x02); write_register(mt_tx_prot_cfg6, 0xe3f42004); write_register(mt_tx_prot_cfg7, 0xe3f42084); write_register(mt_tx_prot_cfg8, 0xe3f42104); write_register(mt_dacclk_en_dly_cfg, 0); write_register(mt_rf_pa_mode_adj0, 0xee000000); write_register(mt_rf_pa_mode_adj1, 0xee000000); write_register(mt_tx0_rf_gain_corr, 0x0f3c3c3c); write_register(mt_tx1_rf_gain_corr, 0x0f3c3c3c); write_register(mt_pbf_cfg, 0x1efebcf5); write_register(mt_pause_enable_control1, 0x0a); write_register(mt_rf_bypass_0, 0x7f000000); write_register(mt_rf_setting_0, 0x1a800000); write_register(mt_xifs_time_cfg, 0x33a40e0a); write_register(mt_fce_l2_stuff, 0x03ff0223); write_register(mt_tx_rts_cfg, 0); write_register(mt_beacon_time_cfg, 0x0640); write_register(mt_ext_cca_cfg, 0xf0e4); write_register(mt_ch_time_cfg, 0x015f); } auto chip::calibrate_crystal() -> int { std::uint8_t trim[4] = {}; if (read_efuse(mt_ee_xtal_trim_2, trim, sizeof(trim)) != 0) return -EIO; auto val = static_cast((trim[3] << 8) | trim[2]); auto offset = static_cast(val & genmask(6, 0)); if ((val & 0xFF) == 0xFF) offset = 0; else if (val & bit(7)) offset = -offset; val >>= 8; if (!val || val == 0xFF) { if (read_efuse(mt_ee_xtal_trim_1, trim, sizeof(trim)) != 0) return -EIO; val = static_cast((trim[3] << 8) | trim[2]); val &= 0xFF; if (!val || val == 0xFF) val = 0x14; // Default value } val = static_cast(static_cast(val & genmask(6, 0)) + offset); auto ctrl = read_register(mt_xo_ctrl5 | mt_vend_type_cfg); write_register(mt_xo_ctrl5 | mt_vend_type_cfg, (ctrl & ~mt_xo_ctrl5_c2_val) | (static_cast(val) << 8)); write_register(mt_xo_ctrl6 | mt_vend_type_cfg, mt_xo_ctrl6_c2_ctrl); write_register(mt_cmb_ctrl, 0x0091a7ff); return 0; } auto chip::init_address() -> int { auto address = mac_address(); if (auto err = write_burst(mt_mac_addr_dw0, address.data(), address.size()); err != 0) return err; if (auto err = write_burst(mt_mac_bssid_dw0, address.data(), address.size()); err != 0) return err; return send_ms_command(ms_command::ms_set_mac_address, address.data(), address.size()); } auto chip::set_idle_time() -> int { // Prevent wireless clients from disconnecting when idle. std::uint8_t payload[4]; xone::store_le32(payload, 64); return send_ms_command(ms_command::ms_set_idle_time, payload, sizeof(payload)); } auto chip::calibrate_radio() -> int { // Enable AGC for all antennas. write_register(mt_bbp_agc(0), 0x0000001f); write_register(mt_bbp_agc(1), 0x0000001f); write_register(mt_bbp_agc(2), 0x0000001f); return calibrate(calibration::cal_rc, 0); } auto chip::get_channel_power(channel *chan) -> int { std::uint32_t addr; std::size_t idx; if (chan->bandwidth == phy_bandwidth::bw_20) { addr = mt_ee_tx_power_0_start_2g; idx = 4; } else { // Each group has its own power table. addr = mt_ee_tx_power_0_start_5g + chan->group * tx_power_group_size_5g; idx = 5; } std::uint8_t entry[8] = {}; if (read_efuse(static_cast(addr), entry, sizeof(entry)) != 0) return -EIO; auto target = entry[idx]; auto offset = entry[idx + chan->band]; // Increase or decrease power by the offset (in 0.5 dB steps). if (offset & bit(7)) chan->power = (offset & bit(6)) ? static_cast(target + (offset & genmask(5, 0))) : static_cast(target - (offset & genmask(5, 0))); else chan->power = target; return 0; } auto chip::switch_channel(channel const *chan) -> int { // struct xone_mt76_msg_switch_channel. std::uint8_t msg[20] = {}; msg[0] = chan->index; xone::store_le16(msg + 4, 0x0101); // Select TX and RX stream 1. msg[16] = chan->bandwidth; msg[17] = chan->power; msg[18] = chan->scan ? 1 : 0; return send_command(mcu_cmd::cmd_switch_channel_op, msg, sizeof(msg)); } auto chip::evaluate_channels() -> int { for (std::size_t i = 0; i < num_channels; i++) channels_[i] = channels[i]; for (std::size_t i = 0; i < num_channels; i++) { if (auto err = get_channel_power(&channels_[i]); err != 0) return err; if (auto err = switch_channel(&channels_[i]); err != 0) return err; } // The last channel may not be the best one. current_channel_ = channels_.back(); return 0; } auto chip::init_channels() -> int { // Enable promiscuous mode. write_register(mt_rx_filtr_cfg, 0x014f13); if (auto err = evaluate_channels(); err != 0) return err; // Disable promiscuous mode. write_register(mt_rx_filtr_cfg, 0x017f17); current_channel_.scan = true; if (auto err = switch_channel(¤t_channel_); err != 0) return err; if (auto err = set_power_mode(power_mode::radio_off); err != 0) return err; usleep(50 * 1000); if (auto err = set_power_mode(power_mode::radio_on); err != 0) return err; current_channel_.scan = false; if (auto err = switch_channel(¤t_channel_); err != 0) return err; return set_channel_candidates(); } auto chip::write_beacon(bool pair) -> int { auto address = mac_address(); // Beacon management frame (port of struct ieee80211_mgmt, zero // initialized). Layout: [frame_control][duration][da][sa][bssid] // [seq_ctrl][timestamp][beacon_int][capab_info]. std::uint8_t mgmt[36] = {}; xone::store_le16(mgmt + 0, ieee80211_fc_beacon); // frame_control: MGMT | BEACON std::memcpy(mgmt + 4, broadcast_address, sizeof(broadcast_address)); // da std::memcpy(mgmt + 10, address.data(), address.size()); // sa std::memcpy(mgmt + 16, address.data(), address.size()); // bssid // seq_ctrl and timestamp stay zero; the MAC splices in the TSF. xone::store_le16(mgmt + 32, 100); // beacon_int: default (100 ms) xone::store_le16(mgmt + 34, 0xc631); // capab_info: original // Information element with Microsoft's OUI (00:50:f2). std::uint8_t data[20] = { 0x00, 0x00, 0xdd, 0x10, 0x00, 0x50, 0xf2, 0x11, 0x01, 0x10, 0, 0xa5, 0x30, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; data[10] = pair ? 1 : 0; // struct mt76_txwi (20 bytes, packed). std::uint8_t txwi[20] = {}; xone::store_le16(txwi + 0, bit(3)); // flags: TS xone::store_le16(txwi + 2, field_prep(mt_rxwi_rate_phy, phy_type::phy_ofdm)); // rate txwi[4] = mt_txwi_ack_ctl_nseq; // ack_ctl // wcid, iv, eiv, aid, txstream, ctl2, pktid stay zero. xone::store_le16(txwi + 6, static_cast(sizeof(mgmt) + sizeof(data))); std::vector frame(sizeof(txwi) + sizeof(mgmt) + sizeof(data)); std::memcpy(frame.data(), txwi, sizeof(txwi)); std::memcpy(frame.data() + sizeof(txwi), mgmt, sizeof(mgmt)); std::memcpy(frame.data() + sizeof(txwi) + sizeof(mgmt), data, sizeof(data)); // Program the beacon buffer via an MCU burst write into PBF shared // memory. The vendor register path does not map this region on this // hardware. return write_burst(mt_beacon_base, frame.data(), frame.size()); } auto chip::set_channel_candidates() -> int { // [le32 1][le32 best][le32 count-1][le32 chan]... (skip the best). std::vector buf; auto append = [&](std::uint32_t val) { buf.resize(buf.size() + 4); xone::store_le32(buf.data() + buf.size() - 4, val); }; append(1); append(current_channel_.index); append(num_channels - 1); for (auto const &c : channels_) if (c.index != current_channel_.index) append(c.index); return send_ms_command(ms_command::ms_set_chan_candidates, buf.data(), buf.size()); } auto chip::set_pairing(bool enable) -> int { if (auto err = write_beacon(enable); err != 0) return err; // Enable TSF/TBTT timers, AP mode and beacon transmission. write_register(mt_beacon_time_cfg, mt_beacon_time_cfg_beacon_tx | mt_beacon_time_cfg_tbtt_en | mt_beacon_time_cfg_sync_mode | mt_beacon_time_cfg_timer_en | field_prep(mt_beacon_time_cfg_intval, 0x0640)); return 0; } auto chip::init_radio() -> int { xone::log_msg(log_level::info, "mt76: init radio (id=0x%04x)", chip_id()); if (auto err = select_function(mcu_function::fun_q_select, 1); err != 0) return err; if (auto err = set_power_mode(power_mode::radio_on); err != 0) return err; if (auto err = load_cr(cr_mode::rf_bbp_cr); err != 0) return err; init_registers(); if (auto err = calibrate_crystal(); err != 0) return err; if (auto err = init_address(); err != 0) return err; if (auto err = set_idle_time(); err != 0) return err; if (auto err = calibrate_radio(); err != 0) return err; if (auto err = init_channels(); err != 0) return err; // Mandatory delay after channel change. usleep(1000 * 1000); return set_pairing(false); } auto chip::set_wow_enable(bool enable) -> int { std::uint8_t payload[6]; xone::store_le32(payload + 0, static_cast(wow_feature::wow_enable)); payload[4] = enable ? 1 : 0; payload[5] = current_channel_.index; return send_command(mcu_cmd::cmd_wow_feature, payload, sizeof(payload)); } auto chip::set_wow_traffic(wow_traffic traffic) -> int { std::uint8_t payload[5]; xone::store_le32(payload + 0, static_cast(wow_feature::wow_traffic_op)); payload[4] = static_cast(traffic); return send_command(mcu_cmd::cmd_wow_feature, payload, sizeof(payload)); } auto chip::suspend_radio() -> int { write_register(mt_mac_sys_ctrl, 0); // Enable wake-on-wireless. if (auto err = set_wow_enable(true); err != 0) return err; return set_wow_traffic(wow_traffic::wow_to_host); } auto chip::resume_radio() -> int { if (auto err = set_wow_traffic(wow_traffic::wow_to_firmware); err != 0) return err; // Disable wake-on-wireless. if (auto err = set_wow_enable(false); err != 0) return err; if (auto err = switch_channel(¤t_channel_); err != 0) return err; if (auto err = set_pairing(false); err != 0) return err; write_register(mt_mac_sys_ctrl, mt_mac_sys_ctrl_enable_rx | mt_mac_sys_ctrl_enable_tx); return 0; } } // namespace xone::mt76