├── Aime_Reader.h ├── ESP32-CardReader.ino ├── OTA └── index.php ├── PCB ├── Gerber_ESP32S-PN532-v2.zip └── PCB_ESP32S-PN532-v2.json ├── README.md └── src ├── connection.h └── wrappers.h /Aime_Reader.h: -------------------------------------------------------------------------------- 1 | #if defined(ESP32) 2 | #pragma message "当前的开发板是 ESP32" 3 | #define SerialDevice Serial 4 | #define PN532_SPI_SS 5 5 | #define LED_PIN 13 6 | 7 | #define SW1_MODE 33 8 | #define SW2_OTA 25 9 | #define SW3_CARD 26 10 | #define SW4_FW 27 11 | 12 | // #define OTA_Enable 13 | #ifdef OTA_Enable 14 | #pragma message "已开启 OTA 更新功能" 15 | #define STASSID "SSIDNAME" 16 | #define STAPASS "PASSWORD" 17 | #define OTA_URL "http://esp-update.local/Sucareto/ESP32-Reader:2333/" 18 | #include 19 | #include 20 | #endif 21 | 22 | #else 23 | #error "未适配的开发板!!!" 24 | #endif 25 | 26 | #define old_fw_version "TN32MSEC003S F/W Ver1.2" 27 | #define old_hw_version "TN32MSEC003S H/W Ver3.0" 28 | #define old_led_info "15084\xFF\x10\x00\x12" 29 | 30 | #define new_fw_version "\x94" 31 | #define new_hw_version "837-15396" 32 | #define new_led_info "000-00000\xFF\x11\x40" 33 | 34 | bool ReaderMode, FWSW; 35 | uint8_t len, r, checksum; 36 | bool escape = false; 37 | unsigned long ConnectTime = 0; 38 | bool ConnectStatus = false; 39 | uint16_t SleepDelay = 10000; // ms 40 | 41 | 42 | #include 43 | #include 44 | PN532_SPI pn532(SPI, PN532_SPI_SS); 45 | 46 | #include "PN532.h" 47 | PN532 nfc(pn532); 48 | 49 | #include "FastLED.h" 50 | #define NUM_LEDS 8 51 | CRGB leds[NUM_LEDS]; 52 | 53 | uint8_t KeyA[6], KeyB[6]; 54 | uint8_t DefaultKey[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; 55 | 56 | #include 57 | U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0); 58 | 59 | #define SPICEAPI_INTERFACE Serial 60 | #include "src/wrappers.h" 61 | spiceapi::Connection CON(512); 62 | 63 | static uint8_t mifare_data[][16] = { 64 | // https://github.com/Sucareto/Arduino-Aime-Reader/blob/main/doc/aime%E7%A4%BA%E4%BE%8B.mct 65 | { 0x8A, 0x1B, 0x72, 0xE1, 0x02, 0x08, 0x04, 0x00, 0x02, 0xFE, 0x9F, 0xC6, 0xDD, 0x8A, 0x3D, 0x1D }, // 前 4 位是 UID 66 | {}, // 空数据占位符 67 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x11, 0x45, 0x14, 0x19, 0x19, 0x81, 0x02, 0x33, 0x33 }, // access code 68 | { 0x57, 0x43, 0x43, 0x46, 0x76, 0x32, 0x08, 0x77, 0x8F, 0x11, 0x57, 0x43, 0x43, 0x46, 0x76, 0x32 }, 69 | }; 70 | 71 | static unsigned char rf_open[] = { 72 | 0x00, 0x00, 0x00, 0x00, 0x04, 0x20, 0x02, 0x40, 0x02, 0x40, 0x11, 0x88, 0x91, 0x89, 0x49, 0x92, 73 | 0x49, 0x92, 0x91, 0x89, 0x11, 0x88, 0x02, 0x40, 0x02, 0x40, 0x04, 0x20, 0x00, 0x00, 0x00, 0x00 74 | }; 75 | 76 | static unsigned char rf_off[] = { 77 | 0x01, 0x00, 0x02, 0x00, 0x04, 0x20, 0x0A, 0x40, 0x12, 0x40, 0x21, 0x88, 0x51, 0x89, 0x89, 0x92, 78 | 0x49, 0x93, 0x91, 0x8A, 0x11, 0x8C, 0x02, 0x48, 0x02, 0x50, 0x04, 0x20, 0x00, 0x40, 0x00, 0x80 79 | }; 80 | 81 | static unsigned char card[] = { 82 | 0x00, 0x00, 0xFC, 0x3F, 0x02, 0x40, 0x32, 0x48, 0x52, 0x48, 0x92, 0x48, 0x12, 0x49, 0x12, 0x4A, 83 | 0x52, 0x48, 0x92, 0x48, 0x12, 0x49, 0x12, 0x4A, 0x12, 0x4C, 0x02, 0x40, 0xFC, 0x3F, 0x00, 0x00 84 | }; 85 | 86 | static unsigned char blank[] = { 87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 89 | }; 90 | 91 | enum { 92 | CMD_GET_FW_VERSION = 0x30, 93 | CMD_GET_HW_VERSION = 0x32, 94 | // Card read 95 | CMD_START_POLLING = 0x40, 96 | CMD_STOP_POLLING = 0x41, 97 | CMD_CARD_DETECT = 0x42, 98 | CMD_CARD_SELECT = 0x43, 99 | CMD_CARD_HALT = 0x44, 100 | // MIFARE 101 | CMD_MIFARE_KEY_SET_A = 0x50, 102 | CMD_MIFARE_AUTHORIZE_A = 0x51, 103 | CMD_MIFARE_READ = 0x52, 104 | // CMD_MIFARE_WRITE = 0x53, 105 | CMD_MIFARE_KEY_SET_B = 0x54, 106 | CMD_MIFARE_AUTHORIZE_B = 0x55, 107 | // Boot,update 108 | CMD_TO_UPDATER_MODE = 0x60, 109 | CMD_SEND_HEX_DATA = 0x61, 110 | CMD_TO_NORMAL_MODE = 0x62, 111 | CMD_SEND_BINDATA_INIT = 0x63, 112 | CMD_SEND_BINDATA_EXEC = 0x64, 113 | // FeliCa 114 | // CMD_FELICA_PUSH = 0x70, 115 | CMD_FELICA_THROUGH = 0x71, 116 | // LED board 117 | CMD_EXT_BOARD_LED = 0x80, 118 | CMD_EXT_BOARD_LED_RGB = 0x81, 119 | CMD_EXT_BOARD_LED_RGB_UNKNOWN = 0x82, // 未知 120 | CMD_EXT_BOARD_INFO = 0xf0, 121 | CMD_EXT_FIRM_SUM = 0xf2, 122 | CMD_EXT_SEND_HEX_DATA = 0xf3, 123 | CMD_EXT_TO_BOOT_MODE = 0xf4, 124 | CMD_EXT_TO_NORMAL_MODE = 0xf5, 125 | }; 126 | 127 | enum { 128 | STATUS_OK = 0x00, 129 | STATUS_CARD_ERROR = 0x01, 130 | STATUS_NOT_ACCEPT = 0x02, 131 | STATUS_INVALID_COMMAND = 0x03, 132 | STATUS_INVALID_DATA = 0x04, 133 | STATUS_SUM_ERROR = 0x05, 134 | STATUS_INTERNAL_ERROR = 0x06, 135 | STATUS_INVALID_FIRM_DATA = 0x07, 136 | STATUS_FIRM_UPDATE_SUCCESS = 0x08, 137 | STATUS_COMP_DUMMY_2ND = 0x10, // 837-15286 138 | STATUS_COMP_DUMMY_3RD = 0x20, // 837-15396 139 | }; 140 | 141 | typedef union { 142 | uint8_t bytes[128]; 143 | struct { 144 | uint8_t frame_len; 145 | uint8_t addr; 146 | uint8_t seq_no; 147 | uint8_t cmd; 148 | uint8_t payload_len; 149 | union { 150 | uint8_t key[6]; // CMD_MIFARE_KEY_SET 151 | uint8_t color_payload[3]; // CMD_EXT_BOARD_LED_RGB 152 | struct { // CMD_CARD_SELECT,AUTHORIZE,READ 153 | uint8_t uid[4]; 154 | uint8_t block_no; 155 | }; 156 | struct { // CMD_FELICA_THROUGH 157 | uint8_t encap_IDm[8]; 158 | uint8_t felica_through_payload[1]; 159 | }; 160 | }; 161 | }; 162 | } packet_request_t; 163 | 164 | typedef union { 165 | uint8_t bytes[128]; 166 | struct { 167 | uint8_t frame_len; 168 | uint8_t addr; 169 | uint8_t seq_no; 170 | uint8_t cmd; 171 | uint8_t status; 172 | uint8_t payload_len; 173 | union { 174 | uint8_t version[1]; // CMD_GET_FW_VERSION,CMD_GET_HW_VERSION,CMD_EXT_BOARD_INFO 175 | uint8_t block[16]; // CMD_MIFARE_READ 176 | struct { // CMD_CARD_DETECT 177 | uint8_t count; 178 | uint8_t type; 179 | uint8_t id_len; 180 | union { 181 | uint8_t mifare_uid[4]; 182 | struct { 183 | uint8_t IDm[8]; 184 | uint8_t PMm[8]; 185 | }; 186 | }; 187 | }; 188 | uint8_t felica_through_payload[1]; 189 | }; 190 | }; 191 | } packet_response_t; 192 | 193 | packet_request_t req; 194 | packet_response_t res; 195 | 196 | uint8_t packet_read() { 197 | while (SerialDevice.available()) { 198 | r = SerialDevice.read(); 199 | if (r == 0xE0) { 200 | req.frame_len = 0xFF; 201 | continue; 202 | } 203 | if (req.frame_len == 0xFF) { 204 | req.frame_len = r; 205 | len = 0; 206 | checksum = r; 207 | continue; 208 | } 209 | if (r == 0xD0) { 210 | escape = true; 211 | continue; 212 | } 213 | if (escape) { 214 | r++; 215 | escape = false; 216 | } 217 | req.bytes[++len] = r; 218 | if (len == req.frame_len) { 219 | if (req.cmd == CMD_SEND_BINDATA_EXEC) return req.cmd; 220 | return checksum == r ? req.cmd : STATUS_SUM_ERROR; 221 | } 222 | checksum += r; 223 | } 224 | return 0; 225 | } 226 | 227 | void packet_write() { 228 | uint8_t checksum = 0, len = 0; 229 | if (res.cmd == 0) { 230 | return; 231 | } 232 | SerialDevice.write(0xE0); 233 | while (len <= res.frame_len) { 234 | uint8_t w; 235 | if (len == res.frame_len) { 236 | w = checksum; 237 | } else { 238 | w = res.bytes[len]; 239 | checksum += w; 240 | } 241 | if (w == 0xE0 || w == 0xD0) { 242 | SerialDevice.write(0xD0); 243 | SerialDevice.write(--w); 244 | } else { 245 | SerialDevice.write(w); 246 | } 247 | len++; 248 | } 249 | res.cmd = 0; 250 | } 251 | 252 | void res_init(uint8_t payload_len = 0) { 253 | res.frame_len = 6 + payload_len; 254 | res.addr = req.addr; 255 | res.seq_no = req.seq_no; 256 | res.cmd = req.cmd; 257 | res.status = STATUS_OK; 258 | res.payload_len = payload_len; 259 | } 260 | 261 | void sys_to_normal_mode() { 262 | res_init(); 263 | if (nfc.getFirmwareVersion()) { 264 | res.status = STATUS_INVALID_COMMAND; 265 | u8g2.drawXBM(95, 0, 16, 16, blank); 266 | u8g2.drawXBM(113, 0, 16, 16, blank); 267 | } else { 268 | res.status = STATUS_INTERNAL_ERROR; 269 | u8g2.drawXBM(95, 0, 16, 16, rf_off); 270 | FastLED.showColor(0xFF0000); 271 | } 272 | } 273 | 274 | void sys_get_fw_version() { 275 | if (FWSW) { 276 | res_init(sizeof(old_fw_version) - 1); 277 | memcpy(res.version, old_fw_version, res.payload_len); 278 | } else { 279 | res_init(sizeof(new_fw_version) - 1); 280 | memcpy(res.version, new_fw_version, res.payload_len); 281 | } 282 | } 283 | 284 | void sys_get_hw_version() { 285 | if (FWSW) { 286 | res_init(sizeof(old_hw_version) - 1); 287 | memcpy(res.version, old_hw_version, res.payload_len); 288 | } else { 289 | res_init(sizeof(new_hw_version) - 1); 290 | memcpy(res.version, new_hw_version, res.payload_len); 291 | } 292 | } 293 | 294 | void sys_get_led_info() { 295 | if (FWSW) { 296 | res_init(sizeof(old_led_info) - 1); 297 | memcpy(res.version, old_led_info, res.payload_len); 298 | } else { 299 | res_init(sizeof(new_led_info) - 1); 300 | memcpy(res.version, new_led_info, res.payload_len); 301 | } 302 | } 303 | 304 | void nfc_start_polling() { 305 | res_init(); 306 | nfc.setRFField(0x00, 0x01); 307 | u8g2.drawXBM(95, 0, 16, 16, rf_open); 308 | } 309 | 310 | void nfc_stop_polling() { 311 | res_init(); 312 | nfc.setRFField(0x00, 0x00); 313 | u8g2.drawXBM(95, 0, 16, 16, blank); 314 | } 315 | 316 | void nfc_card_detect() { 317 | uint16_t SystemCode; 318 | uint8_t bufferLength; 319 | if (!digitalRead(SW3_CARD)) { 320 | memcpy(res.mifare_uid, mifare_data[0], 0x04); 321 | res.id_len = 0x04; 322 | res_init(0x07); 323 | res.count = 1; 324 | res.type = 0x10; 325 | } else if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, res.mifare_uid, &res.id_len) && nfc.getBuffer(&bufferLength)[4] == 0x08) { // Only read cards with sak=0x08 326 | res_init(0x07); 327 | res.count = 1; 328 | res.type = 0x10; 329 | } else if (nfc.felica_Polling(0xFFFF, 0x00, res.IDm, res.PMm, &SystemCode, 200) == 1) { 330 | res_init(0x13); 331 | res.count = 1; 332 | res.type = 0x20; 333 | res.id_len = 0x10; 334 | } else { 335 | res_init(1); 336 | res.count = 0; 337 | u8g2.drawXBM(113, 0, 16, 16, blank); 338 | return; 339 | } 340 | u8g2.drawXBM(113, 0, 16, 16, card); 341 | } 342 | 343 | void nfc_mifare_authorize_a() { 344 | res_init(); 345 | if (!nfc.mifareclassic_AuthenticateBlock(req.uid, 4, req.block_no, 0, KeyA)) { 346 | res.status = STATUS_CARD_ERROR; 347 | } 348 | } 349 | 350 | void nfc_mifare_authorize_b() { 351 | res_init(); 352 | if (!nfc.mifareclassic_AuthenticateBlock(req.uid, 4, req.block_no, 1, KeyB)) { 353 | res.status = STATUS_CARD_ERROR; 354 | } 355 | } 356 | 357 | void nfc_mifare_read() { 358 | res_init(0x10); 359 | if (!digitalRead(SW3_CARD)) { 360 | memcpy(res.block, mifare_data[req.block_no], 16); 361 | res_init(0x10); 362 | return; 363 | } else if (!nfc.mifareclassic_ReadDataBlock(req.block_no, res.block)) { 364 | res_init(); 365 | res.status = STATUS_CARD_ERROR; 366 | } 367 | } 368 | 369 | void nfc_felica_through() { 370 | uint8_t response_length = 0xFF; 371 | if (nfc.inDataExchange(req.felica_through_payload, req.felica_through_payload[0], res.felica_through_payload, &response_length)) { 372 | res_init(response_length); 373 | } else { 374 | res_init(); 375 | res.status = STATUS_CARD_ERROR; 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /ESP32-CardReader.ino: -------------------------------------------------------------------------------- 1 | #include "Aime_Reader.h" 2 | 3 | void (*ReaderMain)(); 4 | 5 | void setup() { 6 | pinMode(SW1_MODE, INPUT_PULLUP); // Switch mode 7 | pinMode(SW3_CARD, INPUT_PULLUP); // Hardcode mifare 8 | pinMode(SW4_FW, INPUT_PULLUP); // (Aime) Baudrate & fw/hw | (Spice) 1P 2P 9 | u8g2.begin(); 10 | u8g2.setFont(u8g2_font_6x12_mf); 11 | u8g2.clearBuffer(); 12 | FastLED.addLeds(leds, 8); 13 | FastLED.setBrightness(20); // LED brightness 14 | FastLED.showColor(0); 15 | 16 | #ifdef OTA_Enable // update check 17 | pinMode(SW2_OTA, INPUT_PULLUP); // Enable OTA 18 | if (!digitalRead(SW2_OTA)) { 19 | WiFi.begin(STASSID, STAPASS); 20 | u8g2.drawStr(0, 28, "WiFi Connecting..."); 21 | u8g2.sendBuffer(); 22 | while (WiFi.status() != WL_CONNECTED) { 23 | delay(100); 24 | }; 25 | u8g2.drawStr(0, 28, "WiFi connected. "); 26 | u8g2.drawStr(0, 41, "Check update... "); 27 | u8g2.sendBuffer(); 28 | WiFiClient client; 29 | httpUpdate.setLedPin(LED_BUILTIN, LOW); 30 | t_httpUpdate_return ret = httpUpdate.update(client, OTA_URL); 31 | switch (ret) { 32 | case HTTP_UPDATE_FAILED: 33 | u8g2.drawStr(0, 41, "Check failed. "); 34 | break; 35 | case HTTP_UPDATE_NO_UPDATES: 36 | u8g2.drawStr(0, 41, "Already up to date. "); 37 | break; 38 | } 39 | } 40 | #endif 41 | 42 | nfc.begin(); 43 | while (!nfc.getFirmwareVersion()) { 44 | u8g2.drawXBM(95, 0, 16, 16, rf_off); 45 | u8g2.sendBuffer(); 46 | FastLED.showColor(0xFF0000); 47 | delay(500); 48 | FastLED.showColor(0); 49 | delay(500); 50 | } 51 | nfc.setPassiveActivationRetries(0x10); 52 | nfc.SAMConfig(); 53 | u8g2.clearBuffer(); 54 | 55 | // mode select 56 | ReaderMode = !digitalRead(SW1_MODE); 57 | FWSW = !digitalRead(SW4_FW); 58 | if (ReaderMode) { // BEMANI mode 59 | SerialDevice.begin(115200); 60 | u8g2.drawStr(0, 14, "SpiceTools"); 61 | u8g2.drawStr(117, 64, FWSW ? "2P" : "1P"); 62 | FastLED.showColor(CRGB::Yellow); 63 | ReaderMain = SpiceToolsReader; 64 | } else { // Aime mode 65 | SerialDevice.begin(FWSW ? 38400 : 115200); 66 | u8g2.drawStr(0, 16, "Aime Reader"); 67 | u8g2.drawStr(105, 64, FWSW ? "LOW" : "HIGH"); 68 | u8g2.drawStr(0, 64, FWSW ? "TN32MSEC003S" : new_hw_version); 69 | FastLED.showColor(FWSW ? CRGB::Green : CRGB::Blue); 70 | ReaderMain = AimeCardReader; 71 | } 72 | 73 | memset(req.bytes, 0, sizeof(req.bytes)); 74 | memset(res.bytes, 0, sizeof(res.bytes)); 75 | u8g2.sendBuffer(); 76 | 77 | ConnectTime = millis(); 78 | ConnectStatus = true; 79 | } 80 | 81 | 82 | void loop() { 83 | ReaderMain(); 84 | 85 | if (ConnectStatus) { 86 | if ((millis() - ConnectTime) > SleepDelay) { 87 | u8g2.sleepOn(); 88 | ConnectStatus = false; 89 | } 90 | } else { 91 | if ((millis() - ConnectTime) < SleepDelay) { 92 | u8g2.sleepOff(); 93 | ConnectStatus = true; 94 | } 95 | } 96 | } 97 | 98 | 99 | void SpiceToolsReader() { // Spice mode 100 | uint16_t SystemCode; 101 | char card_id[17]; 102 | if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, res.mifare_uid, &res.id_len) 103 | && nfc.mifareclassic_AuthenticateBlock(res.mifare_uid, res.id_len, 1, 0, DefaultKey) 104 | && nfc.mifareclassic_ReadDataBlock(1, res.block)) { 105 | sprintf(card_id, "%02X%02X%02X%02X%02X%02X%02X%02X", 106 | res.block[0], res.block[1], res.block[2], res.block[3], 107 | res.block[4], res.block[5], res.block[6], res.block[7]); 108 | 109 | } else if (nfc.felica_Polling(0xFFFF, 0x00, res.IDm, res.PMm, &SystemCode, 200) == 1) { 110 | sprintf(card_id, "%02X%02X%02X%02X%02X%02X%02X%02X", 111 | res.IDm[0], res.IDm[1], res.IDm[2], res.IDm[3], 112 | res.IDm[4], res.IDm[5], res.IDm[6], res.IDm[7]); 113 | } else { 114 | return; 115 | } 116 | u8g2.drawXBM(113, 0, 16, 16, card); 117 | u8g2.sendBuffer(); 118 | spiceapi::InfoAvs avs_info{}; 119 | if (spiceapi::info_avs(CON, avs_info)) { 120 | FWSW = !digitalRead(SW4_FW); 121 | spiceapi::card_insert(CON, FWSW, card_id); 122 | u8g2.drawStr(0, 30, card_id); 123 | u8g2.drawStr(0, 64, (avs_info.model + ":" + avs_info.dest + avs_info.spec + avs_info.rev + ":" + avs_info.ext).c_str()); 124 | u8g2.drawStr(117, 64, FWSW ? "2P" : "1P"); 125 | u8g2.sendBuffer(); 126 | for (int i = 0; i < 8; i++) { 127 | leds[i] = CRGB::Red; 128 | leds[7 - i] = CRGB::Blue; 129 | FastLED.delay(50); 130 | leds[i] = CRGB::Black; 131 | leds[7 - i] = CRGB::Black; 132 | } 133 | FastLED.show(); 134 | } 135 | u8g2.drawXBM(113, 0, 16, 16, blank); 136 | u8g2.drawStr(0, 30, " "); 137 | u8g2.sendBuffer(); 138 | ConnectTime = millis(); 139 | } 140 | 141 | 142 | void AimeCardReader() { // Aime mode 143 | switch (packet_read()) { 144 | case 0: 145 | return; 146 | case CMD_TO_NORMAL_MODE: 147 | sys_to_normal_mode(); 148 | break; 149 | case CMD_GET_FW_VERSION: 150 | sys_get_fw_version(); 151 | break; 152 | case CMD_GET_HW_VERSION: 153 | sys_get_hw_version(); 154 | break; 155 | // Card read 156 | case CMD_START_POLLING: 157 | nfc_start_polling(); 158 | break; 159 | case CMD_STOP_POLLING: 160 | nfc_stop_polling(); 161 | break; 162 | case CMD_CARD_DETECT: 163 | nfc_card_detect(); 164 | break; 165 | // MIFARE 166 | case CMD_MIFARE_KEY_SET_A: 167 | memcpy(KeyA, req.key, 6); 168 | res_init(); 169 | break; 170 | case CMD_MIFARE_KEY_SET_B: 171 | res_init(); 172 | memcpy(KeyB, req.key, 6); 173 | break; 174 | case CMD_MIFARE_AUTHORIZE_A: 175 | nfc_mifare_authorize_a(); 176 | break; 177 | case CMD_MIFARE_AUTHORIZE_B: 178 | nfc_mifare_authorize_b(); 179 | break; 180 | case CMD_MIFARE_READ: 181 | nfc_mifare_read(); 182 | break; 183 | // FeliCa 184 | // case CMD_FELICA_THROUGH: 185 | // nfc_felica_through(); 186 | // break; 187 | // LED 188 | case CMD_EXT_BOARD_LED_RGB: 189 | FastLED.showColor(CRGB(req.color_payload[0], req.color_payload[1], req.color_payload[2])); 190 | break; 191 | case CMD_EXT_BOARD_INFO: 192 | sys_get_led_info(); 193 | break; 194 | case CMD_EXT_BOARD_LED_RGB_UNKNOWN: 195 | break; 196 | case CMD_CARD_SELECT: 197 | case CMD_CARD_HALT: 198 | case CMD_EXT_TO_NORMAL_MODE: 199 | case CMD_TO_UPDATER_MODE: 200 | case CMD_SEND_BINDATA_INIT: 201 | res_init(); 202 | break; 203 | case CMD_SEND_BINDATA_EXEC: 204 | res_init(); 205 | res.status = STATUS_FIRM_UPDATE_SUCCESS; 206 | break; 207 | case CMD_SEND_HEX_DATA: 208 | res_init(); 209 | res.status = STATUS_COMP_DUMMY_3RD; 210 | break; 211 | case STATUS_SUM_ERROR: 212 | res_init(); 213 | res.status = STATUS_SUM_ERROR; 214 | break; 215 | 216 | default: 217 | res_init(); 218 | res.status = STATUS_INVALID_COMMAND; 219 | } 220 | u8g2.sendBuffer(); 221 | ConnectTime = millis(); 222 | packet_write(); 223 | } 224 | -------------------------------------------------------------------------------- /OTA/index.php: -------------------------------------------------------------------------------- 1 | 点击展开 17 | 18 | ![读卡器](https://user-images.githubusercontent.com/28331534/170975617-4c0de22a-8daa-4263-a6b7-a09e974af1d3.jpg) 19 | ![拨码开关](https://user-images.githubusercontent.com/28331534/170975647-94706142-f535-4d15-8fc0-86bf5a60257b.jpg) 20 | 21 | https://user-images.githubusercontent.com/28331534/170975661-137f3474-f61a-4a4d-8ec2-b13b3c165761.mp4 22 | 23 | 24 | 25 | ## 使用说明: 26 | 27 | ### PCB 设计: 28 | - 该 PCB 方案仅用于我自己快速测试,并非最佳实现方案 29 | - 如果需要使用此 PCB 方案,请务必把 logo 删掉,然后根据需求认真审查 PCB 设计并作出修改后再进行制作 30 | - 建议使用 PN532 HSU 模式,可以去掉 TTL 拨码开关,需要直通 PN532 时,在代码里实现 Serial 数据转发 31 | 32 | 33 | ### 拨码开关: 34 | 在 PCB 放置了两个 4P 拨码开关: 35 | 36 | #### SW 1-4: 37 | - **SW1:读卡器模式切换(重启生效)** 38 | - ON:SpiceTools 模式,需要在 SpiceTools 添加启动参数 `-apiserial COM1 -apiserialbaud 115200`,"COM1" 改为实际的端口号 39 | - OFF:Aime 模式,和 [Arduino-Aime-Reader](https://github.com/Sucareto/Arduino-Aime-Reader) 使用方法一致 40 | - **SW2:OTA 开关(重启生效)** 41 | - ON:连接 WiFi 获取更新,如未能连接到 WiFi 则会持续到连接成功后才能启动 42 | - OFF:跳过检查更新,直接启动 43 | - **SW3:无卡测试模式** 44 | - ON:跳过读卡,使用硬编码的 `mifare_data` 数据 45 | - OFF:读取实际卡片 46 | - **SW4:固件功能切换** 47 | - Spice 模式 48 | - ON:卡号发送到 2P 槽位(需要游戏支持) 49 | - OFF:卡号发送到 1P 槽位 50 | - Aime 模式(重启生效) 51 | - ON:使用 38400 波特率初始化,固件版本是 TN32MSEC003S 52 | - OFF:使用 115200 波特率初始化,固件版本是 837-15396 53 | 54 | #### TTL:(用于切换至 “USB PN532 读卡器”模式,需要在断电情况下切换) 55 | - SW1:连接到 ESP32 的 `EN` 引脚,调为 `ON` 后会停用 ESP32,只使用 CH340 串口通信芯片 56 | - SW2:在目前版本(v2)为空置 57 | - SW3 & SW4:连接 CH340 和 PN532 的 `RX TX` 引脚 58 | 59 | 把 TTL 的拨码开关全部调为 `ON` 后,主机通过 ESP32 的 CH340 和 PN532 通信,PN532 的拨码开关也要设置为 `HSU` 模式,重新通电即可。 60 | **如果正常使用 ESP32 时连接 `RX TX` 引脚,会影响串口信息接收。** 61 | 62 | 63 | ### OTA 更新: 64 | 在代码里修改以下定义: 65 | - `STASSID`:WIFI 名 66 | - `STAPSK`:WIFI 密码 67 | - `OTA_URL`:更新包下载地址,可以使用文件地址,或者使用 [index.php](OTA/index.php) 来控制是否需要更新 68 | 69 | [index.php](OTA/index.php) 默认是判断文件 MD5 是否一致,不一致就会发送更新。 70 | 71 | ## 参考: 72 | 73 | - 原项目:[Arduino-Aime-Reader](https://github.com/Sucareto/Arduino-Aime-Reader) 74 | - 操作 SSD1306:[u8g2](https://github.com/olikraus/u8g2) 75 | - OLED 显示图案设计:PCtoLCD2002 76 | - Spice 通信参考:[PN5180-cardio SpiceAPI branch](https://github.com/CrazyRedMachine/PN5180-cardio/tree/SpiceAPI) 77 | - Spice 通信(已复制到本仓库并根据需要修改了部分代码):[SpiceAPI](https://github.com/spicetools/spicetools/tree/master/api/resources/arduino) 78 | - PCB 图案:[妖夢 - ぷりん](https://www.pixiv.net/artworks/87578487) 79 | - OTA 代码参考:[OTA Updates](https://arduino-esp8266.readthedocs.io/en/latest/ota_updates/readme.html#http-server) 80 | 81 | -------------------------------------------------------------------------------- /src/connection.h: -------------------------------------------------------------------------------- 1 | /* 2 | 库代码来自:https://github.com/spicetools/spicetools/tree/master/api/resources/arduino 3 | 删除了未使用的函数。 4 | */ 5 | #ifndef SPICEAPI_CONNECTION_H 6 | #define SPICEAPI_CONNECTION_H 7 | 8 | #include 9 | 10 | #ifndef SPICEAPI_INTERFACE 11 | #define SPICEAPI_INTERFACE Serial 12 | #endif 13 | 14 | namespace spiceapi { 15 | 16 | class Connection { 17 | private: 18 | uint8_t* receive_buffer; 19 | size_t receive_buffer_size; 20 | 21 | public: 22 | Connection(size_t receive_buffer_size); 23 | void reset(); 24 | const char* request(char* json); 25 | }; 26 | } 27 | 28 | spiceapi::Connection::Connection(size_t receive_buffer_size) { 29 | this->receive_buffer = new uint8_t[receive_buffer_size]; 30 | this->receive_buffer_size = receive_buffer_size; 31 | this->reset(); 32 | } 33 | 34 | void spiceapi::Connection::reset() { 35 | 36 | // drop all input 37 | while (SPICEAPI_INTERFACE.available()) { 38 | SPICEAPI_INTERFACE.read(); 39 | } 40 | } 41 | 42 | const char* spiceapi::Connection::request(char* json_data) { 43 | auto json_len = strlen(json_data) + 1; 44 | 45 | // send 46 | auto send_result = SPICEAPI_INTERFACE.write((const char*)json_data, (int)json_len); 47 | SPICEAPI_INTERFACE.flush(); 48 | if (send_result < (int)json_len) { 49 | return ""; 50 | } 51 | 52 | // receive 53 | size_t receive_data_len = 0; 54 | while (SPICEAPI_INTERFACE) { 55 | 56 | // read single byte 57 | auto b = SPICEAPI_INTERFACE.read(); 58 | if (b < 0) continue; 59 | receive_buffer[receive_data_len++] = b; 60 | 61 | // check for buffer overflow 62 | if (receive_data_len >= receive_buffer_size) { 63 | this->reset(); 64 | return ""; 65 | } 66 | 67 | // check for message end 68 | if (receive_buffer[receive_data_len - 1] == 0) 69 | break; 70 | } 71 | 72 | // return resulting json 73 | return (const char*)&receive_buffer[0]; 74 | } 75 | 76 | #endif //SPICEAPI_CONNECTION_H 77 | -------------------------------------------------------------------------------- /src/wrappers.h: -------------------------------------------------------------------------------- 1 | /* 2 | 库代码来自:https://github.com/spicetools/spicetools/tree/master/api/resources/arduino 3 | 删除了未使用的函数。 4 | */ 5 | #ifndef SPICEAPI_WRAPPERS_H 6 | #define SPICEAPI_WRAPPERS_H 7 | 8 | #define ARDUINOJSON_USE_LONG_LONG 1 9 | #include "ArduinoJson.h" 10 | 11 | #include 12 | #include "connection.h" 13 | 14 | // default buffer sizes 15 | #ifndef SPICEAPI_WRAPPER_BUFFER_SIZE 16 | #define SPICEAPI_WRAPPER_BUFFER_SIZE 256 17 | #endif 18 | #ifndef SPICEAPI_WRAPPER_BUFFER_SIZE_STR 19 | #define SPICEAPI_WRAPPER_BUFFER_SIZE_STR 256 20 | #endif 21 | 22 | namespace spiceapi { 23 | 24 | /* 25 | * Structs 26 | */ 27 | 28 | struct InfoAvs { 29 | String model, dest, spec, rev, ext; 30 | }; 31 | 32 | // static storage 33 | char JSON_BUFFER_STR[SPICEAPI_WRAPPER_BUFFER_SIZE_STR]; 34 | 35 | /* 36 | * Helpers 37 | */ 38 | 39 | uint64_t msg_gen_id() { 40 | static uint64_t id_global = 0; 41 | return ++id_global; 42 | } 43 | 44 | char *doc2str(DynamicJsonDocument *doc) { 45 | char *buf = JSON_BUFFER_STR; 46 | serializeJson(*doc, buf, SPICEAPI_WRAPPER_BUFFER_SIZE_STR); 47 | return buf; 48 | } 49 | 50 | DynamicJsonDocument *request_gen(const char *module, const char *function) { 51 | 52 | // create document 53 | auto doc = new DynamicJsonDocument(SPICEAPI_WRAPPER_BUFFER_SIZE); 54 | 55 | // add attributes 56 | (*doc)["id"] = msg_gen_id(); 57 | (*doc)["module"] = module; 58 | (*doc)["function"] = function; 59 | 60 | // add params 61 | (*doc).createNestedArray("params"); 62 | 63 | // return document 64 | return doc; 65 | } 66 | 67 | DynamicJsonDocument *response_get(Connection &con, const char *json) { 68 | 69 | // parse document 70 | DynamicJsonDocument *doc = new DynamicJsonDocument(SPICEAPI_WRAPPER_BUFFER_SIZE); 71 | auto err = deserializeJson(*doc, (char *)json); 72 | 73 | // check for parse error 74 | if (err) { 75 | delete doc; 76 | return nullptr; 77 | } 78 | 79 | // check id 80 | if (!(*doc)["id"].is()) { 81 | delete doc; 82 | return nullptr; 83 | } 84 | 85 | // check errors 86 | auto errors = (*doc)["errors"]; 87 | if (!errors.is()) { 88 | delete doc; 89 | return nullptr; 90 | } 91 | 92 | // check error count 93 | if (errors.as().size() > 0) { 94 | delete doc; 95 | return nullptr; 96 | } 97 | 98 | // check data 99 | if (!(*doc)["data"].is()) { 100 | delete doc; 101 | return nullptr; 102 | } 103 | 104 | // return document 105 | return doc; 106 | } 107 | 108 | bool card_insert(Connection &con, size_t index, const char *card_id) { 109 | auto req = request_gen("card", "insert"); 110 | auto params = (*req)["params"].as(); 111 | params.add(index); 112 | params.add(card_id); 113 | auto req_str = doc2str(req); 114 | delete req; 115 | auto res = response_get(con, con.request(req_str)); 116 | if (!res) 117 | return false; 118 | delete res; 119 | return true; 120 | } 121 | 122 | bool info_avs(Connection &con, InfoAvs &info) { 123 | auto req = request_gen("info", "avs"); 124 | auto req_str = doc2str(req); 125 | delete req; 126 | auto res = response_get(con, con.request(req_str)); 127 | if (!res) 128 | return false; 129 | auto data = (*res)["data"][0]; 130 | info.model = (const char *)data["model"]; 131 | info.dest = (const char *)data["dest"]; 132 | info.spec = (const char *)data["spec"]; 133 | info.rev = (const char *)data["rev"]; 134 | info.ext = (const char *)data["ext"]; 135 | delete res; 136 | return true; 137 | } 138 | } 139 | 140 | #endif //SPICEAPI_WRAPPERS_H 141 | --------------------------------------------------------------------------------