Skip to content

Conversation

@gulrotkake
Copy link
Contributor

This PR adds pin exchange using the red sync button, which enables reconnect without pairing/scanning.

Depends on #8 .

@wraybowling
Copy link
Contributor

This is incredible progress and confirms a lot of things I found in my own research, most of all that the PIN is the last 4 digits of the previously connected device reversed.
I am having trouble approving this PR though due to a lack of a working demo. This isn't entirely your fault. The repo lacks a proper examples folder. What I don't understand is how you're establishing the difference between "connecting" and "bonding" to use classic Bluetooth parlance. In simple terms, we should always assume the device is in "re-connection" mode in which connection is done via a wiimote A button, or 1+2 button or a balance board A button. In this mode, the PIN must be provided. That's the mode that it looks like you've added but I'm having trouble initiating. If the user begins a "NEW" device scan initiated by pressing the little red button on the Wii console then and only then does a sort of temporary no-PIN-necessary "connect" phase begin. A myriad of things can cause a wiimote to disconnect briefly, just long enough that a new connection needs establishing, and the default mode of course is the one that requires the PIN. It looks like what you are trying to do here is automatically determine which device is the initiator. If I'm understanding correctly, that's not wise because you can easily guess wrong. A working example sketch I would expect to read a momentary button at A1 to kick off a scan, or I suppose this could be automatically started if no prior PIN is found in the EEPROM. I'll keep poking at this though. I'm very likely also doing something wrong too. Did you verify these changes with the example code in the readme?

@wraybowling
Copy link
Contributor

Following up to say i got these changes working. The proof of concept is definitely there. It needs some tweaks though. Scan timeout needs to be lowered for the controller to find the ESP within a 3 second window. I used 1 second so it gets multiple tries. There's a quirk where using the Reset button on the ESP will make a reconnection but no data is actually transmitted. I saw you were working on disconnecting remotes as well and was trying to detect that edge case and automatically remove them. Waiting 10 seconds before reconnecting also makes the controller abandon the prior connection and that seemed to help. Now i'm onto the last test... this PR was really going well until I tried pairing a new second controller. That did not work. The ESP only wants to connect to the first one that completed the "auth" process. Again, this may be the fault of my demo sketch.

@gulrotkake
Copy link
Contributor Author

Glad you got parts of it working, I have not tried pairing two controllers at once, so may very well be that does not work. I'm a bit preoccupied with work this week, but will take a look this weekend and see if I can get it working.

You are definitely correct about scan interfering with reconnect though, so a better demo example is needed. Perhaps we need to add something similar to the "red sync"-button on the esp, which stops scanning after about 20 seconds.

@wraybowling
Copy link
Contributor

ESP32 can only connect to one device at a time, but the case I'm testing with 2 devices is making sure that this code is willing to overwrite previously stored mac adrress / PIN for a new device. Right now it seems like the answer is no. I can now ONLY connect to the controller I paired with the first time the auth function ran successfully. I can press red pair buttons on other controllers all I like and it will refuse to talk to them. I'm going to take a third controller out this afternoon after work just to make absolutely sure this is what's happening.

@gulrotkake
Copy link
Contributor Author

Some discoveries after testing:

  1. There was a race condition in the disconnect code which could result in the l2connection list getting into a bad state.
  2. You can connect multiple Wiimotes to a single ESP32 device, however, you cannot pair another device while a device is connected. If a device is connected the remote_name request fails with 0x09 (Connection Limit Exceeded)
[ 74580][D][Wiimote.cpp:392] process_inquiry_result_event(): queued remote_name_request.
[ 74588][D][Wiimote.cpp:1155] handle(): SEND => 01 19 04 0A B4 00 D8 1D 19 00 01 00 60 B2 
[ 74597][D][Wiimote.cpp:1060] process_hci_event(): **** HCI_EVENT code=0F len=4 data=09 05 19 04 
[ 74606][D][Wiimote.cpp:342] process_command_status_event(): remote_name_request failed. error=09
  1. To connect multiple devices I first paired one, disconnected it, paired the second, and then reconnected the first by pressing a regular button:
