feat: pairing mode and RX frame logging

The dongle beacon carries a pairing flag in the Microsoft OUI IE; with
it clear, controllers ignore the beacon. Port xone_dongle_toggle_pairing:
xone_set_pairing() rewrites the beacon with the flag set and blinks the
LED, the GUI button triggers it, EVT_BUTTON (dongle button) does too, and
the LED follows upstream state (blink while pairing, on when a controller
is connected, off otherwise). The CLI gains a pair command.
Log every inbound frame (info word + first 16 bytes) so the RX path can
be traced against hardware.

Co-Authored-By: qwen3.8-27b@q2_k_xl: pairing mode and RX logging
This commit is contained in:
portersky
2026-08-17 20:53:40 +02:00
parent dae51f085b
commit 5fd1b3a11f
5 changed files with 78 additions and 11 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$")
endif() endif()
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 3.21)
project(xone_macos VERSION 0.1.10 LANGUAGES CXX Swift) project(xone_macos VERSION 0.1.11 LANGUAGES CXX Swift)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+3
View File
@@ -72,6 +72,9 @@ 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);
// Enter or leave pairing mode (beacon pairing flag + LED blink).
void xone_set_pairing(xone_dongle *d, bool enable);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+55 -1
View File
@@ -3,6 +3,7 @@
#include "app/xone_api.h" #include "app/xone_api.h"
#include <array> #include <array>
#include <atomic>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
@@ -46,6 +47,10 @@ struct xone_dongle {
// Connected controllers (wcid 1..16). Guarded by lock. // Connected controllers (wcid 1..16). Guarded by lock.
std::vector<controller> controllers; std::vector<controller> controllers;
// Pairing mode state (port of dongle->pairing). Shared between the UI
// thread and the transport reader thread.
std::atomic<bool> pairing{false};
~xone_dongle() ~xone_dongle()
{ {
// 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
@@ -66,6 +71,9 @@ struct xone_dongle {
auto handle_client_command(std::span<std::uint8_t const> payload, auto handle_client_command(std::span<std::uint8_t const> payload,
std::uint8_t wcid, std::uint8_t wcid,
std::span<std::uint8_t const> addr) -> void; std::span<std::uint8_t const> addr) -> void;
// Enter or leave pairing mode (port of xone_dongle_toggle_pairing).
auto set_pairing(bool enable) -> void;
}; };
namespace { namespace {
@@ -109,6 +117,14 @@ auto xone_dongle::process_message(void const *data, std::size_t len) -> void
// Strip the header + trailer. // Strip the header + trailer.
buf = buf.subspan(cmd_hdr_len, len - 2 * cmd_hdr_len); buf = buf.subspan(cmd_hdr_len, len - 2 * cmd_hdr_len);
char hex[64] = "";
int off = 0;
for (std::size_t i = 0; i < buf.size() && i < 16 && off < 48; ++i)
off += std::snprintf(hex + off, sizeof(hex) - off, "%02x ", buf[i]);
xone::log_msg(xone::log_level::debug,
"api: rx info=0x%08x port=%d len=%zu %s",
(unsigned)info, (unsigned)port, buf.size(), hex);
if (port == dma_msg_port::wlan_port) { if (port == dma_msg_port::wlan_port) {
process_wlan(buf); process_wlan(buf);
return; return;
@@ -119,7 +135,9 @@ auto xone_dongle::process_message(void const *data, std::size_t len) -> void
switch (field_get(mt_rx_fce_info_evt_type, info)) { switch (field_get(mt_rx_fce_info_evt_type, info)) {
case cpu_evt::evt_button: case cpu_evt::evt_button:
// Pairing mode is entered via the CLI for now. // The dongle pairing button was pressed (port of
// xone_dongle_handle_button).
set_pairing(true);
break; break;
case cpu_evt::evt_packet_rx: case cpu_evt::evt_packet_rx:
process_wlan(buf); process_wlan(buf);
@@ -166,6 +184,25 @@ auto xone_dongle::process_wlan(std::span<std::uint8_t const> buf) -> void
// DATA|QOS_DATA (GIP) is handled in a later increment. // DATA|QOS_DATA (GIP) is handled in a later increment.
} }
auto xone_dongle::set_pairing(bool enable) -> void
{
if (!chip)
return;
pairing = enable;
chip->set_pairing(enable);
bool has_clients = false;
{
std::lock_guard<std::mutex> guard(lock);
has_clients = !controllers.empty();
}
auto led = enable ? xone::mt76::led_mode::led_blink
: (has_clients ? xone::mt76::led_mode::led_on
: xone::mt76::led_mode::led_off);
chip->set_led_mode(led);
xone::log_msg(xone::log_level::info, "api: pairing mode %s",
enable ? "enabled" : "disabled");
}
auto xone_dongle::handle_association(std::span<std::uint8_t const> addr) -> void auto xone_dongle::handle_association(std::span<std::uint8_t const> addr) -> void
{ {
// Find a free WCID slot (1..16). // Find a free WCID slot (1..16).
@@ -192,8 +229,12 @@ auto xone_dongle::handle_association(std::span<std::uint8_t const> addr) -> void
return; return;
} }
{
std::lock_guard<std::mutex> guard(lock); std::lock_guard<std::mutex> guard(lock);
controllers.push_back(controller{wcid, mac}); controllers.push_back(controller{wcid, mac});
}
if (!pairing)
chip->set_led_mode(xone::mt76::led_mode::led_on);
xone::log_msg(xone::log_level::info, xone::log_msg(xone::log_level::info,
"api: controller associated (wcid=%d)", wcid); "api: controller associated (wcid=%d)", wcid);
} }
@@ -206,6 +247,8 @@ auto xone_dongle::handle_disassociation(std::uint8_t wcid) -> void
// Remove from the chip outside the lock. // Remove from the chip outside the lock.
chip->remove_client(wcid); chip->remove_client(wcid);
bool last = false;
{
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) {
@@ -213,6 +256,10 @@ auto xone_dongle::handle_disassociation(std::uint8_t wcid) -> void
break; break;
} }
} }
last = controllers.empty();
}
if (last && !pairing)
chip->set_led_mode(xone::mt76::led_mode::led_off);
xone::log_msg(xone::log_level::info, xone::log_msg(xone::log_level::info,
"api: controller removed (wcid=%d)", wcid); "api: controller removed (wcid=%d)", wcid);
} }
@@ -400,3 +447,10 @@ extern "C" int xone_controller_mac(xone_dongle const *d, int index, char *buf,
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;
} }
// 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)
{
d->set_pairing(enable);
}
+4 -2
View File
@@ -58,9 +58,11 @@ struct ContentView: View {
HStack { HStack {
Button("Pair Controller") { Button("Pair Controller") {
// TODO(phase 5): pairing UI (LED blink + controller press) if let s = session {
xone_set_pairing(s, true)
} }
.disabled(!donglePresent) }
.disabled(radioState != XONE_STATE_READY)
Spacer() Spacer()
+8
View File
@@ -25,6 +25,7 @@ auto usage() -> int
" info Show dongle and chip state\n" " info Show dongle and chip state\n"
" firmware <path> Load the firmware image\n" " firmware <path> Load the firmware image\n"
" radio-init Initialize the radio\n" " radio-init Initialize the radio\n"
" pair Enter pairing mode (beacon flag + LED blink)\n"
" led <mode> Set LED mode (0 blink, 1 on, 2 off)\n" " led <mode> Set LED mode (0 blink, 1 on, 2 off)\n"
" func <func> <val> MCU function select (debug)\n" " func <func> <val> MCU function select (debug)\n"
" radio-deinit Suspend the radio\n" " radio-deinit Suspend the radio\n"
@@ -68,6 +69,13 @@ auto run(int argc, char **argv, xone::usb::transport &transport,
int ret = chip.init_radio(); int ret = chip.init_radio();
if (ret != 0) if (ret != 0)
std::fprintf(stderr, "xone_cli: radio init failed (%d)\n", ret); std::fprintf(stderr, "xone_cli: radio init failed (%d)\n", ret);
} else if (!std::strcmp(cmd, "pair")) {
int ret = chip.set_pairing(true);
if (ret != 0) {
std::fprintf(stderr, "xone_cli: pair failed (%d)\n", ret);
return 1;
}
chip.set_led_mode(xone::mt76::led_mode::led_blink);
} else if (!std::strcmp(cmd, "func")) { } else if (!std::strcmp(cmd, "func")) {
if (i + 2 > argc) if (i + 2 > argc)
return usage(); return usage();