Implement Switch2 commands
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
1b65893a69
commit
950c5de1ea
2 changed files with 264 additions and 0 deletions
79
switch2_commands.cpp
Normal file
79
switch2_commands.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#include "switch2_commands.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
// Captured command vectors and acknowledgements:
|
||||
// https://github.com/ndeadly/switch2_controller_research/blob/d1c5a7f7ba298f83017fae84952a4e6d2ef8fc92/commands.md
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kSelectReportResponse[] = {
|
||||
0x03, 0x01, 0x00, 0x0A, 0x00, 0xF8, 0x00, 0x00,
|
||||
};
|
||||
constexpr uint8_t kInitializeUsbResponse[] = {
|
||||
0x03, 0x01, 0x00, 0x0D, 0x00, 0xF8, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Switch2VendorCommand switch2_classify_vendor_request(
|
||||
const uint8_t* data,
|
||||
std::size_t length) {
|
||||
if (data == nullptr || length < 8) {
|
||||
return Switch2VendorCommand::Unsupported;
|
||||
}
|
||||
if (data[0] != 0x03 || data[1] != 0x91 || data[2] != 0x00 ||
|
||||
data[4] != 0x00 || data[6] != 0x00 || data[7] != 0x00) {
|
||||
return Switch2VendorCommand::Unsupported;
|
||||
}
|
||||
if (length != static_cast<std::size_t>(8 + data[5])) {
|
||||
return Switch2VendorCommand::Unsupported;
|
||||
}
|
||||
|
||||
switch (data[3]) {
|
||||
case 0x0A:
|
||||
if (data[5] != 4 || data[9] != 0 || data[10] != 0 || data[11] != 0) {
|
||||
return Switch2VendorCommand::Unsupported;
|
||||
}
|
||||
if (data[8] == 0x05) return Switch2VendorCommand::SelectReport05;
|
||||
if (data[8] == 0x09) return Switch2VendorCommand::SelectReport09;
|
||||
return Switch2VendorCommand::Unsupported;
|
||||
|
||||
case 0x0D:
|
||||
if (data[5] == 8 && data[8] == 0x01) {
|
||||
return Switch2VendorCommand::InitializeUsb;
|
||||
}
|
||||
return Switch2VendorCommand::Unsupported;
|
||||
|
||||
default:
|
||||
return Switch2VendorCommand::Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t switch2_build_vendor_response(
|
||||
Switch2VendorCommand command,
|
||||
uint8_t* output,
|
||||
std::size_t capacity) {
|
||||
const uint8_t* response = nullptr;
|
||||
std::size_t response_length = 0;
|
||||
|
||||
switch (command) {
|
||||
case Switch2VendorCommand::Unsupported:
|
||||
return 0;
|
||||
case Switch2VendorCommand::SelectReport05:
|
||||
case Switch2VendorCommand::SelectReport09:
|
||||
response = kSelectReportResponse;
|
||||
response_length = sizeof(kSelectReportResponse);
|
||||
break;
|
||||
case Switch2VendorCommand::InitializeUsb:
|
||||
response = kInitializeUsbResponse;
|
||||
response_length = sizeof(kInitializeUsbResponse);
|
||||
break;
|
||||
}
|
||||
|
||||
if (output == nullptr || capacity < response_length) {
|
||||
return 0;
|
||||
}
|
||||
std::memcpy(output, response, response_length);
|
||||
return response_length;
|
||||
}
|
||||
185
tests/firmware/test_switch2_commands.cpp
Normal file
185
tests/firmware/test_switch2_commands.cpp
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#include "test_support.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "../../switch2_commands.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool switch2_classifies_captured_report_selection_requests() {
|
||||
// Given: captured common and Pro report-selection request vectors.
|
||||
static constexpr uint8_t select_05[] = {
|
||||
0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00,
|
||||
0x05, 0x00, 0x00, 0x00,
|
||||
};
|
||||
static constexpr uint8_t select_09[] = {
|
||||
0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00,
|
||||
0x09, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// When/Then: each captured vector selects only its represented report.
|
||||
CHECK(switch2_classify_vendor_request(select_05, sizeof(select_05)) ==
|
||||
Switch2VendorCommand::SelectReport05);
|
||||
CHECK(switch2_classify_vendor_request(select_09, sizeof(select_09)) ==
|
||||
Switch2VendorCommand::SelectReport09);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool switch2_builds_exact_report_selection_ack() {
|
||||
// Given: the two supported report-selection classifications.
|
||||
static constexpr uint8_t expected[] = {
|
||||
0x03, 0x01, 0x00, 0x0A, 0x00, 0xF8, 0x00, 0x00,
|
||||
};
|
||||
std::array<uint8_t, 12> output{};
|
||||
|
||||
// When: either response is built. Then: both equal the captured ACK.
|
||||
CHECK(switch2_build_vendor_response(
|
||||
Switch2VendorCommand::SelectReport05, output.data(), output.size()) ==
|
||||
sizeof(expected));
|
||||
CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0);
|
||||
output.fill(0);
|
||||
CHECK(switch2_build_vendor_response(
|
||||
Switch2VendorCommand::SelectReport09, output.data(), output.size()) ==
|
||||
sizeof(expected));
|
||||
CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool switch2_classifies_captured_and_opaque_usb_init_requests() {
|
||||
// Given: the pinned vector and another opaque host-address payload.
|
||||
static constexpr uint8_t captured[] = {
|
||||
0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00,
|
||||
0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
};
|
||||
static constexpr uint8_t opaque_address[] = {
|
||||
0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00,
|
||||
0x01, 0xA5, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60,
|
||||
};
|
||||
|
||||
// When/Then: both structurally valid requests initialize USB.
|
||||
CHECK(switch2_classify_vendor_request(captured, sizeof(captured)) ==
|
||||
Switch2VendorCommand::InitializeUsb);
|
||||
CHECK(switch2_classify_vendor_request(opaque_address, sizeof(opaque_address)) ==
|
||||
Switch2VendorCommand::InitializeUsb);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool switch2_builds_exact_usb_init_ack() {
|
||||
// Given: the captured initialization acknowledgement.
|
||||
static constexpr uint8_t expected[] = {
|
||||
0x03, 0x01, 0x00, 0x0D, 0x00, 0xF8, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
};
|
||||
std::array<uint8_t, sizeof(expected)> output{};
|
||||
|
||||
// When: the initialization response is built. Then: every byte is exact.
|
||||
CHECK(switch2_build_vendor_response(
|
||||
Switch2VendorCommand::InitializeUsb, output.data(), output.size()) ==
|
||||
sizeof(expected));
|
||||
CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool switch2_rejects_malformed_header_and_length_fields() {
|
||||
// Given: a structurally valid selection request as the mutation baseline.
|
||||
static constexpr uint8_t valid[] = {
|
||||
0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00,
|
||||
0x05, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// When/Then: truncation, extension, and every fixed header violation reject.
|
||||
CHECK(switch2_classify_vendor_request(nullptr, 0) == Switch2VendorCommand::Unsupported);
|
||||
for (std::size_t length = 0; length < sizeof(valid); ++length) {
|
||||
CHECK(switch2_classify_vendor_request(valid, length) ==
|
||||
Switch2VendorCommand::Unsupported);
|
||||
}
|
||||
std::array<uint8_t, sizeof(valid) + 1> extended{};
|
||||
std::memcpy(extended.data(), valid, sizeof(valid));
|
||||
CHECK(switch2_classify_vendor_request(extended.data(), extended.size()) ==
|
||||
Switch2VendorCommand::Unsupported);
|
||||
for (uint8_t offset : {0, 1, 2, 4, 6, 7}) {
|
||||
std::array<uint8_t, sizeof(valid)> malformed{};
|
||||
std::memcpy(malformed.data(), valid, sizeof(valid));
|
||||
++malformed[offset];
|
||||
CHECK(switch2_classify_vendor_request(malformed.data(), malformed.size()) ==
|
||||
Switch2VendorCommand::Unsupported);
|
||||
}
|
||||
for (uint8_t declared_length : {0x03, 0x05}) {
|
||||
std::array<uint8_t, sizeof(valid)> malformed{};
|
||||
std::memcpy(malformed.data(), valid, sizeof(valid));
|
||||
malformed[5] = declared_length;
|
||||
CHECK(switch2_classify_vendor_request(malformed.data(), malformed.size()) ==
|
||||
Switch2VendorCommand::Unsupported);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool switch2_rejects_unsupported_commands_and_payloads() {
|
||||
// Given: valid framing mutated to unsupported subcommands and payloads.
|
||||
std::array<uint8_t, 12> request = {
|
||||
0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00,
|
||||
0x05, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// When/Then: no command outside the captured subset is classified.
|
||||
request[3] = 0x03;
|
||||
CHECK(switch2_classify_vendor_request(request.data(), request.size()) ==
|
||||
Switch2VendorCommand::Unsupported);
|
||||
request[3] = 0x0A;
|
||||
request[8] = 0x08;
|
||||
CHECK(switch2_classify_vendor_request(request.data(), request.size()) ==
|
||||
Switch2VendorCommand::Unsupported);
|
||||
request[8] = 0x05;
|
||||
request[9] = 0x01;
|
||||
CHECK(switch2_classify_vendor_request(request.data(), request.size()) ==
|
||||
Switch2VendorCommand::Unsupported);
|
||||
|
||||
std::array<uint8_t, 16> init = {
|
||||
0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00,
|
||||
0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
|
||||
};
|
||||
CHECK(switch2_classify_vendor_request(init.data(), init.size()) ==
|
||||
Switch2VendorCommand::Unsupported);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool switch2_response_builder_rejects_unsupported_or_small_outputs() {
|
||||
// Given: a sentinel output buffer and every unsupported capacity.
|
||||
std::array<uint8_t, 12> output{};
|
||||
|
||||
// When/Then: unsupported, null, and short outputs produce no bytes or writes.
|
||||
output.fill(0xA5);
|
||||
CHECK(switch2_build_vendor_response(
|
||||
Switch2VendorCommand::Unsupported, output.data(), output.size()) == 0);
|
||||
for (uint8_t byte : output) CHECK(byte == 0xA5);
|
||||
CHECK(switch2_build_vendor_response(
|
||||
Switch2VendorCommand::InitializeUsb, nullptr, output.size()) == 0);
|
||||
for (std::size_t capacity = 0; capacity < 8; ++capacity) {
|
||||
output.fill(0xA5);
|
||||
CHECK(switch2_build_vendor_response(
|
||||
Switch2VendorCommand::SelectReport05, output.data(), capacity) == 0);
|
||||
for (uint8_t byte : output) CHECK(byte == 0xA5);
|
||||
}
|
||||
for (std::size_t capacity = 0; capacity < 12; ++capacity) {
|
||||
output.fill(0xA5);
|
||||
CHECK(switch2_build_vendor_response(
|
||||
Switch2VendorCommand::InitializeUsb, output.data(), capacity) == 0);
|
||||
for (uint8_t byte : output) CHECK(byte == 0xA5);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void run_switch2_command_tests(TestRunner& runner) {
|
||||
runner.run("Switch 2 captured report selections", switch2_classifies_captured_report_selection_requests);
|
||||
runner.run("Switch 2 report selection ACK", switch2_builds_exact_report_selection_ack);
|
||||
runner.run("Switch 2 captured USB initialization", switch2_classifies_captured_and_opaque_usb_init_requests);
|
||||
runner.run("Switch 2 USB initialization ACK", switch2_builds_exact_usb_init_ack);
|
||||
runner.run("Switch 2 malformed command framing", switch2_rejects_malformed_header_and_length_fields);
|
||||
runner.run("Switch 2 unsupported commands", switch2_rejects_unsupported_commands_and_payloads);
|
||||
runner.run("Switch 2 response capacity", switch2_response_builder_rejects_unsupported_or_small_outputs);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue