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:
+24
-2
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user