Skip to content

Instantly share code, notes, and snippets.

@GaryOderNichts
Last active December 3, 2025 22:07
Show Gist options
  • Select an option

  • Save GaryOderNichts/643b624d82cdc8849a101c8cbd4a7778 to your computer and use it in GitHub Desktop.

Select an option

Save GaryOderNichts/643b624d82cdc8849a101c8cbd4a7778 to your computer and use it in GitHub Desktop.
Launch Wii U titles using NFC tags from the Wii U menu.

NfcLaunch

Launch Wii U titles using NFC tags from the Wii U menu.

Usage

Tags should be type 2 NTAGs and contain a single record with the URI wiiu://<titleid> where <titleid> is the Title ID of the title to launch in hex. For example wiiu://0005000010145000 to launch Super Smash Bros. for Wii U (EUR).

#include <wups.h>
#include <string.h>
#include <stdlib.h>
#include <nfc/nfc.h>
#include <coreinit/alarm.h>
#include <coreinit/thread.h>
#include <coreinit/time.h>
#include <coreinit/title.h>
#include <coreinit/debug.h>
#include <coreinit/mcp.h>
#include <sysapp/title.h>
#include <sysapp/launch.h>
#include <nn/acp.h>
#if 0
#include <nsysccr/cdc.h>
#endif
#include <notifications/notifications.h>
WUPS_PLUGIN_NAME("NfcLaunch");
WUPS_PLUGIN_DESCRIPTION("Launch titles using NFC tags");
WUPS_PLUGIN_VERSION("v1.0");
WUPS_PLUGIN_AUTHOR("GaryOderNichts");
WUPS_PLUGIN_LICENSE("GPLv3");
OSAlarm gProcAlarm;
OSAlarm gSearchAlarm;
static bool LaunchTitle(uint64_t titleId)
{
if (!SYSCheckTitleExists(titleId)) {
OSReport("Title does not exist\n");
return false;
}
MCPTitleListType titleInfo;
int32_t handle = MCP_Open();
MCPError err = MCP_GetTitleInfo(handle, titleId, &titleInfo);
MCP_Close(handle);
if (err != 0) {
OSReport("Failed to get title info\n");
return false;
}
char metaDir[256];
if (ACPGetTitleMetaDir(titleId, metaDir, sizeof(metaDir) - 1) != ACP_RESULT_SUCCESS) {
OSReport("Failed to get title meta dir\n");
return false;
}
ACPAssignTitlePatch(&titleInfo);
_SYSLaunchTitleByPathFromLauncher(titleInfo.path, strlen(titleInfo.path));
return true;
}
static uint8_t *TLV_Parse(uint8_t *tlv, uint8_t *outTag, uint16_t *outLen)
{
uint8_t tag = *tlv++;
uint16_t len = *tlv++;
// If the length is 0xFF, 2 bytes with length follow
if (len == 0xFF) {
len = ((uint16_t)tlv[0] << 8) | tlv[1];
tlv += 2;
}
if (outTag) {
*outTag = tag;
}
if (outLen) {
*outLen = len;
}
// Return pointer to value
return tlv;
}
static uint8_t *NDEF_GetType(uint8_t *rec, uint8_t *outTNF, uint8_t *outTypeLen)
{
uint8_t recHdr = *rec++;
uint8_t typeLen = *rec++;
// Skip payload length
if (recHdr & 0x10) {
rec += 1;
} else {
rec += 4;
}
// Skip ID length, if it exists
if (recHdr & 0x08) {
rec += 1;
}
if (outTNF) {
*outTNF = recHdr & 0x07;
}
if (outTypeLen) {
*outTypeLen = typeLen;
}
// Return pointer to type field
return rec;
}
static uint8_t *NDEF_GetPayload(uint8_t *rec, uint32_t* outPayloadLen)
{
uint8_t recHdr = *rec++;
uint8_t typeLen = *rec++;
// Payload length can be 1 or 4 bytes long, depending on the SR bit
uint32_t payloadLen;
if (recHdr & 0x10) {
payloadLen = *rec++;
} else {
payloadLen = ((uint32_t)rec[0] << 24) | ((uint32_t)rec[1] << 16) | ((uint32_t)rec[2] << 8) | rec[3];
rec += 4;
}
// Read ID length, if it exists
uint8_t idLen;
if (recHdr & 0x08) {
idLen = *rec++;
} else {
idLen = 0;
}
if (outPayloadLen) {
*outPayloadLen = payloadLen;
}
// Return pointer to payload
return rec + idLen + typeLen;
}
static void NdefReadCallback(VPADChan chan, NFCError error, uint32_t responseSize, void *responseData, void *userContext)
{
if (error != 0) {
OSReport("Failed to read NDEF message\n");
return;
}
// Parse the TLV again to get the V field (NDEF record)
uint8_t *rec = TLV_Parse((uint8_t *)responseData, NULL, NULL);
// Get the NDEF record type
uint8_t tnf, typeLen;
uint8_t *type = NDEF_GetType(rec, &tnf, &typeLen);
// We only support well-known types
if (tnf != 0x01) {
OSReport("Unsupported TNF\n");
return;
}
// Make sure this is a URI
if ((typeLen != 0x01) || (*type != 'U')) {
OSReport("Unsupported type\n");
return;
}
// Get the NDEF payload
uint32_t payloadLen;
uint8_t *payload = NDEF_GetPayload(rec, &payloadLen);
// We can now parse the payload
uint8_t identifier = *payload++;
if (identifier != 0) {
OSReport("Unsupported identifier\n");
return;
}
if (memcmp(payload, "wiiu://", 7) != 0) {
OSReport("Unsupported prefix\n");
return;
}
// Append the rest of the payloa
char titleIdStr[64];
if (payloadLen - 7 - 1 > 64) {
OSReport("Payload too long\n");
return;
}
strncpy(titleIdStr, (char *)payload + 7, payloadLen - 7 - 1);
uint64_t tid = strtoull(titleIdStr, NULL, 16);
OSReport("Launching: %s %llx\n", titleIdStr, tid);
if (NotificationModule_InitLibrary() == NOTIFICATION_MODULE_RESULT_SUCCESS) {
NotificationModule_AddInfoNotification("NfcLaunch: Starting title...");
}
if (!LaunchTitle(tid)) {
if (NotificationModule_InitLibrary() == NOTIFICATION_MODULE_RESULT_SUCCESS) {
NotificationModule_AddErrorNotification("NfcLaunch: Failed to start title");
}
}
}
void NdefDetectionCallback(VPADChan chan, NFCError error, uint32_t responseSize, void *responseData, void *userContext)
{
if (error != 0 || responseSize < 16) {
OSReport("Failed to read capability container: %x %u\n", error, responseSize);
return;
}
// Verify that the CC contains the NFC defined magic and contains a supported version
uint8_t *cc = (uint8_t *)responseData;
if (cc[0] != 0xE1 || cc[1] >> 4 != 0x1) {
OSReport("Tag not supported\n");
return;
}
// Parse TLV
uint8_t tag;
uint16_t length;
TLV_Parse((uint8_t *)responseData + 4, &tag, &length);
// Check the tag field for the NDEF type
if (tag != 0x03) {
OSReport("Tag does not contain an NDEF message\n");
return;
}
uint8_t blockCount = (length + 4) / 4;
// Read the full NDEF data using the NTAG FAST_READ command
uint8_t command[3];
command[0] = 0x3A; // Command (FAST_READ)
command[1] = 0x04; // Start Block (04)
command[2] = 0x04 + blockCount; // End Block (04 + block count)
NFCError err = NFCSendRawData(VPAD_CHAN_0, FALSE, 500, 500,
sizeof(command), blockCount * 4, command,
NdefReadCallback, NULL);
if (err != 0) {
OSReport("NFCSendRawData(1) failed: %x\n", err);
}
}
void GetTagInfoCallback(VPADChan chan, NFCError error, NFCTagInfo *tagInfo, void *userContext)
{
if (error != 0) {
return;
}
OSReport("Found tag!\n");
OSReport(" UID: %02x:%02x:%02x:%02x:%02x:%02x:%02x\n",
tagInfo->uid[0], tagInfo->uid[1], tagInfo->uid[2], tagInfo->uid[3],
tagInfo->uid[4], tagInfo->uid[5], tagInfo->uid[6]);
OSReport(" Protocol: %d Technology: %d\n", tagInfo->protocol, tagInfo->technology);
if (tagInfo->protocol == NFC_PROTOCOL_T2T) {
// Read the first 16-bytes from the tag for NDEF detection
uint8_t command[2];
command[0] = 0x30; // Command (READ)
command[1] = 0x03; // Block Number (03 = Capability Container)
NFCError err = NFCSendRawDataWithPrePollingEx(VPAD_CHAN_0, TRUE, 500, 500,
sizeof(command), 16, command,
NdefDetectionCallback, NULL);
if (err != 0) {
OSReport("NFCSendRawData(0) failed: %x\n", err);
}
}
}
void SearchCallback(OSAlarm *, OSContext *)
{
if (!NFCIsInit(VPAD_CHAN_0)) {
NFCInit(VPAD_CHAN_0);
return;
}
// Start searching for tags by getting their info
NFCGetTagInfo(VPAD_CHAN_0, 500, GetTagInfoCallback, NULL);
}
void ProcCallback(OSAlarm *, OSContext *)
{
NFCProc(VPAD_CHAN_0);
}
bool IsWiiUMenu()
{
return OSGetTitleID() == _SYSGetSystemApplicationTitleId(SYSTEM_APP_ID_WII_U_MENU);
}
bool IsHAS()
{
return OSGetTitleID() == _SYSGetSystemApplicationTitleId(SYSTEM_APP_ID_HEALTH_AND_SAFETY);
}
INITIALIZE_PLUGIN()
{
}
DEINITIALIZE_PLUGIN()
{
}
ON_APPLICATION_START()
{
if (!IsWiiUMenu()) {
#if 0
if (!IsHAS()) {
CCRCDCSysConsoleShutdownInd(CCR_CDC_DESTINATION_DRC0);
}
#endif
return;
}
#if 0
CCRCDCWowlWakeDrcArg arg;
arg.state = CCR_CDC_WAKE_STATE_ACTIVE;
CCRCDCWowlWakeDrc(&arg);
#endif
NFCInit(VPAD_CHAN_0);
OSCreateAlarm(&gProcAlarm);
OSSetPeriodicAlarm(&gProcAlarm, 0, OSMillisecondsToTicks(15), ProcCallback);
OSCreateAlarm(&gSearchAlarm);
OSSetPeriodicAlarm(&gSearchAlarm, 0, OSSecondsToTicks(2), SearchCallback);
}
ON_APPLICATION_ENDS()
{
if (!IsWiiUMenu()) {
return;
}
OSCancelAlarm(&gProcAlarm);
OSCancelAlarm(&gSearchAlarm);
}
#-------------------------------------------------------------------------------
.SUFFIXES:
#-------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITPRO)),)
$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/devkitpro")
endif
TOPDIR ?= $(CURDIR)
include $(DEVKITPRO)/wups/share/wups_rules
WUT_ROOT := $(DEVKITPRO)/wut
WUMS_ROOT := $(DEVKITPRO)/wums
#-------------------------------------------------------------------------------
# TARGET is the name of the output
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# DATA is a list of directories containing data files
# INCLUDES is a list of directories containing header files
#-------------------------------------------------------------------------------
TARGET := NfcLaunch
BUILD := build
SOURCES := source
DATA := data
INCLUDES := source
#-------------------------------------------------------------------------------
# options for code generation
#-------------------------------------------------------------------------------
CFLAGS := -Wall -O2 -ffunction-sections $(MACHDEP)
CFLAGS += $(INCLUDE) -D__WIIU__ -D__WUT__ -D__WUPS__
CXXFLAGS := $(CFLAGS)
ASFLAGS := $(ARCH)
LDFLAGS = $(ARCH) $(RPXSPECS) -Wl,-Map,$(notdir $*.map) $(WUPSSPECS)
ifeq ($(DEBUG),1)
CXXFLAGS += -DDEBUG -g
CFLAGS += -DDEBUG -g
endif
ifeq ($(DEBUG),VERBOSE)
CXXFLAGS += -DDEBUG -DVERBOSE_DEBUG -g
CFLAGS += -DDEBUG -DVERBOSE_DEBUG -g
endif
LIBS := -lnotifications -lwups -lwut
#-------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level
# containing include and lib
#-------------------------------------------------------------------------------
LIBDIRS := $(PORTLIBS) $(WUMS_ROOT) $(WUPS_ROOT) $(WUT_ROOT)
#-------------------------------------------------------------------------------
# no real need to edit anything past this point unless you need to add additional
# rules for different file extensions
#-------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#-------------------------------------------------------------------------------
export OUTPUT := $(CURDIR)/$(TARGET)
export TOPDIR := $(CURDIR)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
#-------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#-------------------------------------------------------------------------------
# ifeq ($(strip $(CPPFILES)),)
#-------------------------------------------------------------------------------
# export LD := $(CC)
#-------------------------------------------------------------------------------
# else
#-------------------------------------------------------------------------------
export LD := $(CXX)
#-------------------------------------------------------------------------------
# endif
#-------------------------------------------------------------------------------
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SRC)
export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES)))
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
.PHONY: $(BUILD) clean all
#-------------------------------------------------------------------------------
all: $(BUILD)
$(BUILD):
@$(shell [ ! -d $(BUILD) ] && mkdir -p $(BUILD))
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
#-------------------------------------------------------------------------------
clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).wps $(TARGET).elf
#-------------------------------------------------------------------------------
else
.PHONY: all
DEPENDS := $(OFILES:.o=.d)
#-------------------------------------------------------------------------------
# main targets
#-------------------------------------------------------------------------------
all : $(OUTPUT).wps
$(OUTPUT).wps : $(OUTPUT).elf
$(OUTPUT).elf : $(OFILES)
$(OFILES_SRC) : $(HFILES_BIN)
#-------------------------------------------------------------------------------
# you need a rule like this for each extension you use as binary data
#-------------------------------------------------------------------------------
%.bin.o %_bin.h : %.bin
#-------------------------------------------------------------------------------
@echo $(notdir $<)
@$(bin2o)
-include $(DEPENDS)
#-------------------------------------------------------------------------------
endif
#-------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment