Files
wiicompiled/runtime/tests/bluetooth_controller_tests.cpp
T
2026-10-20 02:05:29 +02:00

369 lines
14 KiB
C++

#include "bluetooth/bt_controller.h"
#include "bluetooth/bt_sdp.h"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <vector>
namespace {
using Bytes = std::vector<uint8_t>;
using Bluetooth::BdAddr;
using Bluetooth::Controller;
using Bluetooth::LinkState;
int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
struct FakeHost : Bluetooth::RemoteHost {
std::array<bool, Bluetooth::kRemoteSlots> attached{};
std::array<bool, Bluetooth::kRemoteSlots> wake{};
std::array<std::deque<Bytes>, Bluetooth::kRemoteSlots> input;
std::vector<std::pair<size_t, Bytes>> output;
std::array<LinkState, Bluetooth::kRemoteSlots> states{};
std::vector<size_t> guestDisconnects;
bool inquiry = false;
bool IsAttached(size_t slot) const override { return attached[slot]; }
bool ConsumeWake(size_t slot) override {
const bool w = wake[slot];
wake[slot] = false;
return w;
}
bool PopInputReport(size_t slot, Bytes& report) override {
if (input[slot].empty()) return false;
report = input[slot].front();
input[slot].pop_front();
return true;
}
void SendOutputReport(size_t slot, const uint8_t* report, size_t size) override {
output.emplace_back(slot, Bytes(report, report + size));
}
void OnLinkStateChanged(size_t slot, LinkState state) override { states[slot] = state; }
void OnGuestDisconnect(size_t slot) override { guestDisconnects.push_back(slot); }
void OnGuestInquiry(bool active) override { inquiry = active; }
const char* DeviceName(size_t) const override { return "Nintendo RVL-CNT-01"; }
};
uint16_t Le16(const Bytes& b, size_t i) {
return static_cast<uint16_t>(b[i] | (b[i + 1] << 8));
}
void PutLe16(Bytes& b, uint16_t v) {
b.push_back(static_cast<uint8_t>(v));
b.push_back(static_cast<uint8_t>(v >> 8));
}
Bytes Command(uint16_t opcode, Bytes params) {
Bytes packet;
PutLe16(packet, opcode);
packet.push_back(static_cast<uint8_t>(params.size()));
packet.insert(packet.end(), params.begin(), params.end());
return packet;
}
struct Guest {
Controller& controller;
uint64_t now = 1000;
std::vector<Bytes> Run(uint16_t opcode, Bytes params = {}) {
const Bytes packet = Command(opcode, std::move(params));
controller.ExecuteCommand(packet.data(), packet.size(), now);
return Events();
}
std::vector<Bytes> Events() {
std::vector<Bytes> events;
Bytes event;
while (controller.PopEvent(event)) events.push_back(event);
return events;
}
std::vector<Bytes> Acl() {
std::vector<Bytes> packets;
Bytes packet;
while (controller.PopAcl(packet)) packets.push_back(packet);
return packets;
}
void Tick() {
now += 1000;
controller.Tick(now);
}
void SendL2cap(uint16_t handle, uint16_t cid, const Bytes& payload) {
Bytes packet;
PutLe16(packet, static_cast<uint16_t>(handle | 0x2000));
PutLe16(packet, static_cast<uint16_t>(payload.size() + 4));
PutLe16(packet, static_cast<uint16_t>(payload.size()));
PutLe16(packet, cid);
packet.insert(packet.end(), payload.begin(), payload.end());
controller.ReceiveAcl(packet.data(), packet.size(), now);
}
void Signal(uint16_t handle, uint8_t code, uint8_t ident, const Bytes& data) {
Bytes payload = {code, ident};
PutLe16(payload, static_cast<uint16_t>(data.size()));
payload.insert(payload.end(), data.begin(), data.end());
SendL2cap(handle, 0x0001, payload);
}
};
bool IsCommandComplete(const Bytes& e, uint16_t opcode) {
return e.size() >= 6 && e[0] == 0x0E && Le16(e, 3) == opcode && e[5] == 0x00;
}
bool IsCommandStatusOk(const Bytes& e, uint16_t opcode) {
return e.size() == 6 && e[0] == 0x0F && e[2] == 0x00 && Le16(e, 4) == opcode;
}
// Splits a signalling ACL packet from the remote into (code, ident, data).
struct Signal {
uint8_t code = 0;
uint8_t ident = 0;
Bytes data;
};
std::vector<Signal> Signals(const std::vector<Bytes>& acl) {
std::vector<Signal> out;
for (const Bytes& packet : acl) {
if (packet.size() < 8 || Le16(packet, 6) != 0x0001) continue;
size_t i = 8;
while (i + 4 <= packet.size()) {
Signal s;
s.code = packet[i];
s.ident = packet[i + 1];
const uint16_t length = Le16(packet, i + 2);
s.data.assign(packet.begin() + static_cast<std::ptrdiff_t>(i + 4),
packet.begin() + static_cast<std::ptrdiff_t>(i + 4 + length));
out.push_back(s);
i += 4 + length;
}
}
return out;
}
// Opens one HID channel the remote requested and completes both configurations.
uint16_t AcceptRemoteChannel(Guest& guest, uint16_t handle, uint16_t psm, uint16_t guestCid) {
guest.Events();
const auto requests = Signals(guest.Acl());
CHECK(requests.size() == 1);
if (requests.size() != 1) return 0;
CHECK(requests[0].code == 0x02 && Le16(requests[0].data, 0) == psm);
const uint16_t remoteCid = Le16(requests[0].data, 2);
Bytes rsp;
PutLe16(rsp, guestCid);
PutLe16(rsp, remoteCid);
PutLe16(rsp, 0);
PutLe16(rsp, 0);
guest.Signal(handle, 0x03, requests[0].ident, rsp);
const auto config = Signals(guest.Acl());
CHECK(config.size() == 1 && config[0].code == 0x04 && Le16(config[0].data, 0) == guestCid);
Bytes cfg;
PutLe16(cfg, remoteCid);
PutLe16(cfg, 0);
cfg.insert(cfg.end(), {0x01, 0x02, 0xA0, 0x02});
guest.Signal(handle, 0x04, 0x40, cfg);
const auto cfgRsp = Signals(guest.Acl());
CHECK(cfgRsp.size() == 1 && cfgRsp[0].code == 0x05 && cfgRsp[0].ident == 0x40 &&
Le16(cfgRsp[0].data, 0) == guestCid && Le16(cfgRsp[0].data, 4) == 0);
Bytes ourCfgRsp;
PutLe16(ourCfgRsp, remoteCid);
PutLe16(ourCfgRsp, 0);
PutLe16(ourCfgRsp, 0);
if (!config.empty()) guest.Signal(handle, 0x05, config[0].ident, ourCfgRsp);
return remoteCid;
}
void TestBootAndConnection() {
FakeHost host;
Controller controller(host);
Guest guest{controller};
controller.Open(guest.now);
auto e = guest.Run(0x0C03);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x0C03));
e = guest.Run(0x1005);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1005) && Le16(e[0], 6) == 339 && Le16(e[0], 9) == 10);
e = guest.Run(0x1009);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1009) && e[0][11] == 0xFF);
e = guest.Run(0x1001);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1001) && Le16(e[0], 7) == 0x40A7);
e = guest.Run(0x1003);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1003));
e = guest.Run(0x0C0D, {0, 0, 0, 0, 0, 0, 1});
CHECK(e.size() == 2 && e[0][0] == 0x15 && e[0][2] == 4 && IsCommandComplete(e[1], 0x0C0D) &&
Le16(e[1], 8) == 4);
Bytes slot0Key;
if (!e.empty() && e[0].size() >= 25) slot0Key.assign(e[0].begin() + 9, e[0].begin() + 25);
e = guest.Run(0xFC4F, {0x00});
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0xFC4F));
// Attached remote, page scan off: nothing yet.
host.attached[0] = true;
host.wake[0] = true;
guest.Tick();
CHECK(guest.Events().empty());
e = guest.Run(0x0C1A, {0x02});
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x0C1A));
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x04 && e[0][7] == 0x00);
const BdAddr addr = Controller::RemoteAddress(0);
Bytes accept(addr.begin(), addr.end());
accept.push_back(0x00);
e = guest.Run(0x0409, accept);
CHECK(e.size() == 3 && IsCommandStatusOk(e[0], 0x0409) && e[1][0] == 0x12 && e[2][0] == 0x03 &&
e[2][2] == 0x00 && Le16(e[2], 3) == 0x0100);
CHECK(host.states[0] == LinkState::Connecting);
// Authentication with the stored key.
e = guest.Run(0x0411, {0x00, 0x01});
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0411) && e[1][0] == 0x17);
Bytes reply(addr.begin(), addr.end());
reply.insert(reply.end(), slot0Key.begin(), slot0Key.end());
e = guest.Run(0x040B, reply);
CHECK(e.size() == 2 && IsCommandComplete(e[0], 0x040B) && e[1][0] == 0x06 && e[1][2] == 0x00);
guest.Tick();
const uint16_t controlCid = AcceptRemoteChannel(guest, 0x0100, 0x0011, 0x0041);
CHECK(host.states[0] == LinkState::Connecting);
guest.Tick();
const uint16_t interruptCid = AcceptRemoteChannel(guest, 0x0100, 0x0013, 0x0042);
CHECK(controlCid != 0 && interruptCid != 0);
CHECK(host.states[0] == LinkState::Ready);
// Input passthrough.
host.input[0].push_back({0x30, 0x00, 0x08});
guest.Tick();
guest.Events();
auto acl = guest.Acl();
CHECK(acl.size() == 1);
if (acl.size() == 1) {
const Bytes& p = acl[0];
CHECK(Le16(p, 0) == 0x2100 && Le16(p, 6) == 0x0042 && p[8] == 0xA1 && p[9] == 0x30 && p[11] == 0x08);
}
// Output passthrough on the interrupt channel, and a SET_REPORT on control.
guest.SendL2cap(0x0100, interruptCid, {0xA2, 0x11, 0x10});
guest.SendL2cap(0x0100, controlCid, {0x52, 0x15, 0x00});
CHECK(host.output.size() == 2 && host.output[0].second == Bytes({0x11, 0x10}) &&
host.output[1].second == Bytes({0x15, 0x00}));
acl = guest.Acl();
CHECK(acl.size() == 1 && acl[0].size() == 9 && acl[0][8] == 0x00);
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x13 && e[0][2] == 1 && Le16(e[0], 3) == 0x0100 && Le16(e[0], 5) == 2);
// The game drops the remote: a real one powers off.
e = guest.Run(0x0406, {0x00, 0x01, 0x13});
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0406) && e[1][0] == 0x05 && e[1][5] == 0x16);
CHECK(host.guestDisconnects.size() == 1 && host.states[0] == LinkState::Down);
guest.Tick();
CHECK(guest.Events().empty());
}
void TestInquiryAndPaging() {
FakeHost host;
Controller controller(host);
Guest guest{controller};
controller.Open(guest.now);
guest.Run(0x0C45, {0x01});
host.attached[1] = true;
host.wake[1] = true;
guest.Tick(); // consumes the wake; page scan is off
auto e = guest.Run(0x0401, {0x00, 0x8B, 0x9E, 0x03, 0x01});
CHECK(e.size() == 1 && IsCommandStatusOk(e[0], 0x0401) && host.inquiry);
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x22 && e[0][8] == 0x01);
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x01);
CHECK(!host.inquiry);
Bytes create;
const BdAddr addr = Controller::RemoteAddress(1);
create.insert(create.end(), addr.begin(), addr.end());
create.insert(create.end(), {0x18, 0xCC, 0x01, 0x00, 0x00, 0x00, 0x00});
e = guest.Run(0x0405, create);
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0405) && e[1][0] == 0x03 && e[1][2] == 0x00 &&
Le16(e[1], 3) == 0x0101);
// Paging a remote that is not there times out.
const BdAddr missing = Controller::RemoteAddress(2);
Bytes create2(missing.begin(), missing.end());
create2.insert(create2.end(), {0x18, 0xCC, 0x01, 0x00, 0x00, 0x00, 0x00});
e = guest.Run(0x0405, create2);
CHECK(e.size() == 1);
guest.now += 6'000'000;
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x03 && e[0][2] == 0x04);
// The guest opens SDP itself on a paged remote.
Bytes sdpReq;
PutLe16(sdpReq, 0x0001);
PutLe16(sdpReq, 0x0050);
guest.Signal(0x0101, 0x02, 0x07, sdpReq);
const auto rsp = Signals(guest.Acl());
CHECK(rsp.size() == 2 && rsp[0].code == 0x03 && Le16(rsp[0].data, 4) == 0 && rsp[1].code == 0x04);
}
void TestSdp() {
// ServiceAttributeRequest for the whole HID record.
const Bytes request = {0x04, 0x00, 0x01, 0x00, 0x0E, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF,
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF, 0x00};
const Bytes response = Bluetooth::Sdp::HandleRequest(request.data(), request.size(), 4096);
CHECK(response.size() > 10 && response[0] == 0x05);
if (response.size() > 10) {
const uint16_t count = static_cast<uint16_t>((response[5] << 8) | response[6]);
CHECK(count == 0x01CC + 3 && response[7] == 0x36 && response[8] == 0x01 && response[9] == 0xCC);
CHECK(response.back() == 0x00);
}
CHECK(Bluetooth::Sdp::WiiRemoteHidDescriptor().size() == 0xD9);
// Continuation with a small MTU reassembles to the same list.
Bytes assembled;
Bytes cont = {0x00};
for (int guard = 0; guard < 64; ++guard) {
Bytes req = {0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40,
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF};
req.insert(req.end(), cont.begin(), cont.end());
req[4] = static_cast<uint8_t>(req.size() - 5);
const Bytes part = Bluetooth::Sdp::HandleRequest(req.data(), req.size(), 48);
CHECK(part.size() <= 48 && part[0] == 0x05);
const uint16_t count = static_cast<uint16_t>((part[5] << 8) | part[6]);
assembled.insert(assembled.end(), part.begin() + 7, part.begin() + 7 + count);
cont.assign(part.begin() + 7 + count, part.end());
if (cont.size() == 1 && cont[0] == 0) break;
}
CHECK(assembled.size() == 0x01CC + 3);
// Device ID record via ServiceSearchAttribute.
const Bytes search = {0x06, 0x00, 0x03, 0x00, 0x0F, 0x35, 0x03, 0x19, 0x12, 0x00, 0xFF, 0xFF,
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF, 0x00};
const Bytes found = Bluetooth::Sdp::HandleRequest(search.data(), search.size(), 4096);
CHECK(found.size() > 12 && found[0] == 0x07 && found[7] == 0x35 && found[9] == 0x36 && found[10] == 0x00 &&
found[11] == 0x5A);
}
} // namespace
int main() {
TestBootAndConnection();
TestInquiryAndPaging();
TestSdp();
if (g_failures != 0) {
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
return EXIT_FAILURE;
}
std::printf("bluetooth controller tests passed\n");
return EXIT_SUCCESS;
}