[   421][I][main.cpp:12] scan(): SCAN enabled for 300 seconds
[  4523][I][main.cpp:37] wiimote_callback(): AUTH Wiimote 81
[  4574][I][main.cpp:40] wiimote_callback(): CONNECT Handle: 81
[  4581][I][main.cpp:46] wiimote_callback(): LED 1 Handle: 81
[  4587][I][main.cpp:16] scan(): SCAN halted
[  7644][I][main.cpp:31] wiimote_callback(): A:BUTTON Disconnecting wiimote 81
[  7680][I][main.cpp:55] wiimote_callback(): DISCONNECT Handle: 81
[  7687][I][main.cpp:12] scan(): SCAN enabled for 60 seconds
[ 16900][I][main.cpp:37] wiimote_callback(): AUTH Wiimote 81
[ 16950][I][main.cpp:40] wiimote_callback(): CONNECT Handle: 81
[ 16957][I][main.cpp:46] wiimote_callback(): LED 1 Handle: 81
[ 16964][I][main.cpp:16] scan(): SCAN halted
[ 26342][I][main.cpp:40] wiimote_callback(): CONNECT Handle: 80
[ 26348][I][main.cpp:46] wiimote_callback(): LED 2 Handle: 80
[ 26355][I][main.cpp:16] scan(): SCAN halted
[ 30229][I][main.cpp:34] wiimote_callback(): B:BUTTON wiimote 80
[ 30938][I][main.cpp:34] wiimote_callback(): B:BUTTON wiimote 80
[ 32399][I][main.cpp:34] wiimote_callback(): B:BUTTON wiimote 81
[ 32819][I][main.cpp:34] wiimote_callback(): B:BUTTON wiimote 81

Here's the code I used for the above:

#include <Arduino.h>
#include <esp32_wiimote/Wiimote.h>
#include <vector>

Wiimote wiimote;
uint64_t scan_timeout{0};
std::vector<uint16_t> connections(4, 0);
uint8_t connection_count{0};

inline void scan(uint32_t seconds) {
  if (seconds > 0) {
    log_i("SCAN enabled for %d seconds", seconds);
    scan_timeout = millis() + seconds * 1'000;
    wiimote.scan(true);
  } else {
    log_i("SCAN halted");
    scan_timeout = 0;
    wiimote.scan(false);
  }
}

void wiimote_callback(wiimote_event_type_t event_type, uint16_t handle, uint8_t *data, size_t len) {
  if (event_type == WIIMOTE_EVENT_INITIALIZE) {
    // Scan for 5 minutes, or until a device connects
    scan(300);
  } else if (event_type == WIIMOTE_EVENT_DATA) {
    bool wiimote_button_B = (data[3] & 0x04) != 0;
    bool wiimote_button_A = (data[3] & 0x08) != 0;

    if (wiimote_button_A) {
      log_i("A:BUTTON Disconnecting wiimote %02X", handle);
      wiimote.disconnect(handle);
    } else if (wiimote_button_B) {
      log_i("B:BUTTON wiimote %02X", handle);
    }
  } else if (event_type == WIIMOTE_EVENT_NEW) {
    log_i("AUTH Wiimote %02X", handle);
    wiimote.initiate_auth(handle);
  } else if (event_type == WIIMOTE_EVENT_CONNECT) {
    log_i("CONNECT Handle: %02X", handle);

    // Find an available LED
    auto itr = std::find_if(connections.begin(), connections.end(), [](uint16_t val) { return val == 0; });
    if (itr != connections.end()) {
      *itr = handle;
      log_i("LED %d Handle: %02X", (1 + itr - connections.begin()), handle);
      wiimote.set_led(handle, 1 + itr - connections.begin());
    }

    // Stop scanning
    scan(0);

    connection_count++;
  } else if (event_type == WIIMOTE_EVENT_DISCONNECT) {
    log_i("DISCONNECT Handle: %02X", handle);

    // Free LED if in use by this wiimote
    auto itr = std::find_if(connections.begin(), connections.end(), [&handle](uint16_t val) { return val == handle; });
    if (itr != connections.end()) {
      *itr = 0;
    }

    connection_count--;
    if (connection_count == 0) {
      // Add devices disconnected, scan for another 60 seconds.
      scan(60);
    }
  } else if (event_type == WIIMOTE_EVENT_SCAN_STOP) {
    if (millis() < scan_timeout) {
      wiimote.scan(true);
    }
  }
}

void setup()
{
  Serial.begin(115200);
  wiimote.init(wiimote_callback);
}

void loop() {
  wiimote.handle();
  if (scan_timeout != 0 && millis() > scan_timeout) {
    scan(0);
  }
}

Note, when using the A button to disconnect (esp32 initiates the disconnect), be sure to hold it in until the wiimote disconnects, otherwise the wiimote will try reconnect. Alternatively, you can disconnect by holding in the power button on the wiimote.

@gulrotkake
Copy link
Contributor Author

On the topic of multiple devices, this commit from AqeeAqee seems relevant:
829da5c

@wraybowling
Copy link
Contributor

wraybowling commented Jun 26, 2025

There was a race condition in the disconnect code which could result in the l2connection list getting into a bad state.

Since you added the disconnect method in this branch, if you know the solution to fixing the race condition, I think it's best to fix it for this PR. Also because I don't know how swiftly @takeru will merge this.

You can connect multiple Wiimotes to a single ESP32 device, however, you cannot pair another device while a device is connected. If a device is connected the remote_name request fails with 0x09 (Connection Limit Exceeded)

