Last active
July 13, 2025 08:28
-
-
Save KokoseiJ/56d76506c6733541ea1ddcafa36bb209 to your computer and use it in GitHub Desktop.
experimental Taiko no Tatsujin IO implementation for ESP32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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