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:
+237
-21
@@ -14,6 +14,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "common/log.hpp"
|
||||
#include "gip/protocol.hpp"
|
||||
#include "mt76/mt76.hpp"
|
||||
#include "usb/usb_transport.hpp"
|
||||
|
||||
@@ -21,11 +22,72 @@
|
||||
#define XONE_VERSION "0.1.0"
|
||||
#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
|
||||
// lock; written by the transport reader thread, read by the UI thread.
|
||||
struct controller {
|
||||
std::uint8_t wcid;
|
||||
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.
|
||||
@@ -44,8 +106,9 @@ struct xone_dongle {
|
||||
// Firmware load + radio init runs here so the UI thread never blocks.
|
||||
std::thread worker;
|
||||
|
||||
// Connected controllers (wcid 1..16). Guarded by lock.
|
||||
std::vector<controller> controllers;
|
||||
// Connected controllers (wcid 1..16). The pointed-to objects stay stable
|
||||
// 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
|
||||
// thread and the transport reader thread.
|
||||
@@ -53,19 +116,20 @@ struct 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
|
||||
// completion cannot touch freed state.
|
||||
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
|
||||
// xone_dongle_process_* functions in transport/dongle.c.
|
||||
auto process_message(void const *data, std::size_t len) -> 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_disassociation(std::uint8_t wcid) -> void;
|
||||
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).
|
||||
auto set_pairing(bool enable) -> void;
|
||||
auto setup_controller(controller& controller) -> void;
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -93,6 +158,75 @@ auto firmware_path_for(std::uint16_t) -> std::string
|
||||
|
||||
} // 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_*) ---------------------------
|
||||
|
||||
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 ctl = xone::load_le32(buf.data() + 4);
|
||||
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.
|
||||
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 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.
|
||||
std::size_t pad = (rxinfo & mt_rxinfo_l2pad) ? 2 : 0;
|
||||
// L2PAD: 2 bytes of padding after the 802.11 header. MPDU length is
|
||||
// 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)) {
|
||||
handle_association(frame.subspan(10, 6)); // addr2
|
||||
} else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_disassoc)) {
|
||||
handle_disassociation(wcid);
|
||||
} else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_wlan_reserved)) {
|
||||
handle_client_command(frame.subspan(k_mgmt_hdr_len + pad), wcid,
|
||||
frame.subspan(10, 6)); // addr2
|
||||
handle_client_command(payload, wcid, frame.subspan(10, 6));
|
||||
} 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
|
||||
@@ -213,8 +379,8 @@ auto xone_dongle::handle_association(std::span<std::uint8_t const> addr) -> void
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(lock);
|
||||
for (auto const& c : controllers) {
|
||||
if (c.mac == mac) {
|
||||
wcid = c.wcid;
|
||||
if (c->mac == mac) {
|
||||
wcid = c->wcid;
|
||||
existing = true;
|
||||
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) {
|
||||
bool used = false;
|
||||
for (auto const& c : controllers)
|
||||
if (c.wcid == i) { used = true; break; }
|
||||
if (c->wcid == i) { used = true; 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) {
|
||||
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);
|
||||
controllers.push_back(controller{wcid, mac});
|
||||
controllers.push_back(std::move(new_controller));
|
||||
}
|
||||
if (!pairing)
|
||||
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);
|
||||
for (auto it = controllers.begin(); it != controllers.end(); ++it) {
|
||||
if (it->wcid == wcid) {
|
||||
if ((*it)->wcid == wcid) {
|
||||
controllers.erase(it);
|
||||
break;
|
||||
}
|
||||
@@ -296,9 +466,28 @@ auto xone_dongle::handle_client_command(std::span<std::uint8_t const> payload,
|
||||
"api: controller paired (wcid=%d)", wcid);
|
||||
break;
|
||||
}
|
||||
case client_cmd::client_enable_encryption:
|
||||
// Encryption is enabled in a later increment (needs the GIP key).
|
||||
case client_cmd::client_enable_encryption: {
|
||||
controller* client = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(lock);
|
||||
for (auto const& c : controllers) {
|
||||
if (c->wcid == wcid) {
|
||||
client = c.get();
|
||||
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:
|
||||
break;
|
||||
}
|
||||
@@ -353,8 +542,9 @@ extern "C" int xone_start(xone_dongle *d, const char *firmware_path)
|
||||
if (!d)
|
||||
return -1;
|
||||
|
||||
// Select the image per product ID when no explicit path is given.
|
||||
std::string path = firmware_path ? std::string(firmware_path) : firmware_path_for(d->pid);
|
||||
// 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::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);
|
||||
if (index < 0 || index >= static_cast<int>(d->controllers.size()))
|
||||
return -1;
|
||||
auto const& c = d->controllers[index];
|
||||
auto const& c = *d->controllers[index];
|
||||
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]);
|
||||
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
|
||||
// LED while enabled (port of xone_dongle_toggle_pairing).
|
||||
extern "C" void xone_set_pairing(xone_dongle *d, bool enable)
|
||||
|
||||
+148
-15
@@ -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 {
|
||||
@State private var donglePresent = false
|
||||
@State private var session: OpaquePointer? = nil
|
||||
// Radio state polled from the background worker each tick; a change here
|
||||
// is what re-renders the debug section.
|
||||
@State private var radioState = XONE_STATE_IDLE
|
||||
// Connected controllers (MAC strings), refreshed each tick.
|
||||
@State private var controllers: [String] = []
|
||||
// Connected controllers and their latest input snapshot.
|
||||
@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()
|
||||
|
||||
var body: some View {
|
||||
@@ -138,12 +153,32 @@ struct ContentView: View {
|
||||
// Query the C API for connected controllers and store their MACs.
|
||||
private func refreshControllers(_ session: OpaquePointer) {
|
||||
let count = Int(xone_controller_count(session))
|
||||
var list: [String] = []
|
||||
var list: [ControllerSnapshot] = []
|
||||
for i in 0..<count {
|
||||
var buf = [CChar](repeating: 0, count: 18)
|
||||
if xone_controller_mac(session, Int32(i), &buf, 18) == 0 {
|
||||
list.append(String(cString: buf))
|
||||
var macBuffer = [CChar](repeating: 0, count: 18)
|
||||
guard xone_controller_mac(session, Int32(i), &macBuffer, 18) == 0 else {
|
||||
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
|
||||
}
|
||||
@@ -159,24 +194,122 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private var controllersView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Controllers")
|
||||
.font(.headline)
|
||||
if controllers.isEmpty {
|
||||
Text("No controllers connected.")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(controllers, id: \.self) { mac in
|
||||
HStack {
|
||||
Image(systemName: "gamecontroller")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(mac)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
}
|
||||
ForEach(controllers) { controller in
|
||||
controllerView(controller)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func controllerView(_ controller: ControllerSnapshot) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "gamecontroller")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(controller.id)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point: SwiftUI App provides a static main() (top-level code lives in
|
||||
|
||||
@@ -808,6 +808,49 @@ auto chip::send_wlan(std::span<std::uint8_t const> frame) -> int
|
||||
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,
|
||||
std::span<std::uint8_t const> addr) -> int
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user