The fact that we can connect to multiple devices already is absolutely wild. I had no idea! I played with your demo and confirmed it works. Still some weirdness that I've been mulling on the last couple of days. Some of the terminology of the demo code and the lack of access to reconnection in the past led me to believe we always need to scan. I thought that scan() was how we established connection. Here's how it looked in my head

graph LR
I[Init] --> S[🛜 Scan] --> P[🔴 Pair button] --> C[✳️ Connected] --> D[❌ Disconnect] 
C --> O[🛑 Stop Scanning] -->S
Loading

Now I've learned better. Init actually enables bluetooth connections with previously vetted devices. With access to reconnection, we can bypass scanning. Instead it actually looks like this

graph TD
I[🛜 Init] --> S[🫱 Scan] --> P[🔴 Pair button] --> N[❇️ New Connection] --> A[🤝 Auth] --> C[✳️ Connected]
I --> B[🔘A Button] --> T[❇️ Temp Connection] --> Q{📋 Check List} --pass--> C
Q -- fail--> D[❌ Disconnect]
A --> L[📝 Add To List]
S ---> O[🤷🏻‍♂️ Stop Scan]
Loading

You've done incredible work doing the auth handshake, and writing to the list of approved devices so that the reconnection flow works. It also had a side effect of exposing that this multiple connection stuff was always possible for better and for worse. I think it's worth adding a method to remove items from the auth list. As it is right now, a device can be disconnected, but it can't be stopped from reconnecting.

In my own use case, I want to limit the user to one device. A workaround I found was lowering DL2CAP_CONNECTION_LIST_SIZE to 2 (each device requires 2 packets). I did this with a build flag:

// Wiimote.cpp
#ifndef L2CAP_CONNECTION_LIST_SIZE
#define L2CAP_CONNECTION_LIST_SIZE 8
#endif
// platformio.ini
build_flags = -DL2CAP_CONNECTION_LIST_SIZE=2

I was not able to find a workaround for removing a device though. For installation use this could be a nightmare. Pressing the A button will connect to any ESP running the firmware that previously passed auth at random, and rebooting the ESP will only cause the remote/board to reconnect in an instant.

Another side-effect is that the temporary connection to verify PIN counts as a connection that does not increment connection_count. There's no event attached to that. But there is one attached to the disconnection that follows a failure. So you can end up with a negative connection count.

@wraybowling
Copy link
Contributor

Here's code that demonstrates my flow chart. Attach a push button to pin 21 and ground. Pressing it will disconnect any other devices (using your disconnect function) and allow pairing with new devices. Reconnection can resume after scan stops which happens without the need for an external timer.

#include <Wiimote.h>
#include <vector>

Wiimote wii;
bool is_scanning = false;
#define pair_button_gpio 21
std::vector<uint16_t> connections(4, 0);
std::vector<uint16_t> connected_wiimotes(0, 0);

void wiimote_callback(wiimote_event_type_t event_type, uint16_t wiimote, uint8_t* data, size_t len) {
  printf("len:%02X ", len);

  if (wiimote != 0) {
    printf("Wiimote:%04X ", wiimote);
  }

  if (event_type == WIIMOTE_EVENT_DATA) {
    if (data[1] == 0x32) {
      printf("🪇 ");
      for (int i = 0; i < 4; i++) {
        printf("%02X ", data[i]);
      }
      // http://wiibrew.org/wiki/Wiimote/Extension_Controllers/Nunchuck
      uint8_t* ext = data + 4;
      printf(" ... Nunchuk: sx=%3d sy=%3d c=%d z=%d\n",
             ext[0],
             ext[1],
             0 == (ext[5] & 0x02),
             0 == (ext[5] & 0x01));
    } else if (data[1] == 0x34) {
      printf("👟 ");
      for (int i = 0; i < 4; i++) {
        printf("%02X ", data[i]);
      }
      // https://wiibrew.org/wiki/Wii_Balance_Board#Data_Format
      uint8_t* ext = data + 4;
      /*printf(" ... Wii Balance Board: TopRight=%d BottomRight=%d TopLeft=%d BottomLeft=%d Temperature=%d BatteryLevel=0x%02x\n",
        ext[0] * 256 + ext[1],
        ext[2] * 256 + ext[3],
        ext[4] * 256 + ext[5],
        ext[6] * 256 + ext[7],
        ext[8],
        ext[10]
      );*/

      float weight[4];
      wii.get_balance_weight(data, weight);

      printf(" ... Wii Balance Board: TopRight=%f BottomRight=%f TopLeft=%f BottomLeft=%f\n",
             weight[BALANCE_POSITION_TOP_RIGHT],
             weight[BALANCE_POSITION_BOTTOM_RIGHT],
             weight[BALANCE_POSITION_TOP_LEFT],
             weight[BALANCE_POSITION_BOTTOM_LEFT]);
    } else {
      printf("🪄 ");
      for (int i = 0; i < len; i++) {
        printf("%02X ", data[i]);
      }
      printf("\n");
    }

    bool wiimote_button_down = (data[2] & 0x01) != 0;
    bool wiimote_button_up = (data[2] & 0x02) != 0;
    bool wiimote_button_right = (data[2] & 0x04) != 0;
    bool wiimote_button_left = (data[2] & 0x08) != 0;
    bool wiimote_button_plus = (data[2] & 0x10) != 0;
    bool wiimote_button_2 = (data[3] & 0x01) != 0;
    bool wiimote_button_1 = (data[3] & 0x02) != 0;
    bool wiimote_button_B = (data[3] & 0x04) != 0;
    bool wiimote_button_A = (data[3] & 0x08) != 0;
    bool wiimote_button_minus = (data[3] & 0x10) != 0;
    bool wiimote_button_home = (data[3] & 0x80) != 0;
    static bool rumble = false;
    if (wiimote_button_plus && !rumble) {
      wii.set_rumble(wiimote, true);
      rumble = true;
    }
    if (wiimote_button_minus && rumble) {
      wii.set_rumble(wiimote, false);
      rumble = false;
    }
  } else if (event_type == WIIMOTE_EVENT_INITIALIZE) {
    printf("🛜 INITIALIZE Bluetooth\n");
  } else if (event_type == WIIMOTE_EVENT_SCAN_START) {
    printf("🫱 Scan started. Accepting new devices.\n", wiimote);
  } else if (event_type == WIIMOTE_EVENT_NEW) {
    printf("🤝 Authenticating Wiimote %02X.\n", wiimote);
    wii.initiate_auth(wiimote);
  } else if (event_type == WIIMOTE_EVENT_CONNECT) {
    // add wiimote to connected list
    connected_wiimotes.push_back(wiimote);
    wii.set_led(wiimote, 1);
    // Stop scanning happens automatically
    // wii.scan(false);
    printf("✅ CONNECT(%d) Wiimote %02X.\n", connected_wiimotes.size(), wiimote);
  } else if (event_type == WIIMOTE_EVENT_DISCONNECT) {
    // remove wiimote from connected list
    auto iterator = std::find(connected_wiimotes.begin(), connected_wiimotes.end(), wiimote);
    if (iterator != connected_wiimotes.end()) {
      connected_wiimotes.erase(iterator);
    }

    printf("❌ DISCONNECT(%d) Wiimote: %02X\n", connected_wiimotes.size(), wiimote);
  } else if (event_type == WIIMOTE_EVENT_SCAN_STOP) {
    // try again if nothing connected
    if (connected_wiimotes.size() == 0) {
    //   printf("🔄 Scan Retry\n");
    //   wii.scan(true);
    // } else {
      printf("🛑 Scan Stop\n");
      is_scanning = false;
    }
  }
}

volatile bool scan_button_pressed = false;
void IRAM_ATTR press_scan() {
  scan_button_pressed = digitalRead(pair_button_gpio) == LOW;
}

void setup() {
  Serial.begin(115200);
  pinMode(pair_button_gpio, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(pair_button_gpio), press_scan, CHANGE);
  wii.init(wiimote_callback);
}

void loop() {
  wii.handle();
  if (scan_button_pressed) {
    scan_button_pressed = false;
    is_scanning = true;

    // loop through connected wiimotes and disconnect them
    for (auto& wiimote : connected_wiimotes) {
      wii.disconnect(wiimote);
    }
    wii.scan(true);
  }

  digitalWrite(LED_BUILTIN, is_scanning ? HIGH : LOW);
}

a few notes:
connection_count has been replaced with an array that keeps track of device handles that successfully connect so that disconnections don't naively subtract the count. Apologies I deleted your code that displays unused LED numbers because I'm testing with balance boards which have to use 1 regardless of their controller number. The controller type is tossed out after the process_remote_name_request_complete_event() unfortunately so I can't conditionally light up wiimotes differently than balance boards right now. And speaking of balance boards idk why but reconnection works fine in another codebase I have going but not this one. I haven't been able to spot the differences yet. But this sketch seems much harder to reconnect to and when I do, the balance board data doesn't stream in. Maybe platformIO's compiler just does a better job. anyway, the fact that it works somewhere means it's an issue on my end. I just want to be sure we have a solid demo that works for everyone. Tentatively I approve this PR though.

@wraybowling
Copy link
Contributor

Sorry I'm making so many replies. I got everything working on my end! @takeru this PR is good to merge! 🚀 I'm building another PR right now with a working example and some more clear documentation to close out #11

@wraybowling wraybowling mentioned this pull request Jun 27, 2025
@takeru takeru merged commit de08e93 into takeru:master Dec 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants