/*
Copyright 2022 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see .
*/
#include "card_reader.h"
#include "oslib/oslib.h"
#include "hw/sh4/modules/modules.h"
#include "hw/maple/maple_cfg.h"
#include "hw/maple/maple_devs.h"
#include
#include
#include
namespace card_reader {
class CardReaderWriter
{
public:
virtual ~CardReaderWriter() = default;
void insertCard()
{
cardInserted = loadCard();
if (cardInserted)
INFO_LOG(NAOMI, "Card inserted");
}
protected:
virtual bool loadCard() = 0;
bool loadCard(u8 *cardData, u32 len)
{
std::string path = hostfs::getArcadeFlashPath() + ".card";
FILE *fp = nowide::fopen(path.c_str(), "rb");
if (fp == nullptr)
return false;
INFO_LOG(NAOMI, "Loading card file from %s", path.c_str());
if (fread(cardData, 1, len, fp) != len)
WARN_LOG(NAOMI, "Truncated or empty card file: %s" ,path.c_str());
fclose(fp);
return true;
}
void saveCard(const u8 *cardData, u32 len)
{
std::string path = hostfs::getArcadeFlashPath() + ".card";
FILE *fp = nowide::fopen(path.c_str(), "wb");
if (fp == nullptr)
{
WARN_LOG(NAOMI, "Can't create card file %s: errno %d", path.c_str(), errno);
return;
}
INFO_LOG(NAOMI, "Saving card file to %s", path.c_str());
if (fwrite(cardData, 1, len, fp) != len)
WARN_LOG(NAOMI, "Truncated write to file: %s", path.c_str());
fclose(fp);
}
template
static u8 calcCrc(T begin, T end)
{
u32 crc = 0;
for (auto it = begin; it != end; it++)
crc ^= *it;
return crc;
}
bool cardInserted = false;
std::deque outBuffer;
std::vector inBuffer;
static constexpr u8 STX = 2;
static constexpr u8 ETX = 3;
static constexpr u8 ENQ = 5;
static constexpr u8 ACK = 6;
};
/*
Sanwa CRP-1231BR-10 card reader/writer protocol (from my good friend Metallic)
used in InitialD and Derby Owners Club
>>> SEND PKT: [START 02][LEN][CMD][0][0][0]{optional data}[STOP 03][CRC]
<<< RECV ACK: [OK 06]
>>> SEND REQ: [RQ 05]
<<< RECV PKT: [START 02][LEN][CMD][RESULT1][RESULT2][RESULT3]{optional data}[STOP 03][CRC]
<<< RECV ERR: [15]
RESULT1 (SENSORS): binary value SSTCCCCC
S - Shutter state:
0 - "ERROR"
1 - "CLOSED"
2 - "OPEN"
3 - "NOW" (both open and close sensors)
T - Stocker (card dispenser) state:
0 - "NO" (empty)
1 - "OK" (full)
C - Card sensors:
0 - "NO CARD"
1 - "CARD EXIST (ENTER)" (card inserted in front of shutter)
18 - "CARD EXIST" (card loaded inside of reader)
other - "CARD EXIST (OTHER)"
RESULT2: char 01235QRSTUV`
Error ENUM "OK", "READ ERR", "WRITE ERR", "STOP ERR", "PRINT ERR", "READERR T1", "READERR T2", "READERR T3",
"READERR T12", "READERR T13", "READERR T23", "SHUT ERR"
RESULT3 (CMD_STATE): char 02345
State ENUM "OK", "DISABLE", "BUSY", "CARD WAIT", "NO CARD IN BOX"
Protocol dumps and more:
https://www.arcade-projects.com/threads/naomi-2-chihiro-triforce-card-reader-emulator-initial-d3-wmmt-mario-kart-f-zero-ax.814/
Bits from YACardEmu: https://github.com/GXTX/YACardEmu/
Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
Copyright (C) 2022-2023 tugpoat (https://github.com/tugpoat)
*/
class SanwaCRP1231BR : public CardReaderWriter, public SerialPort::Pipe
{
public:
void write(u8 b) override
{
if (inBuffer.empty() && b == ENQ)
{
DEBUG_LOG(NAOMI, "Received RQ(5)");
handleCommand();
return;
}
inBuffer.push_back(b);
if (inBuffer.size() >= 3)
{
if (inBuffer[0] != STX)
{
INFO_LOG(NAOMI, "Unexpected cmd start byte %x", inBuffer[0]);
inBuffer.clear();
return;
}
u32 len = inBuffer[1];
if (inBuffer.size() < len + 2)
{
if (inBuffer.size() == 256)
{
WARN_LOG(NAOMI, "Card reader buffer overflow");
inBuffer.clear();
}
return;
}
u32 crc = calcCrc(inBuffer.begin() + 1, inBuffer.end() - 1);
if (crc != inBuffer.back())
{
INFO_LOG(NAOMI, "Wrong crc: expected %x got %x", crc, inBuffer.back());
inBuffer.clear();
return;
}
DEBUG_LOG(NAOMI, "Received cmd %x len %d", inBuffer[2], inBuffer[1]);
outBuffer.push_back(ACK);
rxCommandLen = std::min(inBuffer.size() - 3, sizeof(rxCommand));
memcpy(rxCommand, &inBuffer[2], rxCommandLen);
inBuffer.clear();
}
}
u8 read() override
{
if (outBuffer.empty())
return 0;
u8 b = outBuffer.front();
outBuffer.pop_front();
DEBUG_LOG(NAOMI, "Sending %x", b);
return b;
}
int available() override {
return outBuffer.size();
}
protected:
enum Commands
{
CARD_INIT = 0x10,
CARD_GET_CARD_STATE = 0x20,
CARD_CANCEL = 0x40,
CARD_LOAD_CARD = 0xB0,
CARD_CLEAN_CARD = 0xA0,
CARD_READ = 0x33,
CARD_WRITE = 0x53,
CARD_PRINT = 0x7C,
CARD_PRINT_SETTINGS = 0x78,
CARD_REGISTER_FONT = 0x7A,
CARD_ERASE_PRINT = 0x7D,
CARD_DOOR = 0xD0,
CARD_EJECT = 0x80,
CARD_NEW = 0xB0,
};
bool loadCard() override {
return CardReaderWriter::loadCard(cardData, sizeof(cardData));
}
virtual u8 getStatus1()
{
return ((doorOpen ? 2 : 1) << 6) | 0x20 | (cardInserted ? 0x18 : 0);
}
void handleCommand()
{
if (rxCommandLen == 0)
return;
outBuffer.push_back(STX);
u32 crcIdx = outBuffer.size();
u8 status1 = getStatus1();
u8 status2 = '0';
u8 status3 = '0';
switch (rxCommand[0])
{
case CARD_DOOR:
doorOpen = rxCommand[4] == '1';
INFO_LOG(NAOMI, "Door %s", doorOpen ? "open" : "closed");
status1 = getStatus1();
break;
case CARD_NEW:
INFO_LOG(NAOMI, "New card");
cardInserted = true;
doorOpen = false;
status1 = getStatus1();
break;
case CARD_WRITE:
// 4: mode ('0': read 0x45 bytes, '1': variable length write 1-47 bytes)
// 5: parity ('0': 7-bit parity, '1': 8-bit no parity)
// 6: track (see below)
INFO_LOG(NAOMI, "Card write mode %c parity %c track %c", rxCommand[4], rxCommand[5], rxCommand[6]);
switch (rxCommand[6])
{
case '0': // track 1
memcpy(cardData, &rxCommand[7], TRACK_SIZE);
break;
case '1': // track 2
memcpy(cardData + TRACK_SIZE, &rxCommand[7], TRACK_SIZE);
break;
case '2': // track 3
memcpy(cardData + TRACK_SIZE * 2, &rxCommand[7], TRACK_SIZE);
break;
case '3': // track 1 & 2
memcpy(cardData, &rxCommand[7], TRACK_SIZE * 2);
break;
case '4': // track 1 & 3
memcpy(cardData, &rxCommand[7], TRACK_SIZE);
memcpy(cardData + TRACK_SIZE * 2, &rxCommand[7 + TRACK_SIZE], TRACK_SIZE);
break;
case '5': // track 2 & 3
memcpy(cardData + TRACK_SIZE, &rxCommand[7], TRACK_SIZE * 2);
break;
case '6': // track 1 2 & 3
memcpy(cardData, &rxCommand[7], TRACK_SIZE * 3);
break;
default:
WARN_LOG(NAOMI, "Unknown track# %02x", rxCommand[6]);
break;
}
saveCard(cardData, sizeof(cardData));
break;
case CARD_READ:
// 4: mode ('0': read 0x45 bytes, '1': variable length read 1-47 bytes, '2': card capture, pull in card?)
// 5: parity ('0': 7-bit parity, '1': 8-bit no parity)
// 6: track (see below)
INFO_LOG(NAOMI, "Card read mode %c parity %c track %c", rxCommand[4], rxCommand[5], rxCommand[6]);
if (!cardInserted || doorOpen)
status3 = cardInserted ? '0' : '4';
break;
case CARD_EJECT:
NOTICE_LOG(NAOMI, "Card ejected");
if (cardInserted)
os_notify("Card ejected", 2000);
cardInserted = false;
status1 = getStatus1();
break;
case CARD_CANCEL:
case CARD_GET_CARD_STATE:
case CARD_INIT:
case CARD_REGISTER_FONT:
case CARD_PRINT_SETTINGS:
case CARD_PRINT:
case CARD_CLEAN_CARD:
break;
default:
WARN_LOG(NAOMI, "Unknown command %x", rxCommand[0]);
break;
}
outBuffer.push_back(ACK);
outBuffer.push_back(rxCommand[0]);
outBuffer.push_back(status1);
outBuffer.push_back(status2);
outBuffer.push_back(status3);
if (rxCommand[0] == CARD_READ && cardInserted && !doorOpen && rxCommand[4] != '2')
{
u32 idx = 0;
u32 size = TRACK_SIZE;
switch (rxCommand[6])
{
case '0': // track 1
break;
case '1': // track 2
idx = TRACK_SIZE;
break;
case '2': // track 3
idx = TRACK_SIZE * 2;
break;
case '3': // track 1 & 2
size = TRACK_SIZE * 2;
break;
case '4': // track 1 & 3
for (u32 i = 0; i < TRACK_SIZE; i++)
outBuffer.push_back(cardData[i]);
outBuffer[crcIdx] += TRACK_SIZE;
idx = TRACK_SIZE * 2;
break;
case '5': // track 2 & 3
idx = TRACK_SIZE;
size = TRACK_SIZE * 2;
break;
case '6': // track 1 2 & 3
size = TRACK_SIZE * 3;
break;
default:
WARN_LOG(NAOMI, "Unknown track# %02x", rxCommand[6]);
size = 0;
break;
}
for (u32 i = 0; i < size; i++)
outBuffer.push_back(cardData[idx + i]);
outBuffer[crcIdx] += size;
}
outBuffer.push_back(ETX);
outBuffer.push_back(calcCrc(outBuffer.begin() + crcIdx, outBuffer.end()));
}
u8 rxCommand[256];
u32 rxCommandLen = 0;
static constexpr u32 TRACK_SIZE = 0x45;
u8 cardData[TRACK_SIZE * 3];
bool doorOpen = false;
};
class SanwaCRP1231LR : public SanwaCRP1231BR
{
u8 getStatus1() override
{
// '0' no card
// '1' pos magnetic read/write
// '2' pos thermal printer
// '3' pos thermal dispenser
// '4' ejected not removed
return cardInserted ? '1' : '0';
}
};
// Hooked to the SH4 SCIF serial port
class InitialDCardReader final : public SanwaCRP1231BR
{
public:
InitialDCardReader() {
SCIFSerialPort::Instance().setPipe(this);
}
~InitialDCardReader() override {
SCIFSerialPort::Instance().setPipe(nullptr);
}
};
// Hooked to the MIE via a 838-13661 RS232/RS422 converter board
class DerbyBRCardReader final : public SanwaCRP1231BR
{
public:
DerbyBRCardReader() {
getMieDevice()->setPipe(this);
}
~DerbyBRCardReader() override {
getMieDevice()->setPipe(nullptr);
}
};
class DerbyLRCardReader final : public SanwaCRP1231LR
{
public:
DerbyLRCardReader() {
getMieDevice()->setPipe(this);
}
~DerbyLRCardReader() override {
getMieDevice()->setPipe(nullptr);
}
};
/*
Club Kart - Sanwa CR-1231R
>>> SEND CMD: [START 02][CMD char1][CMD char2]{parameter char}{data}[STOP 03][CRC]
<<< RECV ACK: [OK 06] or [ERR 15]
<<< RECV STX: [START 02]{REPLY}{data}[STOP 03][CRC]
note: it seems reply packet sent only after command fully completed or error happened
REPLY: 2chars
OK - RESULT_OK
O1 - RESULT_CANCEL_INSERT
N0 - ERROR_CONNECT / Unknown Error
N1 - ERROR_COMMAND / Connection Error
N2 - ERROR_MOTOR / Mechanic Error 1
N3 - ERROR_HEAD_UPDOWN / Mechanic Error 2
N4 - ERROR_CARD_STUCK / Card Stuffed
N5 - ERROR_VERIFY / OK ????
N6 - ERROR_HEAD_TEMP / Mechanic Error 3
N7 - ERROR_CARD_EMPTY / Card Empty
N8 - ERROR_CARD_LOAD / Draw Card Error
N9 - ERROR_NO_HOPPER / Card Empty
NA - ERROR_CARD_PRESENT
NB - ERROR_CARD_EJECT
NC - ERROR_CANT_CANCEL
ND - ERROR_NOT_INSERT
NE - ERROR_NOT_WAIT
NF - ERROR_BAD_CARD
CMD SS REPLY: 6 ASCII characters
5chars '0'/'1' - Card Sensors Status, MSB first
0
10 18
1C C E 8 7 3
other
1char '0'/'1' - Dispenser Status
'0' - Empty
'1' - Full
Commands:
IN - init
CA - cancel command
OT0 - eject card
HI - get new card from dispenser
CL - cleaning
RT5 - unknown
RL - read data/load card into reader
WL - write data (followed by 69 bytes of data)
SS - get status
reply for commands is simple
06
02 'O' 'K' 03 crc
except
- RL command, which have 69 bytes of card data after OK (or no any reply if no card insterted)
- SS command, which is
06
02 '0/1' '0/1' '0/1' '0/1' '0/1' '0/1' 03 crc
there 0/1 - encoded in char binary value described earlier
*/
class ClubKartCardReader : public CardReaderWriter, SerialPort::Pipe
{
public:
ClubKartCardReader() {
SCIFSerialPort::Instance().setPipe(this);
}
~ClubKartCardReader() override {
SCIFSerialPort::Instance().setPipe(nullptr);
}
void write(u8 data) override
{
inBuffer.push_back(data);
if (inBuffer.size() == 5)
{
if ((inBuffer[1] != 'W' || inBuffer[2] != 'L') && inBuffer[2] != 'T')
{
handleCommand();
inBuffer.clear();
}
}
else if (inBuffer.size() == 6 && inBuffer[2] == 'T') // OT0, RT5
{
handleCommand();
inBuffer.clear();
}
else if (inBuffer.size() == TRACK_SIZE + 5) // WL
{
handleCommand();
inBuffer.clear();
}
}
int available() override {
return outBuffer.size();
}
u8 read() override
{
if (outBuffer.empty())
return 0;
u8 b = outBuffer.front();
outBuffer.pop_front();
return b;
}
private:
enum Commands {
CARD_INIT,
CARD_CANCEL_CMD,
CARD_EJECT,
CARD_NEW,
CARD_CLEAN,
CARD_RT5,
CARD_READ,
CARD_WRITE,
CARD_STATUS,
CARD_MAX
};
static const u8 CommandBytes[][2];
bool loadCard() override
{
bool rc = CardReaderWriter::loadCard(cardData, sizeof(cardData));
if (rc && readPending)
{
sendReply(CARD_READ);
readPending = false;
}
return rc;
}
void handleCommand()
{
readPending = false;
int cmd;
for (cmd = 0; cmd < CARD_MAX; cmd++)
if (inBuffer[1] == CommandBytes[cmd][0] && inBuffer[2] == CommandBytes[cmd][1])
break;
if (cmd == CARD_MAX)
{
WARN_LOG(NAOMI, "Unhandled command '%c%c'", inBuffer[1], inBuffer[2]);
return;
}
u32 crc = calcCrc(inBuffer.begin() + 1, inBuffer.end() - 1);
if (crc != inBuffer.back())
{
WARN_LOG(NAOMI, "Wrong crc: expected %x got %x", crc, inBuffer.back());
return;
}
outBuffer.push_back(ACK);
switch (cmd)
{
case CARD_WRITE:
INFO_LOG(NAOMI, "Card write");
for (u32 i = 0; i < sizeof(cardData); i++)
cardData[i] = inBuffer[i + 3];
saveCard(cardData, sizeof(cardData));
break;
case CARD_READ:
INFO_LOG(NAOMI, "Card read");
if (!cardInserted) {
readPending = true;
return;
}
break;
case CARD_EJECT:
NOTICE_LOG(NAOMI, "Card ejected");
if (cardInserted)
os_notify("Card ejected", 2000);
cardInserted = false;
break;
case CARD_NEW:
INFO_LOG(NAOMI, "New card");
cardInserted = true;
break;
case CARD_INIT:
DEBUG_LOG(NAOMI, "Card init");
break;
case CARD_CANCEL_CMD:
DEBUG_LOG(NAOMI, "Cancel cmd");
break;
case CARD_CLEAN:
DEBUG_LOG(NAOMI, "Card clean");
break;
case CARD_RT5:
DEBUG_LOG(NAOMI, "Card RT5");
break;
case CARD_STATUS:
DEBUG_LOG(NAOMI, "Card status (cardInserted %d)", cardInserted);
break;
}
sendReply(cmd);
}
void sendReply(int cmd)
{
outBuffer.push_back(STX);
u32 crcIndex = outBuffer.size();
if (cmd == CARD_STATUS)
{
outBuffer.push_back('0');
outBuffer.push_back('0');
outBuffer.push_back('0');
outBuffer.push_back(cardInserted ? '1' : '0');
outBuffer.push_back(cardInserted ? '1' : '0');
outBuffer.push_back('1'); // dispenser full
}
else
{
outBuffer.push_back('O');
outBuffer.push_back('K');
if (cmd == CARD_READ) {
for (u32 i = 0; i < sizeof(cardData); i++)
outBuffer.push_back(cardData[i]);
}
}
outBuffer.push_back(ETX);
outBuffer.push_back(calcCrc(outBuffer.begin() + crcIndex, outBuffer.end()));
}
static constexpr u32 TRACK_SIZE = 0x45;
u8 cardData[TRACK_SIZE];
bool readPending = false;
};
const u8 ClubKartCardReader::CommandBytes[][2]
{
{ 'I', 'N' }, // init
{ 'C', 'A' }, // cancel command
{ 'O', 'T' }, // ...0 - eject card
{ 'H', 'I' }, // get new card from dispenser
{ 'C', 'L' }, // cleaning
{ 'R', 'T' }, // ...5 - unknown
{ 'R', 'L' }, // read data/load card into reader
{ 'W', 'L' }, // write data (followed by 69 bytes of data)
{ 'S', 'S' }, // get status
};
static std::unique_ptr cardReader;
void initdInit() {
term();
cardReader = std::make_unique();
}
void derbyInit()
{
term();
if (settings.content.gameId == " DERBY OWNERS CLUB WE ---------")
cardReader = std::make_unique();
else
cardReader = std::make_unique();
}
void clubkInit() {
term();
cardReader = std::make_unique();
}
void term() {
cardReader.reset();
}
class BarcodeReader final : public SerialPort::Pipe
{
public:
BarcodeReader() {
SCIFSerialPort::Instance().setPipe(this);
}
~BarcodeReader() override {
SCIFSerialPort::Instance().setPipe(nullptr);
}
int available() override {
return toSend.size();
}
u8 read() override
{
u8 data = toSend.front();
toSend.pop_front();
return data;
}
void insertCard()
{
if (toSend.size() >= 32)
return;
INFO_LOG(NAOMI, "Card read: %s", card.c_str());
std::string data = card + "*";
toSend.insert(toSend.end(), (const u8 *)&data[0], (const u8 *)(&data.back() + 1));
SCIFSerialPort::Instance().updateStatus();
}
std::string getCard() const {
return card;
}
void setCard(const std::string& card) {
this->card = card;
}
private:
std::deque toSend;
std::string card;
};
static std::unique_ptr barcodeReader;
void barcodeInit() {
barcodeReader = std::make_unique();
}
void barcodeTerm() {
barcodeReader.reset();
}
bool barcodeAvailable() {
return barcodeReader != nullptr;
}
std::string barcodeGetCard()
{
if (barcodeReader != nullptr)
return barcodeReader->getCard();
else
return "";
}
void barcodeSetCard(const std::string& card)
{
if (barcodeReader != nullptr)
barcodeReader->setCard(card);
}
void insertCard(int playerNum)
{
if (cardReader != nullptr)
cardReader->insertCard();
else if (barcodeReader != nullptr)
barcodeReader->insertCard();
else
insertRfidCard(playerNum);
}
bool readerAvailable()
{
return cardReader != nullptr
|| barcodeAvailable()
|| getRfidCardData(0) != nullptr;
}
}