diff --git a/include/joybus/joybus.h b/include/joybus/joybus.h index f314808..bad5330 100644 --- a/include/joybus/joybus.h +++ b/include/joybus/joybus.h @@ -14,3 +14,4 @@ #include #include #include +#include diff --git a/include/joybus/target/pixelfx_gameid.h b/include/joybus/target/pixelfx_gameid.h new file mode 100644 index 0000000..f69567f --- /dev/null +++ b/include/joybus/target/pixelfx_gameid.h @@ -0,0 +1,73 @@ +/** + * @defgroup joybus_target_pixelfx_gameid PixelFX Game ID Target + * @ingroup joybus_target + * + * Joybus target which listens for the PixelFX game ID command. N64 flash carts + * and Swiss on the GameCube send it over the controller port so that attached + * devices know which game is running. The command carries 10 bytes of game ID + * and expects no reply, so this target never sends one. + * + * The ID bytes depend on the console. An N64 sends CRC1 and CRC2 from the ROM + * header, the media format and the country code. A GameCube sends the 8-byte + * boot hash, the console ID and the country code. All zeros clears the game ID. + * + * An N64 sends the command to the first controller port only. Swiss sends it to every port. + * + * @{ + */ + +#pragma once + +#include + +#include + +struct joybus_target_pixelfx_gameid; + +/// Macro to cast from a generic Joybus target to a PixelFX game ID target +#define JOYBUS_TARGET_PIXELFX_GAMEID(target) ((struct joybus_target_pixelfx_gameid *)(target)) + +/// Size of a PixelFX game ID, in bytes +#define JOYBUS_PIXELFX_GAMEID_SIZE 10 + +/** + * Callback type for game ID received events. + * + * Runs in interrupt context, so it must return quickly and must not block. + * Copy or flag the ID and act on it from the main loop. + * + * @param target the target that received the game ID, with the ID in its game_id field + */ +typedef void (*joybus_target_pixelfx_gameid_received_cb)(struct joybus_target_pixelfx_gameid *target); + +/** + * PixelFX game ID Joybus target. + */ +struct joybus_target_pixelfx_gameid { + /// Base target interface + struct joybus_target base; + + /// The last game ID received, all zeros until one arrives + uint8_t game_id[JOYBUS_PIXELFX_GAMEID_SIZE]; + + /// Callback for game ID received events + joybus_target_pixelfx_gameid_received_cb on_received; +}; + +/** + * Initialize a PixelFX game ID target. + * + * @param target the target to initialize + */ +void joybus_target_pixelfx_gameid_init(struct joybus_target_pixelfx_gameid *target); + +/** + * Set the game ID received callback for the target. + * + * @param target the target to set the callback for + * @param callback the callback function + */ +void joybus_target_pixelfx_gameid_set_received_cb(struct joybus_target_pixelfx_gameid *target, + joybus_target_pixelfx_gameid_received_cb callback); + +/** @} */ diff --git a/libjoybus.slcc b/libjoybus.slcc index dbf7d93..e28b5d6 100644 --- a/libjoybus.slcc +++ b/libjoybus.slcc @@ -27,3 +27,4 @@ source: - path: src/target/gcn_controller.c - path: src/target/n64_controller.c - path: src/target/n64_rumble_pak.c + - path: src/target/pixelfx_gameid.c diff --git a/src/target/pixelfx_gameid.c b/src/target/pixelfx_gameid.c new file mode 100644 index 0000000..b99186d --- /dev/null +++ b/src/target/pixelfx_gameid.c @@ -0,0 +1,48 @@ +#include + +#include +#include +#include +#include +#include + +JOYBUS_RAM_FUNC +static int pixelfx_gameid_byte_received(struct joybus_target *target, const uint8_t *command, uint8_t bytes_read, + joybus_target_response_cb send_response, void *user_data) +{ + struct joybus_target_pixelfx_gameid *gameid = JOYBUS_TARGET_PIXELFX_GAMEID(target); + + // Decline every other command + if (command[0] != JOYBUS_CMD_PIXELFX_GAMEID) + return -JOYBUS_ERR_NOT_SUPPORTED; + + // Wait until the full command is received + if (bytes_read < JOYBUS_CMD_PIXELFX_GAMEID_TX) + return JOYBUS_CMD_PIXELFX_GAMEID_TX - bytes_read; + + // Store the game ID and notify, the sender expects no reply + memcpy(gameid->game_id, &command[1], JOYBUS_PIXELFX_GAMEID_SIZE); + if (gameid->on_received) + gameid->on_received(gameid); + + return 0; +} + +static const struct joybus_target_api pixelfx_gameid_api = { + .byte_received = pixelfx_gameid_byte_received, +}; + +void joybus_target_pixelfx_gameid_init(struct joybus_target_pixelfx_gameid *target) +{ + // Start from a clean state + memset(target, 0, sizeof(*target)); + + // Set the base target API implementation + target->base.api = &pixelfx_gameid_api; +} + +void joybus_target_pixelfx_gameid_set_received_cb(struct joybus_target_pixelfx_gameid *target, + joybus_target_pixelfx_gameid_received_cb callback) +{ + target->on_received = callback; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0128dbd..db56eeb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,3 +34,6 @@ add_libjoybus_test(test_n64_controller target/test_n64_controller.c) # N64 rumble pak tests add_libjoybus_test(test_n64_rumble_pak target/test_n64_rumble_pak.c) + +# PixelFX game ID target tests +add_libjoybus_test(test_pixelfx_gameid target/test_pixelfx_gameid.c) diff --git a/test/target/test_pixelfx_gameid.c b/test/target/test_pixelfx_gameid.c new file mode 100644 index 0000000..cdf2c72 --- /dev/null +++ b/test/target/test_pixelfx_gameid.c @@ -0,0 +1,64 @@ +#include + +#include +#include +#include +#include + +#include "unity.h" + +#include "harness.h" + +// The game ID target under test +static struct joybus_target_pixelfx_gameid gameid; + +// Spy for the received callback +static int received_count; +static void on_received(struct joybus_target_pixelfx_gameid *target) +{ + received_count++; +} + +void setUp(void) +{ + joybus_target_pixelfx_gameid_init(&gameid); + joybus_target_pixelfx_gameid_set_received_cb(&gameid, on_received); + harness_reset(JOYBUS_TARGET(&gameid)); + received_count = 0; +} + +void tearDown(void) +{ +} + +// Test that a game ID command stores the ID, fires the callback, and sends no reply +static void test_gameid_received(void) +{ + // F-ZERO X, from the PixelFX README + uint8_t command[] = {JOYBUS_CMD_PIXELFX_GAMEID, 0xB3, 0x0E, 0xD9, 0x78, 0x30, 0x03, 0xC9, 0xF9, 0x43, 0x45}; + TEST_ASSERT_EQUAL(0, send_command(command, sizeof(command))); + + TEST_ASSERT_EQUAL_HEX8_ARRAY(&command[1], gameid.game_id, JOYBUS_PIXELFX_GAMEID_SIZE); + TEST_ASSERT_EQUAL(1, received_count); + TEST_ASSERT_EQUAL(0, response.count); +} + +// Test that any other command is declined on its first byte +static void test_other_command_declined(void) +{ + uint8_t command[] = {JOYBUS_CMD_IDENTIFY}; + TEST_ASSERT_EQUAL(-JOYBUS_ERR_NOT_SUPPORTED, send_command(command, sizeof(command))); + + TEST_ASSERT_EQUAL(0, received_count); + TEST_ASSERT_EQUAL(0, response.count); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_gameid_received); + RUN_TEST(test_other_command_declined); + + return UNITY_END(); +}