feat: show live controller input

Route paired controller QoS data through the existing GIP protocol layer and
expose the latest gamepad report through the C API. Add a SwiftUI monitor for
buttons, d-pad, triggers, and stick positions without registering a native HID
device.

Co-Authored-By: openai/gpt-5.6-luna: GIP bridge and live input monitor
This commit is contained in:
portersky
2026-08-29 13:33:32 +02:00
parent 325923751c
commit 9c1b6493d8
7 changed files with 462 additions and 40 deletions
+24 -2
View File
@@ -39,8 +39,8 @@ xone_dongle *xone_open(void);
// Kick off firmware load + radio init on a background thread. Non-blocking: // Kick off firmware load + radio init on a background thread. Non-blocking:
// returns 0 once started, -1 if the session is busy or already ready. If // returns 0 once started, -1 if the session is busy or already ready. If
// firmware_path is NULL, the image is selected per product ID // firmware_path is NULL, the common upstream image
// (firmware/xone_dongle_<pid>.bin). // (firmware/xow_dongle.bin) is used.
int xone_start(xone_dongle *d, const char *firmware_path); int xone_start(xone_dongle *d, const char *firmware_path);
// Close the session and release the dongle. Waits for any in-progress start. // Close the session and release the dongle. Waits for any in-progress start.
@@ -72,6 +72,28 @@ int xone_controller_count(xone_dongle const *d);
// success, -1 if index is out of range. // success, -1 if index is out of range.
int xone_controller_mac(xone_dongle const *d, int index, char *buf, int len); int xone_controller_mac(xone_dongle const *d, int index, char *buf, int len);
// Snapshot of the latest standard Xbox controller input report. Stick values
// are signed 16-bit values; trigger values are unsigned 10-bit values. The
// buttons field uses the standard GIP gamepad bit assignments.
struct xone_controller_state {
uint16_t buttons;
bool guide_down;
bool gip_ready;
bool input_active;
uint16_t trigger_left;
uint16_t trigger_right;
int16_t stick_left_x;
int16_t stick_left_y;
int16_t stick_right_x;
int16_t stick_right_y;
uint32_t sequence;
};
// Copy the latest input state for the controller at index. Returns 0 on
// success, -1 if index is out of range or state is NULL.
int xone_controller_get_state(xone_dongle const *d, int index,
struct xone_controller_state *state);
// Enter or leave pairing mode (beacon pairing flag + LED blink). // Enter or leave pairing mode (beacon pairing flag + LED blink).
void xone_set_pairing(xone_dongle *d, bool enable); void xone_set_pairing(xone_dongle *d, bool enable);
+6
View File
@@ -77,6 +77,12 @@ public:
// then bulk-writes it to EP OUT. // then bulk-writes it to EP OUT.
auto send_wlan(std::span<std::uint8_t const> frame) -> int; auto send_wlan(std::span<std::uint8_t const> frame) -> int;
// Send a GIP payload in an upstream QoS-data frame for one client.
auto send_client_frame(std::uint8_t wcid,
std::span<std::uint8_t const> addr,
std::span<std::uint8_t const> frame,
bool encrypted) -> int;
// Associate a controller: program its WCID MAC, ADD_CLIENT ms_command, // Associate a controller: program its WCID MAC, ADD_CLIENT ms_command,
// and transmit an ASSOC_RESP mgmt frame. // and transmit an ASSOC_RESP mgmt frame.
auto associate_client(std::uint8_t wcid, std::span<std::uint8_t const> addr) auto associate_client(std::uint8_t wcid, std::span<std::uint8_t const> addr)
+3 -1
View File
@@ -458,10 +458,12 @@ constexpr std::uint16_t ieee80211_fctl_ftype = 0x0c;
constexpr std::uint16_t ieee80211_fctl_stype = 0xf0; constexpr std::uint16_t ieee80211_fctl_stype = 0xf0;
constexpr std::uint16_t ieee80211_ftype_mgmt = 0x00; constexpr std::uint16_t ieee80211_ftype_mgmt = 0x00;
constexpr std::uint16_t ieee80211_ftype_data = 0x08; constexpr std::uint16_t ieee80211_ftype_data = 0x08;
constexpr std::uint16_t ieee80211_fctl_from_ds = 0x0200;
constexpr std::uint16_t ieee80211_fctl_protected = 0x4000;
constexpr std::uint16_t ieee80211_stype_assoc_req = 0x00; constexpr std::uint16_t ieee80211_stype_assoc_req = 0x00;
constexpr std::uint16_t ieee80211_stype_assoc_resp = 0x10; constexpr std::uint16_t ieee80211_stype_assoc_resp = 0x10;
constexpr std::uint16_t ieee80211_stype_disassoc = 0xa0; constexpr std::uint16_t ieee80211_stype_disassoc = 0xa0;
constexpr std::uint16_t ieee80211_stype_qos_data = 0x70; constexpr std::uint16_t ieee80211_stype_qos_data = 0x80;
// MGMT subtype for reserved (pairing) frames. // MGMT subtype for reserved (pairing) frames.
constexpr std::uint16_t ieee80211_stype_wlan_reserved = 0x70; constexpr std::uint16_t ieee80211_stype_wlan_reserved = 0x70;
+237 -21
View File
@@ -14,6 +14,7 @@
#include <vector> #include <vector>
#include "common/log.hpp" #include "common/log.hpp"
#include "gip/protocol.hpp"
#include "mt76/mt76.hpp" #include "mt76/mt76.hpp"
#include "usb/usb_transport.hpp" #include "usb/usb_transport.hpp"
@@ -21,11 +22,72 @@
#define XONE_VERSION "0.1.0" #define XONE_VERSION "0.1.0"
#endif #endif
struct controller;
struct xone_dongle;
// QoS-data transport used by the GIP adapter for one wireless client.
class controller_transport : public xone::gip::transport {
public:
controller_transport(xone::mt76::chip& chip, std::uint8_t wcid,
std::array<std::uint8_t, 6> mac)
: chip_(chip)
, wcid_(wcid)
, mac_(mac)
{
}
auto send_frame(std::span<std::uint8_t const> frame) -> int override;
auto set_encryption_key(std::uint8_t client_id,
std::span<std::uint8_t const> key) -> int override;
auto enable_encryption() -> void { encrypted_ = true; }
private:
xone::mt76::chip& chip_;
std::uint8_t wcid_;
std::array<std::uint8_t, 6> mac_;
bool encrypted_ = false;
};
class controller_listener : public xone::gip::client_listener {
public:
controller_listener(xone_dongle& dongle, controller& controller)
: dongle_(dongle)
, controller_(controller)
{
}
auto on_client_added(xone::gip::client&) -> void override;
auto on_client_removed(std::uint8_t) -> void override;
auto on_guide_button(xone::gip::client&, bool down) -> void override;
auto on_input(xone::gip::client&, std::span<std::uint8_t const> data)
-> void override;
private:
xone_dongle& dongle_;
controller& controller_;
};
// One connected controller (tracked for the GUI). Guarded by xone_dongle's // One connected controller (tracked for the GUI). Guarded by xone_dongle's
// lock; written by the transport reader thread, read by the UI thread. // lock; written by the transport reader thread, read by the UI thread.
struct controller { struct controller {
std::uint8_t wcid; std::uint8_t wcid;
std::array<std::uint8_t, 6> mac; std::array<std::uint8_t, 6> mac;
std::uint16_t buttons = 0;
std::uint16_t trigger_left = 0;
std::uint16_t trigger_right = 0;
std::int16_t stick_left_x = 0;
std::int16_t stick_left_y = 0;
std::int16_t stick_right_x = 0;
std::int16_t stick_right_y = 0;
std::uint32_t input_sequence = 0;
bool input_active = false;
bool guide_down = false;
bool gip_ready = false;
// Declared in dependency order so adapter is destroyed before its bridge.
std::unique_ptr<controller_transport> gip_transport;
std::unique_ptr<controller_listener> gip_listener;
std::unique_ptr<xone::gip::adapter> gip_adapter;
}; };
// Definition of the opaque struct xone_dongle from app/xone_api.h. // Definition of the opaque struct xone_dongle from app/xone_api.h.
@@ -44,8 +106,9 @@ struct xone_dongle {
// Firmware load + radio init runs here so the UI thread never blocks. // Firmware load + radio init runs here so the UI thread never blocks.
std::thread worker; std::thread worker;
// Connected controllers (wcid 1..16). Guarded by lock. // Connected controllers (wcid 1..16). The pointed-to objects stay stable
std::vector<controller> controllers; // while the vector changes, as GIP bridge callbacks retain references.
std::vector<std::unique_ptr<controller>> controllers;
// Pairing mode state (port of dongle->pairing). Shared between the UI // Pairing mode state (port of dongle->pairing). Shared between the UI
// thread and the transport reader thread. // thread and the transport reader thread.
@@ -53,19 +116,20 @@ struct xone_dongle {
~xone_dongle() ~xone_dongle()
{ {
// Finish firmware/radio work before tearing down the transport it uses.
if (worker.joinable())
worker.join();
// Stop the reader thread before the chip is torn down so a late IN // Stop the reader thread before the chip is torn down so a late IN
// completion cannot touch freed state. // completion cannot touch freed state.
transport.reset(); transport.reset();
// The session is destroyed at process exit even without an explicit
// xone_close(); joining here avoids terminating on a joinable thread.
if (worker.joinable())
worker.join();
} }
// RX dispatch, driven from the transport reader thread. Port of the // RX dispatch, driven from the transport reader thread. Port of the
// xone_dongle_process_* functions in transport/dongle.c. // xone_dongle_process_* functions in transport/dongle.c.
auto process_message(void const *data, std::size_t len) -> void; auto process_message(void const *data, std::size_t len) -> void;
auto process_wlan(std::span<std::uint8_t const> buf) -> void; auto process_wlan(std::span<std::uint8_t const> buf) -> void;
auto handle_qos_data(std::uint8_t wcid,
std::span<std::uint8_t const> payload) -> void;
auto handle_association(std::span<std::uint8_t const> addr) -> void; auto handle_association(std::span<std::uint8_t const> addr) -> void;
auto handle_disassociation(std::uint8_t wcid) -> void; auto handle_disassociation(std::uint8_t wcid) -> void;
auto handle_client_command(std::span<std::uint8_t const> payload, auto handle_client_command(std::span<std::uint8_t const> payload,
@@ -74,6 +138,7 @@ struct xone_dongle {
// Enter or leave pairing mode (port of xone_dongle_toggle_pairing). // Enter or leave pairing mode (port of xone_dongle_toggle_pairing).
auto set_pairing(bool enable) -> void; auto set_pairing(bool enable) -> void;
auto setup_controller(controller& controller) -> void;
}; };
namespace { namespace {
@@ -93,6 +158,75 @@ auto firmware_path_for(std::uint16_t) -> std::string
} // namespace } // namespace
// --- GIP/MT76 bridge -------------------------------------------------------
auto controller_transport::send_frame(std::span<std::uint8_t const> frame)
-> int
{
return chip_.send_client_frame(wcid_, mac_, frame, encrypted_);
}
auto controller_transport::set_encryption_key(
std::uint8_t client_id, std::span<std::uint8_t const> key) -> int
{
(void)client_id;
return chip_.set_client_key(wcid_, key);
}
auto controller_listener::on_client_added(xone::gip::client&) -> void
{
std::lock_guard<std::mutex> guard(dongle_.lock);
controller_.gip_ready = true;
xone::log_msg(xone::log_level::info,
"api: GIP ready (wcid=%d)", controller_.wcid);
}
auto controller_listener::on_client_removed(std::uint8_t) -> void
{
std::lock_guard<std::mutex> guard(dongle_.lock);
controller_.gip_ready = false;
controller_.input_active = false;
}
auto controller_listener::on_guide_button(xone::gip::client&, bool down)
-> void
{
std::lock_guard<std::mutex> guard(dongle_.lock);
controller_.guide_down = down;
}
auto controller_listener::on_input(
xone::gip::client&, std::span<std::uint8_t const> data) -> void
{
// Standard gamepad input: seven little-endian 16-bit values.
if (data.size() < 14)
return;
std::lock_guard<std::mutex> guard(dongle_.lock);
controller_.buttons = xone::load_le16(data.data());
controller_.trigger_left = xone::load_le16(data.data() + 2);
controller_.trigger_right = xone::load_le16(data.data() + 4);
controller_.stick_left_x = static_cast<std::int16_t>(
xone::load_le16(data.data() + 6));
controller_.stick_left_y = static_cast<std::int16_t>(
~xone::load_le16(data.data() + 8));
controller_.stick_right_x = static_cast<std::int16_t>(
xone::load_le16(data.data() + 10));
controller_.stick_right_y = static_cast<std::int16_t>(
~xone::load_le16(data.data() + 12));
++controller_.input_sequence;
controller_.input_active = true;
}
auto xone_dongle::setup_controller(controller& c) -> void
{
c.gip_transport = std::make_unique<controller_transport>(*chip, c.wcid,
c.mac);
c.gip_listener = std::make_unique<controller_listener>(*this, c);
c.gip_adapter = std::make_unique<xone::gip::adapter>(
*c.gip_transport, *c.gip_listener, 1);
}
// --- RX dispatch (port of xone_dongle_process_*) --------------------------- // --- RX dispatch (port of xone_dongle_process_*) ---------------------------
auto xone_dongle::process_message(void const *data, std::size_t len) -> void auto xone_dongle::process_message(void const *data, std::size_t len) -> void
@@ -159,6 +293,8 @@ auto xone_dongle::process_wlan(std::span<std::uint8_t const> buf) -> void
auto rxinfo = xone::load_le32(buf.data()); auto rxinfo = xone::load_le32(buf.data());
auto ctl = xone::load_le32(buf.data() + 4); auto ctl = xone::load_le32(buf.data() + 4);
auto wcid = static_cast<std::uint8_t>(field_get(mt_rxwi_ctl_wcid, ctl)); auto wcid = static_cast<std::uint8_t>(field_get(mt_rxwi_ctl_wcid, ctl));
auto mpdu_len = static_cast<std::size_t>(
field_get(mt_rxwi_ctl_mpdu_len, ctl));
// The frame starts after the 32-byte rxwi. // The frame starts after the 32-byte rxwi.
std::span<std::uint8_t const> frame = buf.subspan(sizeof(mt76_rxwi)); std::span<std::uint8_t const> frame = buf.subspan(sizeof(mt76_rxwi));
@@ -167,19 +303,49 @@ auto xone_dongle::process_wlan(std::span<std::uint8_t const> buf) -> void
auto fc = xone::load_le16(frame.data()); auto fc = xone::load_le16(frame.data());
auto match = fc & (ieee80211_fctl_ftype | ieee80211_fctl_stype); auto match = fc & (ieee80211_fctl_ftype | ieee80211_fctl_stype);
auto header_len = k_mgmt_hdr_len;
if (match == (ieee80211_ftype_data | ieee80211_stype_qos_data))
header_len += 2; // sequence control plus QoS control
// L2PAD: 2 bytes of padding after the 802.11 header. // L2PAD: 2 bytes of padding after the 802.11 header. MPDU length is
std::size_t pad = (rxinfo & mt_rxinfo_l2pad) ? 2 : 0; // measured after this padding is removed, matching the upstream path.
auto pad = (rxinfo & mt_rxinfo_l2pad) ? std::size_t{2} : std::size_t{0};
if (mpdu_len < header_len || frame.size() < mpdu_len + pad)
return;
auto payload = frame.subspan(header_len + pad, mpdu_len - header_len);
if (match == (ieee80211_ftype_mgmt | ieee80211_stype_assoc_req)) { if (match == (ieee80211_ftype_mgmt | ieee80211_stype_assoc_req)) {
handle_association(frame.subspan(10, 6)); // addr2 handle_association(frame.subspan(10, 6)); // addr2
} else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_disassoc)) { } else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_disassoc)) {
handle_disassociation(wcid); handle_disassociation(wcid);
} else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_wlan_reserved)) { } else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_wlan_reserved)) {
handle_client_command(frame.subspan(k_mgmt_hdr_len + pad), wcid, handle_client_command(payload, wcid, frame.subspan(10, 6));
frame.subspan(10, 6)); // addr2 } else if (match == (ieee80211_ftype_data | ieee80211_stype_qos_data)) {
handle_qos_data(wcid, payload);
} }
// DATA|QOS_DATA (GIP) is handled in a later increment. }
auto xone_dongle::handle_qos_data(
std::uint8_t wcid, std::span<std::uint8_t const> payload) -> void
{
controller* found = nullptr;
{
std::lock_guard<std::mutex> guard(lock);
for (auto const& c : controllers) {
if (c->wcid == wcid) {
found = c.get();
break;
}
}
}
if (!found || !found->gip_adapter)
return;
if (auto err = found->gip_adapter->process_buffer(payload); err != 0)
xone::log_msg(xone::log_level::debug,
"api: GIP packet processing failed (%d)", err);
} }
auto xone_dongle::set_pairing(bool enable) -> void auto xone_dongle::set_pairing(bool enable) -> void
@@ -213,8 +379,8 @@ auto xone_dongle::handle_association(std::span<std::uint8_t const> addr) -> void
{ {
std::lock_guard<std::mutex> guard(lock); std::lock_guard<std::mutex> guard(lock);
for (auto const& c : controllers) { for (auto const& c : controllers) {
if (c.mac == mac) { if (c->mac == mac) {
wcid = c.wcid; wcid = c->wcid;
existing = true; existing = true;
break; break;
} }
@@ -223,7 +389,7 @@ auto xone_dongle::handle_association(std::span<std::uint8_t const> addr) -> void
for (std::uint8_t i = 1; i <= 16; ++i) { for (std::uint8_t i = 1; i <= 16; ++i) {
bool used = false; bool used = false;
for (auto const& c : controllers) for (auto const& c : controllers)
if (c.wcid == i) { used = true; break; } if (c->wcid == i) { used = true; break; }
if (!used) { wcid = i; break; } if (!used) { wcid = i; break; }
} }
} }
@@ -239,8 +405,12 @@ auto xone_dongle::handle_association(std::span<std::uint8_t const> addr) -> void
} }
if (!existing) { if (!existing) {
auto new_controller = std::make_unique<controller>();
new_controller->wcid = wcid;
new_controller->mac = mac;
setup_controller(*new_controller);
std::lock_guard<std::mutex> guard(lock); std::lock_guard<std::mutex> guard(lock);
controllers.push_back(controller{wcid, mac}); controllers.push_back(std::move(new_controller));
} }
if (!pairing) if (!pairing)
chip->set_led_mode(xone::mt76::led_mode::led_on); chip->set_led_mode(xone::mt76::led_mode::led_on);
@@ -260,7 +430,7 @@ auto xone_dongle::handle_disassociation(std::uint8_t wcid) -> void
{ {
std::lock_guard<std::mutex> guard(lock); std::lock_guard<std::mutex> guard(lock);
for (auto it = controllers.begin(); it != controllers.end(); ++it) { for (auto it = controllers.begin(); it != controllers.end(); ++it) {
if (it->wcid == wcid) { if ((*it)->wcid == wcid) {
controllers.erase(it); controllers.erase(it);
break; break;
} }
@@ -296,9 +466,28 @@ auto xone_dongle::handle_client_command(std::span<std::uint8_t const> payload,
"api: controller paired (wcid=%d)", wcid); "api: controller paired (wcid=%d)", wcid);
break; break;
} }
case client_cmd::client_enable_encryption: case client_cmd::client_enable_encryption: {
// Encryption is enabled in a later increment (needs the GIP key). controller* client = nullptr;
{
std::lock_guard<std::mutex> guard(lock);
for (auto const& c : controllers) {
if (c->wcid == wcid) {
client = c.get();
break; break;
}
}
}
if (!client || !client->gip_transport)
break;
std::uint8_t data[2] = {0, 0};
if (chip->send_client_command(
wcid, client->mac, client_cmd::client_enable_encryption,
{data, sizeof(data)}) == 0) {
client->gip_transport->enable_encryption();
}
break;
}
default: default:
break; break;
} }
@@ -353,8 +542,9 @@ extern "C" int xone_start(xone_dongle *d, const char *firmware_path)
if (!d) if (!d)
return -1; return -1;
// Select the image per product ID when no explicit path is given. // Use the upstream common image when no explicit path is given.
std::string path = firmware_path ? std::string(firmware_path) : firmware_path_for(d->pid); std::string path = firmware_path ? std::string(firmware_path)
: firmware_path_for(d->pid);
{ {
std::lock_guard<std::mutex> guard(d->lock); std::lock_guard<std::mutex> guard(d->lock);
@@ -457,12 +647,38 @@ extern "C" int xone_controller_mac(xone_dongle const *d, int index, char *buf,
std::lock_guard<std::mutex> guard(d->lock); std::lock_guard<std::mutex> guard(d->lock);
if (index < 0 || index >= static_cast<int>(d->controllers.size())) if (index < 0 || index >= static_cast<int>(d->controllers.size()))
return -1; return -1;
auto const& c = d->controllers[index]; auto const& c = *d->controllers[index];
std::snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x", std::snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x",
c.mac[0], c.mac[1], c.mac[2], c.mac[3], c.mac[4], c.mac[5]); c.mac[0], c.mac[1], c.mac[2], c.mac[3], c.mac[4], c.mac[5]);
return 0; return 0;
} }
// Copy the latest input state for the controller at index.
extern "C" int xone_controller_get_state(
xone_dongle const *d, int index, struct xone_controller_state *state)
{
if (!d || !state)
return -1;
std::lock_guard<std::mutex> guard(d->lock);
if (index < 0 || index >= static_cast<int>(d->controllers.size()))
return -1;
auto const& c = *d->controllers[index];
state->buttons = c.buttons;
state->guide_down = c.guide_down;
state->gip_ready = c.gip_ready;
state->input_active = c.input_active;
state->trigger_left = c.trigger_left;
state->trigger_right = c.trigger_right;
state->stick_left_x = c.stick_left_x;
state->stick_left_y = c.stick_left_y;
state->stick_right_x = c.stick_right_x;
state->stick_right_y = c.stick_right_y;
state->sequence = c.input_sequence;
return 0;
}
// Enter or leave pairing mode: sets the beacon pairing flag and blinks the // Enter or leave pairing mode: sets the beacon pairing flag and blinks the
// LED while enabled (port of xone_dongle_toggle_pairing). // LED while enabled (port of xone_dongle_toggle_pairing).
extern "C" void xone_set_pairing(xone_dongle *d, bool enable) extern "C" void xone_set_pairing(xone_dongle *d, bool enable)
+143 -10
View File
@@ -24,16 +24,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
private struct ControllerSnapshot: Identifiable {
let id: String
let buttons: UInt16
let guideDown: Bool
let gipReady: Bool
let inputActive: Bool
let triggerLeft: UInt16
let triggerRight: UInt16
let stickLeftX: Int16
let stickLeftY: Int16
let stickRightX: Int16
let stickRightY: Int16
let sequence: UInt32
}
struct ContentView: View { struct ContentView: View {
@State private var donglePresent = false @State private var donglePresent = false
@State private var session: OpaquePointer? = nil @State private var session: OpaquePointer? = nil
// Radio state polled from the background worker each tick; a change here // Radio state polled from the background worker each tick; a change here
// is what re-renders the debug section. // is what re-renders the debug section.
@State private var radioState = XONE_STATE_IDLE @State private var radioState = XONE_STATE_IDLE
// Connected controllers (MAC strings), refreshed each tick. // Connected controllers and their latest input snapshot.
@State private var controllers: [String] = [] @State private var controllers: [ControllerSnapshot] = []
private let timer = Timer.publish(every: 1.0, on: .main, in: .common) private let timer = Timer.publish(every: 0.1, on: .main, in: .common)
.autoconnect() .autoconnect()
var body: some View { var body: some View {
@@ -138,12 +153,32 @@ struct ContentView: View {
// Query the C API for connected controllers and store their MACs. // Query the C API for connected controllers and store their MACs.
private func refreshControllers(_ session: OpaquePointer) { private func refreshControllers(_ session: OpaquePointer) {
let count = Int(xone_controller_count(session)) let count = Int(xone_controller_count(session))
var list: [String] = [] var list: [ControllerSnapshot] = []
for i in 0..<count { for i in 0..<count {
var buf = [CChar](repeating: 0, count: 18) var macBuffer = [CChar](repeating: 0, count: 18)
if xone_controller_mac(session, Int32(i), &buf, 18) == 0 { guard xone_controller_mac(session, Int32(i), &macBuffer, 18) == 0 else {
list.append(String(cString: buf)) continue
} }
var state = xone_controller_state()
guard xone_controller_get_state(session, Int32(i), &state) == 0 else {
continue
}
list.append(ControllerSnapshot(
id: String(cString: macBuffer),
buttons: state.buttons,
guideDown: state.guide_down,
gipReady: state.gip_ready,
inputActive: state.input_active,
triggerLeft: state.trigger_left,
triggerRight: state.trigger_right,
stickLeftX: state.stick_left_x,
stickLeftY: state.stick_left_y,
stickRightX: state.stick_right_x,
stickRightY: state.stick_right_y,
sequence: state.sequence
))
} }
controllers = list controllers = list
} }
@@ -159,22 +194,120 @@ struct ContentView: View {
} }
private var controllersView: some View { private var controllersView: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 12) {
Text("Controllers") Text("Controllers")
.font(.headline) .font(.headline)
if controllers.isEmpty { if controllers.isEmpty {
Text("No controllers connected.") Text("No controllers connected.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else { } else {
ForEach(controllers, id: \.self) { mac in ForEach(controllers) { controller in
controllerView(controller)
}
}
}
}
private func controllerView(_ controller: ControllerSnapshot) -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
Image(systemName: "gamecontroller") Image(systemName: "gamecontroller")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text(mac) Text(controller.id)
.font(.system(.body, design: .monospaced)) .font(.system(.body, design: .monospaced))
Spacer()
Text(controller.gipReady ? "ready" : "connecting")
.foregroundStyle(controller.gipReady ? .green : .orange)
}
HStack(spacing: 4) {
buttonIndicator("Guide", pressed: controller.guideDown)
buttonIndicator("A", pressed: isPressed(controller, 0x0010))
buttonIndicator("B", pressed: isPressed(controller, 0x0020))
buttonIndicator("X", pressed: isPressed(controller, 0x0040))
buttonIndicator("Y", pressed: isPressed(controller, 0x0080))
}
HStack(spacing: 4) {
buttonIndicator("LB", pressed: isPressed(controller, 0x1000))
buttonIndicator("RB", pressed: isPressed(controller, 0x2000))
buttonIndicator("LS", pressed: isPressed(controller, 0x4000))
buttonIndicator("RS", pressed: isPressed(controller, 0x8000))
buttonIndicator("", pressed: isPressed(controller, 0x0100))
buttonIndicator("", pressed: isPressed(controller, 0x0200))
buttonIndicator("", pressed: isPressed(controller, 0x0400))
buttonIndicator("", pressed: isPressed(controller, 0x0800))
}
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Left stick")
.foregroundStyle(.secondary)
signedAxis("X", controller.stickLeftX)
signedAxis("Y", controller.stickLeftY)
}
VStack(alignment: .leading, spacing: 4) {
Text("Right stick")
.foregroundStyle(.secondary)
signedAxis("X", controller.stickRightX)
signedAxis("Y", controller.stickRightY)
} }
} }
HStack(spacing: 12) {
triggerAxis("Left trigger", controller.triggerLeft)
triggerAxis("Right trigger", controller.triggerRight)
} }
Text(controller.inputActive
? "Input #\(controller.sequence)"
: "Waiting for input")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(10)
.background(Color.secondary.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
private func isPressed(_ controller: ControllerSnapshot, _ mask: UInt16) -> Bool {
(controller.buttons & mask) != 0
}
private func buttonIndicator(_ title: String, pressed: Bool) -> some View {
Text(title)
.font(.caption2)
.frame(minWidth: 28, minHeight: 22)
.padding(.horizontal, 2)
.background(pressed ? Color.green : Color.secondary.opacity(0.18))
.foregroundStyle(pressed ? .white : .primary)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
private func signedAxis(_ label: String, _ value: Int16) -> some View {
let normalized = Double(Int(value) + 32768) / 65535.0
return HStack(spacing: 4) {
Text(label)
.font(.caption2)
.frame(width: 12, alignment: .leading)
ProgressView(value: normalized)
.frame(width: 82)
Text("\(value)")
.font(.caption2.monospacedDigit())
.frame(width: 42, alignment: .trailing)
}
}
private func triggerAxis(_ label: String, _ value: UInt16) -> some View {
HStack(spacing: 4) {
Text(label)
.font(.caption2)
.frame(width: 76, alignment: .leading)
ProgressView(value: Double(value), total: 1023)
.frame(width: 70)
Text("\(value)")
.font(.caption2.monospacedDigit())
.frame(width: 32, alignment: .trailing)
} }
} }
} }
+43
View File
@@ -808,6 +808,49 @@ auto chip::send_wlan(std::span<std::uint8_t const> frame) -> int
return ret < 0 ? ret : 0; return ret < 0 ? ret : 0;
} }
auto chip::send_client_frame(std::uint8_t wcid,
std::span<std::uint8_t const> addr,
std::span<std::uint8_t const> frame,
bool encrypted) -> int
{
auto address = mac_address();
std::uint8_t data[8] = {
0x00, 0x00, 0x00, static_cast<std::uint8_t>(wcid - 1),
0x00, 0x00, 0x00, 0x00,
};
std::uint16_t fc = ieee80211_ftype_data | ieee80211_stype_qos_data
| ieee80211_fctl_from_ds;
if (encrypted)
fc |= ieee80211_fctl_protected;
std::uint8_t hdr[26] = {};
xone::store_le16(hdr + 0, fc);
xone::store_le16(hdr + 2, 144); // duration
std::memcpy(hdr + 4, addr.data(), 6);
std::memcpy(hdr + 10, address.data(), 6);
std::memcpy(hdr + 16, address.data(), 6);
// bytes 22-23 are sequence control and stay zero
// bytes 24-25 are QoS control and stay zero
auto txwi = build_txwi(static_cast<std::uint8_t>(wcid - 1),
sizeof(hdr) + frame.size());
std::vector<std::uint8_t> payload(8 + txwi.size() + sizeof(hdr) + 2
+ frame.size());
std::memcpy(payload.data(), data, sizeof(data));
std::memcpy(payload.data() + 8, txwi.data(), txwi.size());
std::memcpy(payload.data() + 8 + txwi.size(), hdr, sizeof(hdr));
std::memcpy(payload.data() + 8 + txwi.size() + sizeof(hdr) + 2,
frame.data(), frame.size());
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, 0);
auto buf = build_message(info, payload.data(), payload.size());
auto ret = transport_.bulk_write(buf.data(), buf.size());
return ret < 0 ? ret : 0;
}
auto chip::associate_client(std::uint8_t wcid, auto chip::associate_client(std::uint8_t wcid,
std::span<std::uint8_t const> addr) -> int std::span<std::uint8_t const> addr) -> int
{ {
+1 -1
View File
@@ -135,7 +135,7 @@ void test_client_association_layout(void)
// Client command and 802.11 frame control values. // Client command and 802.11 frame control values.
TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_assoc_resp, 0x0010); TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_assoc_resp, 0x0010);
TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_disassoc, 0x00a0); TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_disassoc, 0x00a0);
TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_qos_data, 0x0070); TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_qos_data, 0x0080);
TEST_ASSERT_EQUAL_UINT16( TEST_ASSERT_EQUAL_UINT16(
xone::mt76::ieee80211_stype_wlan_reserved, 0x0070); xone::mt76::ieee80211_stype_wlan_reserved, 0x0070);
} }