Skip to content

Instantly share code, notes, and snippets.

@KokoseiJ
Last active July 13, 2025 08:28
Show Gist options
  • Save KokoseiJ/56d76506c6733541ea1ddcafa36bb209 to your computer and use it in GitHub Desktop.
Save KokoseiJ/56d76506c6733541ea1ddcafa36bb209 to your computer and use it in GitHub Desktop.
experimental Taiko no Tatsujin IO implementation for ESP32
#include <Arduino.h>
#include <PN532.h>
#include <PN532_SPI.h>
#include <PN532_debug.h>
#include <esp_adc/adc_oneshot.h>
// #define DEBUG
#define CARD_RESET_INTERVAL 1000
#define VSPI_MOSI GPIO_NUM_23
#define VSPI_MISO GPIO_NUM_19
#define VSPI_CLK GPIO_NUM_18
#define VSPI_CS GPIO_NUM_5
#define PN532_IRQ GPIO_NUM_4
// bruh no pull up
// #define COIN GPIO_NUM_34
// #define SERVICE GPIO_NUM_35
#define COIN GPIO_NUM_17
#define SERVICE GPIO_NUM_16
#define TEST GPIO_NUM_32
#define ENTER GPIO_NUM_33
#define SELECT_UP GPIO_NUM_22
#define SELECT_DOWN GPIO_NUM_21
#define LK1 ADC_CHANNEL_8 // D25
#define LD1 ADC_CHANNEL_9 // D26
#define RD1 ADC_CHANNEL_7 // D27
#define RK1 ADC_CHANNEL_6 // D14
#define LK2 ADC_CHANNEL_5 // D12
#define LD2 ADC_CHANNEL_4 // D13
#define RD2 ADC_CHANNEL_3 // D15
#define RK2 ADC_CHANNEL_2 // D2
#ifdef DEBUG
#define LOG(fmt, ...) debug_log("%s (%s:%d) " fmt "\r\n", __func__, __FILE__, __LINE__, ##__VA_ARGS__)
#else
#define LOG(...)
#endif
#define REV_BYTE64(x) ( \
(x & 0x00000000000000FF) << 8 * 7 | \
(x & 0x000000000000FF00) << 8 * 5 | \
(x & 0x0000000000FF0000) << 8 * 3 | \
(x & 0x00000000FF000000) << 8 | \
(x & 0x000000FF00000000) >> 8 | \
(x & 0x0000FF0000000000) >> 8 * 3 | \
(x & 0x00FF000000000000) >> 8 * 5 | \
(x & 0xFF00000000000000) >> 8 * 7 \
)
enum taikoIO_reqType {
TIO_REQ_AUTO_GET = 0x01,
TIO_REQ_CARD_READ = 0x02
};
struct taikoIO_header {
uint8_t magic[2];
uint8_t messageType;
uint8_t length;
};
struct taikoIO_autoget_req {
struct taikoIO_header header;
uint16_t pollingRate;
};
struct taikoIO_autoget_res {
struct taikoIO_header header;
uint16_t sensorReading[8];
uint8_t coin;
uint8_t service;
uint8_t test;
uint8_t enter;
uint8_t select_up;
uint8_t select_down;
};
struct taikoIO_card_res {
struct taikoIO_header header;
char accessCode[20];
};
// Mifare key from flipper zero people, banakey not confirmed yet
uint8_t aimeKey[6] = {0x57, 0x43, 0x43, 0x46, 0x76, 0x32};
uint8_t banaKey[6] = {0x60, 0x90, 0xd0, 0x06, 0x32, 0xf5};
PN532_SPI pn532_spi(SPI, VSPI_CS);
PN532 nfc(pn532_spi);
uint8_t lastUid[8];
uint64_t lastRead;
adc_oneshot_unit_handle_t adc_handle;
volatile uint8_t sensorReadReady = false;
volatile uint8_t serialInUse = false;
void debug_log(const char *fmt, ...) {
char buf[256];
va_list args;
va_start (args, fmt);
vsnprintf(buf, 256, fmt, args);
va_end(args);
Serial.print(buf);
}
bool felica_read(char *cardId) {
uint8_t idm[8];
uint8_t pmm[8];
uint8_t status;
uint16_t sysResp;
// LOG("felica entry");
status = nfc.felica_Polling(0xFFFF, 0x01, idm, pmm, &sysResp, 50);
if (status != 1) return false;
if (!memcmp(idm, lastUid, 8) && millis() - lastRead < CARD_RESET_INTERVAL) {
lastRead = millis();
return false;
}
memcpy(lastUid, idm, 8);
lastRead = millis();
// To represent the IDm as naive 0008, we need to reverse byte order and read it as uint64
snprintf(cardId, 21, "%020llu", REV_BYTE64(*(uint64_t *)idm));
LOG("felica read");
return true;
}
bool mifare_read(char *cardId) {
uint8_t uid[8] = {0,};
uint8_t uidLen;
uint8_t mifareData[16] = {0, };
uint8_t status;
// LOG("mifare entry");
status = nfc.readPassiveTargetID(
PN532_MIFARE_ISO14443A, uid, &uidLen, 30
);
// if length isnt 4, not mifare, and im not doing virtual felica idm
if (!status || uidLen != 4) return false;
if (!memcmp(uid, lastUid, 4) && millis() - lastRead < CARD_RESET_INTERVAL) {
lastRead = millis();
return false;
}
memcpy(lastUid, uid, uidLen);
lastRead = millis();
// Trying banana first since it is more likely for taiko player to have it,
// makes it more responsive (by like couple msec lol) since retrying takes time
LOG("mifare detected, trying banapass key");
status = nfc.mifareclassic_AuthenticateBlock(uid, uidLen, 2, 1, banaKey);
if (!status) {
LOG("banakey failed, trying aime key");
// Re-read the card, subsequent auth attempt fails without this
status = nfc.readPassiveTargetID(
PN532_MIFARE_ISO14443A, uid, &uidLen, 30
);
if (!status) {
// Just leave the card tapped in lil bro :skull:
LOG("Card left?");
return false;
}
status = nfc.mifareclassic_AuthenticateBlock(uid, uidLen, 2, 1, aimeKey);
if (!status) {
LOG("aimekey failed, probably not aime");
return false;
}
}
status = nfc.mifareclassic_ReadDataBlock(2, mifareData);
if (!status) {
LOG("read failed!");
return false;
}
snprintf(
cardId, 21, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
mifareData[6], mifareData[7], mifareData[8], mifareData[9],
mifareData[10], mifareData[11], mifareData[12], mifareData[13],
mifareData[14], mifareData[15]
);
LOG("mifare read");
return true;
}
void card_handle(bool isFelica) {
char cardId[21];
static taikoIO_card_res packetTmpl = {
.header = {
.magic = {0xDA, 0xD0},
.messageType = TIO_REQ_CARD_READ,
.length = sizeof(taikoIO_card_res) - sizeof(taikoIO_header)
}
};
taikoIO_card_res packet = packetTmpl;
// Choose card type depending on polling mode
if (isFelica) {
if (!felica_read(cardId))
return;
} else {
if (!mifare_read(cardId))
return;
}
LOG("card: %s", cardId);
memcpy(packet.accessCode, cardId, 20);
while(serialInUse) {};
serialInUse = true;
Serial.write((uint8_t *) &packet, sizeof(taikoIO_card_res));
serialInUse = false;
}
void poll_timer() {
sensorReadReady = true;
}
void init_nfc() {
uint32_t version = 0;
LOG("Initializing NFC...");
nfc.begin();
do {
LOG("Checking NFC board...");
version = nfc.getFirmwareVersion();
} while (!version);
LOG(
"Chip: PN5%02x\nFirm: %d.%d",
(version >> 24) & 0xFF,
(version >> 16) & 0xFF,
(version >> 8) & 0xFF
);
nfc.setPassiveActivationRetries(0xFF);
nfc.SAMConfig();
felica_read(NULL);
nfc.startPassiveTargetIDDetection(1);
}
void init_input() {
static adc_oneshot_unit_init_cfg_t adc_config = {
.unit_id = ADC_UNIT_2,
.clk_src = ADC_RTC_CLK_SRC_DEFAULT,
.ulp_mode = ADC_ULP_MODE_DISABLE
};
static adc_oneshot_chan_cfg_t adc_chan_config = {
.atten = ADC_ATTEN_DB_0,
.bitwidth = ADC_BITWIDTH_12
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&adc_config, &adc_handle));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, LK1, &adc_chan_config));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, LD1, &adc_chan_config));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, RD1, &adc_chan_config));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, RK1, &adc_chan_config));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, LK2, &adc_chan_config));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, LD2, &adc_chan_config));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, RD2, &adc_chan_config));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, RK2, &adc_chan_config));
pinMode(COIN, INPUT_PULLUP);
pinMode(SERVICE, INPUT_PULLUP);
pinMode(TEST, INPUT_PULLUP);
pinMode(ENTER, INPUT_PULLUP);
pinMode(SELECT_UP, INPUT_PULLUP);
pinMode(SELECT_DOWN, INPUT_PULLUP);
pinMode(PN532_IRQ, INPUT);
}
void init_interrupt() {
hw_timer_t *timer;
timer = timerBegin(1000000);
timerAttachInterrupt(timer, poll_timer);
timerAlarm(timer, 1000, true, 0);
}
uint64_t start;
uint64_t count;
void loop2(void *);
void setup() {
TaskHandle_t loop2Handle;
Serial.begin(576000);
LOG("Hello, World!");
init_input();
init_interrupt();
start = millis();
count = 0;
xTaskCreatePinnedToCore(&loop2, "loop2", 2048, NULL, 0, &loop2Handle, 0);
}
void loop() {
const taikoIO_autoget_res packet_temp = {
.header = {
{0xDA, 0xD0},
TIO_REQ_AUTO_GET,
sizeof(taikoIO_autoget_res) - sizeof(taikoIO_header)
},
};
taikoIO_autoget_res packet = packet_temp;
int temp;
// aim for 1000hz
if (!sensorReadReady) return;
sensorReadReady = false;
adc_oneshot_read(adc_handle, LK1, &temp);
packet.sensorReading[0] = temp;
adc_oneshot_read(adc_handle, LD1, &temp);
packet.sensorReading[1] = temp;
adc_oneshot_read(adc_handle, RD1, &temp);
packet.sensorReading[2] = temp;
adc_oneshot_read(adc_handle, RK1, &temp);
packet.sensorReading[3] = temp;
adc_oneshot_read(adc_handle, LK2, &temp);
packet.sensorReading[4] = temp;
adc_oneshot_read(adc_handle, LD2, &temp);
packet.sensorReading[5] = temp;
adc_oneshot_read(adc_handle, RD2, &temp);
packet.sensorReading[6] = temp;
adc_oneshot_read(adc_handle, RK2, &temp);
packet.sensorReading[7] = temp;
packet.coin = !digitalRead(COIN);
packet.service = !digitalRead(SERVICE);
packet.test = !digitalRead(TEST);
packet.enter = !digitalRead(ENTER);
packet.select_up = !digitalRead(SELECT_UP);
packet.select_down = !digitalRead(SELECT_DOWN);
while (serialInUse) {}
serialInUse = true;
Serial.write((uint8_t *)&packet, sizeof(taikoIO_autoget_res));
serialInUse = false;
count++;
if (millis() - start >= 1) {
// Serial.println(count * 1000 / (millis() - start));
}
}
void loop2(void *arg) {
bool isFelica = 0;
init_nfc();
while (1) {
card_handle(isFelica);
isFelica = !isFelica;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment