#ifndef LINK_MOBILE_H #define LINK_MOBILE_H // -------------------------------------------------------------------------- // A high level driver for the Mobile Adapter GB. // Check out the REON project -> https://github.com/REONTeam // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: // LinkMobile* linkMobile = new LinkMobile(); // - 2) Add the required interrupt service routines: (*) // interrupt_init(); // interrupt_add(INTR_VBLANK, LINK_MOBILE_ISR_VBLANK); // interrupt_add(INTR_SERIAL, LINK_MOBILE_ISR_SERIAL); // interrupt_add(INTR_TIMER3, LINK_MOBILE_ISR_TIMER); // - 3) Initialize the library with: // linkMobile->activate(); // // (do something until `linkMobile->isSessionActive()` returns `true`) // - 4) Call someone: // linkMobile->call("127000000001"); // // (do something until `linkMobile->isConnectedP2P()` returns `true`) // - 5) Send/receive data: // LinkMobile::DataTransfer dataTransfer = { .size = 5 }; // for (u32 i = 0; i < 5; i++) // dataTransfer.data[i] = ((u8*)"hello")[i]; // linkMobile->transfer(dataTransfer, &dataTransfer); // // (do something until `dataTransfer.completed` is `true`) // // (use `dataTransfer` as the received data) // - 6) Hang up: // linkMobile->hangUp(); // - 7) Connect to the internet: // linkMobile->callISP("REON password"); // // (do something until `linkMobile->isConnectedPPP()` returns `true`) // - 8) Run DNS queries: // LinkMobile::DNSQuery dnsQuery; // linkMobile->dnsQuery("something.com", &dnsQuery); // // (do something until `dnsQuery.completed` is `true`) // // (use `dnsQuery.success` and `dnsQuery.ipv4`) // - 9) Open connections: // auto type = LinkMobile::ConnectionType::TCP; // LinkMobile::OpenConn openConn; // linkMobile->openConnection(dnsQuery.ipv4, connType, &openConn); // // (do something until `openConn.completed` is `true`) // // (use `openConn.connectionId` as last argument of `transfer(...)`) // - 10) Close connections: // LinkMobile::CloseConn closeConn; // linkMobile->closeConnection(openConn.connectionId, type, &closeConn); // // (do something until `closeConn.completed` is `true`) // - 11) Synchronously wait for an action to be completed: // linkMobile->waitFor(&dnsQuery); // - 12) Turn off the adapter: // linkMobile->shutdown(); // -------------------------------------------------------------------------- // (*) libtonc's interrupt handler sometimes ignores interrupts due to a bug. // That causes packet loss. You REALLY want to use libugba's instead. // (see examples) // -------------------------------------------------------------------------- #ifndef LINK_DEVELOPMENT #pragma GCC system_header #endif #include "_link_common.hpp" #include "LinkGPIO.hpp" #include "LinkSPI.hpp" #ifndef LINK_MOBILE_QUEUE_SIZE /** * @brief Request queue size (how many commands can be queued at the same time). * The default value is `10`, which seems fine for most games. * \warning This affects how much memory is allocated. With the default value, * it's around 3 KB. */ #define LINK_MOBILE_QUEUE_SIZE 10 #endif LINK_VERSION_TAG LINK_MOBILE_VERSION = "vLinkMobile/v8.0.3"; #define LINK_MOBILE_MAX_USER_TRANSFER_LENGTH 254 #define LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH 255 #define LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH 32 #define LINK_MOBILE_MAX_LOGIN_ID_LENGTH 32 #define LINK_MOBILE_MAX_PASSWORD_LENGTH 32 #define LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH 253 #define LINK_MOBILE_COMMAND_TRANSFER_BUFFER \ (LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH + 4) #define LINK_MOBILE_DEFAULT_TIMEOUT (60 * 10) #define LINK_MOBILE_DEFAULT_TIMER_ID 3 #if LINK_ENABLE_DEBUG_LOGS != 0 #define _LMLOG_(...) Link::log(__VA_ARGS__) #else #define _LMLOG_(...) #endif /** * @brief A high level driver for the Mobile Adapter GB. */ class LinkMobile { private: using u32 = Link::u32; using u16 = Link::u16; using u8 = Link::u8; static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024; static constexpr int INIT_WAIT_FRAMES = 7; static constexpr int INIT_TIMEOUT_FRAMES = 60 * 3; static constexpr int PING_FREQUENCY_FRAMES = 60; static constexpr int ADAPTER_WAITING = 0xD2; static constexpr u32 ADAPTER_WAITING_32BIT = 0xD2D2D2D2; static constexpr int GBA_WAITING = 0x4B; static constexpr u32 GBA_WAITING_32BIT = 0x4B4B4B4B; static constexpr int OR_VALUE = 0x80; static constexpr int COMMAND_MAGIC_VALUE1 = 0x99; static constexpr int COMMAND_MAGIC_VALUE2 = 0x66; static constexpr int DEVICE_GBA = 0x1; static constexpr int DEVICE_ADAPTER_BLUE = 0x8; static constexpr int DEVICE_ADAPTER_YELLOW = 0x9; static constexpr int DEVICE_ADAPTER_GREEN = 0xA; static constexpr int DEVICE_ADAPTER_RED = 0xB; static constexpr int ACK_SENDER = 0; static constexpr int CONFIGURATION_DATA_SIZE = 192; static constexpr int CONFIGURATION_DATA_CHUNK = CONFIGURATION_DATA_SIZE / 2; static constexpr const char* FALLBACK_ISP_NUMBER = "#9677"; static constexpr int COMMAND_BEGIN_SESSION = 0x10; static constexpr int COMMAND_END_SESSION = 0x11; static constexpr int COMMAND_DIAL_TELEPHONE = 0x12; static constexpr int COMMAND_HANG_UP_TELEPHONE = 0x13; static constexpr int COMMAND_WAIT_FOR_TELEPHONE_CALL = 0x14; static constexpr int COMMAND_TRANSFER_DATA = 0x15; static constexpr int COMMAND_RESET = 0x16; static constexpr int COMMAND_TELEPHONE_STATUS = 0x17; static constexpr int COMMAND_SIO32 = 0x18; static constexpr int COMMAND_READ_CONFIGURATION_DATA = 0x19; static constexpr int COMMAND_ISP_LOGIN = 0x21; static constexpr int COMMAND_ISP_LOGOUT = 0x22; static constexpr int COMMAND_OPEN_TCP_CONNECTION = 0x23; static constexpr int COMMAND_CLOSE_TCP_CONNECTION = 0x24; static constexpr int COMMAND_OPEN_UDP_CONNECTION = 0x25; static constexpr int COMMAND_CLOSE_UDP_CONNECTION = 0x26; static constexpr int COMMAND_DNS_QUERY = 0x28; static constexpr int COMMAND_CONNECTION_CLOSED = 0x1F; static constexpr int COMMAND_ERROR_STATUS = 0x6E | OR_VALUE; static constexpr u8 WAIT_TICKS[] = {4, 8}; static constexpr int LOGIN_PARTS_SIZE = 8; static constexpr u8 LOGIN_PARTS[] = {0x4E, 0x49, 0x4E, 0x54, 0x45, 0x4E, 0x44, 0x4F}; static constexpr int SUPPORTED_DEVICES_SIZE = 4; static constexpr u8 SUPPORTED_DEVICES[] = { DEVICE_ADAPTER_BLUE, DEVICE_ADAPTER_YELLOW, DEVICE_ADAPTER_GREEN, DEVICE_ADAPTER_RED}; static constexpr u8 DIAL_PHONE_FIRST_BYTE[] = {0, 2, 1, 1}; public: enum class State { NEEDS_RESET = 0, PINGING = 1, WAITING_TO_START = 2, STARTING_SESSION = 3, ACTIVATING_SIO32 = 4, WAITING_32BIT_SWITCH = 5, READING_CONFIGURATION = 6, SESSION_ACTIVE = 7, CALL_REQUESTED = 8, CALLING = 9, CALL_ESTABLISHED = 10, ISP_CALL_REQUESTED = 11, ISP_CALLING = 12, PPP_LOGIN = 13, PPP_ACTIVE = 14, SHUTDOWN_REQUESTED = 15, ENDING_SESSION = 16, WAITING_8BIT_SWITCH = 17, SHUTDOWN = 18 }; enum class Role { NO_P2P_CONNECTION, CALLER, RECEIVER }; enum class ConnectionType { TCP, UDP }; struct ConfigurationData { char magic[2]; u8 registrationState; u8 _unused1_; u8 primaryDNS[4]; u8 secondaryDNS[4]; char loginId[10]; u8 _unused2_[22]; char email[24]; u8 _unused3_[6]; char smtpServer[20]; char popServer[19]; u8 _unused4_[5]; u8 configurationSlot1[24]; u8 configurationSlot2[24]; u8 configurationSlot3[24]; u8 checksumHigh; u8 checksumLow; char _ispNumber1[16 + 1]; // (parsed from `configurationSlot1`) } LINK_PACKED; struct AsyncRequest { volatile bool completed = false; bool success = false; bool fail() { success = false; completed = true; return false; } }; struct DNSQuery : public AsyncRequest { u8 ipv4[4] = {}; }; struct OpenConn : public AsyncRequest { u8 connectionId = 0; }; struct CloseConn : public AsyncRequest {}; struct DataTransfer : public AsyncRequest { u8 data[LINK_MOBILE_MAX_USER_TRANSFER_LENGTH] = {}; u8 size = 0; }; enum class CommandResult { PENDING, SUCCESS, INVALID_DEVICE_ID, INVALID_COMMAND_ACK, INVALID_MAGIC_BYTES, WEIRD_DATA_SIZE, WRONG_CHECKSUM, ERROR_CODE, WEIRD_ERROR_CODE }; struct Error { enum class Type { NONE, ADAPTER_NOT_CONNECTED, PPP_LOGIN_FAILED, COMMAND_FAILED, WEIRD_RESPONSE, TIMEOUT, WTF }; Error::Type type = Error::Type::NONE; State state = State::NEEDS_RESET; u8 cmdId = 0; CommandResult cmdResult = CommandResult::PENDING; u8 cmdErrorCode = 0; bool cmdIsSending = false; int reqType = -1; }; /** * @brief Constructs a new LinkMobile object. * @param timeout Number of *frames* without completing a request to reset a * connection. Defaults to 600 (10 seconds). * @param timerId `(0~3)` GBA Timer to use for waiting. */ explicit LinkMobile(u32 timeout = LINK_MOBILE_DEFAULT_TIMEOUT, u8 timerId = LINK_MOBILE_DEFAULT_TIMER_ID) { config.timeout = timeout; config.timerId = timerId; } /** * @brief Returns whether the library is active or not. */ [[nodiscard]] bool isActive() { return isEnabled; } /** * @brief Activates the library. After some time, if an adapter is connected, * the state will be changed to `SESSION_ACTIVE`. If not, the state will be * `NEEDS_RESET`, and you can retrieve the error with `getError()`. */ void activate() { LINK_READ_TAG(LINK_MOBILE_VERSION); static_assert(LINK_MOBILE_QUEUE_SIZE >= 1); error = {}; LINK_BARRIER; isEnabled = false; LINK_BARRIER; resetState(); stop(); LINK_BARRIER; isEnabled = true; LINK_BARRIER; start(); } /** * @brief Deactivates the library, resetting the serial mode to GPIO. * \warning Calling `shutdown()` first is recommended, but the adapter will * put itself in sleep mode after 3 seconds anyway. */ void deactivate() { error = {}; isEnabled = false; resetState(); stop(); } /** * @brief Gracefully shuts down the adapter, closing all connections. * After some time, the state will be changed to `SHUTDOWN`, and only then * it's safe to call `deactivate()`. * \warning Non-blocking. Returns `true` immediately, or `false` if there's no * active session or available request slots. */ bool shutdown() { if (!canShutdown() || userRequests.isFull()) return false; pushRequest(UserRequest{.type = UserRequest::Type::SHUTDOWN}); return true; } /** * @brief Initiates a P2P connection with a `phoneNumber`. After some time, * the state will be `CALL_ESTABLISHED` (or `ACTIVE_SESSION` if the * connection fails or ends). * @param phoneNumber The phone number to call. In REON/libmobile this can be * a number assigned by the relay server, or a 12-digit IPv4 address (for * example, "127000000001" would be 127.0.0.1). * \warning Non-blocking. Returns `true` immediately, or `false` if there's no * active session or available request slots. */ bool call(const char* phoneNumber) { if (state != State::SESSION_ACTIVE || userRequests.isFull()) return false; auto request = UserRequest{.type = UserRequest::Type::CALL}; copyString(request.phoneNumber, phoneNumber, LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH); pushRequest(request); return true; } /** * @brief Calls the ISP number registered in the adapter configuration, or a * default number if the adapter hasn't been configured. Then, performs a * login operation using the provided REON `password` and `loginId`. After * some time, the state will be `PPP_ACTIVE`. If `loginId` is empty and the * adapter has been configured, it will use the one stored in the * configuration. * @param password The password, as a null-terminated string (max `32` * characters). * @param loginId The login ID, as a null-terminated string (max `32` * characters). It can be empty if it's already stored in the configuration. * \warning Non-blocking. Returns `true` immediately, or `false` * if there's no active session, no available request slots, or no login ID. */ bool callISP(const char* password, const char* loginId = "") { if (state != State::SESSION_ACTIVE || userRequests.isFull()) return false; auto request = UserRequest{.type = UserRequest::Type::PPP_LOGIN}; copyString(request.password, password, LINK_MOBILE_MAX_PASSWORD_LENGTH); if (Link::strlen(loginId) > 0) copyString(request.loginId, loginId, LINK_MOBILE_MAX_LOGIN_ID_LENGTH); else if (adapterConfiguration.isValid()) copyString(request.loginId, adapterConfiguration.fields._ispNumber1, LINK_MOBILE_MAX_LOGIN_ID_LENGTH); else return false; pushRequest(request); return true; } /** * @brief Looks up the IPv4 address for a domain name. * @param domain A null-terminated string for the domain name (max `253` * characters). It also accepts a ASCII IPv4 address, converting it into a * 4-byte address instead of querying the DNS server. * @param result A pointer to a `LinkMobile::DNSQuery` struct that * will be filled with the result. When the request is completed, the * `completed` field will be `true`. If an IP address was found, the `success` * field will be `true` and the `ipv4` field can be read as a 4-byte address. * \warning Non-blocking. Returns `true` immediately, or `false` if there's no * active PPP session or available request slots. */ bool dnsQuery(const char* domainName, DNSQuery* result) { if (state != State::PPP_ACTIVE || userRequests.isFull()) return result->fail(); result->completed = false; result->success = false; u32 size = Link::strlen(domainName); if (size > LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH) size = LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH; auto request = UserRequest{.type = UserRequest::Type::DNS_QUERY, .dns = result, .send = {.data = {}}, .commandSent = false}; for (u32 i = 0; i < size; i++) request.send.data[i] = domainName[i]; request.send.size = size; pushRequest(request); return true; } /** * @brief Opens a TCP/UDP (`type`) connection at the given `ip` (4-byte * address) on the given `port`. * @param ip The 4-byte address. * @param port The port. * @param type One of the enum values from `LinkMobile::ConnectionType`. * @param result A pointer to a `LinkMobile::OpenConn` struct that * will be filled with the result. When the request is completed, the * `completed` field will be `true`. If the connection was successful, the * `success` field will be `true` and the `connectionId` field can be used * when calling the `transfer(...)` method. If not, you can assume that the * connection was closed. * \warning Only `2` connections can be opened at the same time. * \warning Non-blocking. Returns `true` immediately, or `false` if there's no * active PPP session, no available request slots. */ bool openConnection(const u8* ip, u16 port, ConnectionType type, OpenConn* result) { if (state != State::PPP_ACTIVE || userRequests.isFull()) return result->fail(); result->completed = false; result->success = false; auto request = UserRequest{.type = UserRequest::Type::OPEN_CONNECTION, .open = result, .connectionType = type, .commandSent = false}; for (u32 i = 0; i < 4; i++) request.ip[i] = ip[i]; request.port = port; pushRequest(request); return true; } /** * @brief Closes an active TCP/UDP (`type`) connection. * @param connectionId The ID of the connection. * @param type One of the enum values from `LinkMobile::ConnectionType`. * @param result A pointer to a `LinkMobile::CloseConn` struct that * will be filled with the result. When the request is completed, the * `completed` field will be `true`. If the connection was closed correctly, * the `success` field will be `true`. * \warning Non-blocking. Returns `true` immediately, or `false` if there's no * active PPP session, no available request slots. */ bool closeConnection(u8 connectionId, ConnectionType type, CloseConn* result) { if (state != State::PPP_ACTIVE || userRequests.isFull()) return result->fail(); result->completed = false; result->success = false; auto request = UserRequest{.type = UserRequest::Type::CLOSE_CONNECTION, .close = result, .connectionType = type, .commandSent = false}; request.connectionId = connectionId; pushRequest(request); return true; } /** * @brief Requests a data transfer and responds the received data. The * transfer can be done with the other node in a P2P connection, or with any * open TCP/UDP connection if a PPP session is active. In the case of a * TCP/UDP connection, the `connectionId` must be provided. * @param dataToSend The data to send, up to 254 bytes. * @param result A pointer to a `LinkMobile::DataTransfer` struct that * will be filled with the received data. It can also point to `dataToSend` to * reuse the struct. When the transfer is completed, the `completed` field * will be `true`. If the transfer was successful, the `success` field will be * `true`. * @param connectionId The ID of the connection (or `0xFF` for P2P). * \warning Non-blocking. Returns `true` immediately, or `false` if * there's no active call or available request slots. */ bool transfer(DataTransfer dataToSend, DataTransfer* result, u8 connectionId = 0xFF) { if ((state != State::CALL_ESTABLISHED && state != State::PPP_ACTIVE) || userRequests.isFull()) return result->fail(); result->completed = false; result->success = false; auto request = UserRequest{.type = UserRequest::Type::TRANSFER, .connectionId = connectionId, .send = {.data = {}, .size = dataToSend.size}, .receive = result, .commandSent = false}; for (u32 i = 0; i < dataToSend.size; i++) request.send.data[i] = dataToSend.data[i]; pushRequest(request); return true; } /** * @brief Waits for `asyncRequest` to be completed. Returns `true` if the * request was completed && successful, and the adapter session is still * alive. Otherwise, it returns `false`. * @param asyncRequest A pointer to a `LinkMobile::DNSQuery`, * `LinkMobile::OpenConn`, `LinkMobile::CloseConn`, or * `LinkMobile::DataTransfer`. */ bool waitFor(AsyncRequest* asyncRequest) { while (isSessionActive() && !asyncRequest->completed) Link::_IntrWait(1, Link::_IRQ_SERIAL | Link::_IRQ_VBLANK); return isSessionActive() && asyncRequest->completed && asyncRequest->success; } /** * @brief Hangs up the current P2P or PPP call. Closes all connections. * \warning Non-blocking. Returns `true` immediately, or `false` if there's no * active call or available request slots. */ bool hangUp() { if ((state != State::CALL_ESTABLISHED && state != State::PPP_ACTIVE) || userRequests.isFull()) return false; pushRequest(UserRequest{.type = UserRequest::Type::HANG_UP}); return true; } /** * @brief Retrieves the adapter configuration. * @param configurationData A structure that will be filled with the * configuration data. If the adapter has an active session, the data is * already loaded, so it's instantaneous. * \warning Returns `true` if `configurationData` has been filled, or `false` * if there's no active session. */ [[nodiscard]] bool readConfiguration(ConfigurationData& configurationData) { if (!isSessionActive()) return false; configurationData = adapterConfiguration.fields; return true; } /** * @brief Returns the current state. * @return One of the enum values from `LinkMobile::State`. */ [[nodiscard]] State getState() { return state; } /** * @brief Returns the current role in the P2P connection. * @return One of the enum values from `LinkMobile::Role`. */ [[nodiscard]] Role getRole() { return role; } /** * @brief Returns whether the adapter has been configured or not. * @return 1 = yes, 0 = no, -1 = unknown (no session active). */ [[nodiscard]] int isConfigurationValid() { if (!isSessionActive()) return -1; return (int)adapterConfiguration.isValid(); } /** * @brief Returns `true` if a P2P call is established (the state is * `CALL_ESTABLISHED`). */ [[nodiscard]] bool isConnectedP2P() { return state == State::CALL_ESTABLISHED; } /** * @brief Returns `true` if a PPP session is active (the state is * `PPP_ACTIVE`). */ [[nodiscard]] bool isConnectedPPP() { return state == State::PPP_ACTIVE; } /** * @brief Returns `true` if the session is active. */ [[nodiscard]] bool isSessionActive() { return state >= State::SESSION_ACTIVE && state <= State::SHUTDOWN_REQUESTED; } /** * @brief Returns `true` if there's an active session and there's no previous * shutdown requests. */ [[nodiscard]] bool canShutdown() { return isSessionActive() && state != State::SHUTDOWN_REQUESTED; } /** * @brief Returns the current operation mode (`LinkSPI::DataSize`). */ [[nodiscard]] LinkSPI::DataSize getDataSize() { return linkSPI.getDataSize(); } /** * @brief Returns details about the last error that caused the connection to * be aborted. */ [[nodiscard]] Error getError() { return error; } /** * @brief This method is called by the VBLANK interrupt handler. * \warning This is internal API! */ void _onVBlank() { if (!isEnabled) return; if (shouldAbortOnStateTimeout()) { timeoutStateFrames++; if (timeoutStateFrames >= INIT_TIMEOUT_FRAMES) return abort(Error::Type::ADAPTER_NOT_CONNECTED); } pingFrameCount++; if (pingFrameCount >= PING_FREQUENCY_FRAMES && isSessionActive() && !asyncCommand.isActive) { pingFrameCount = 0; cmdTelephoneStatus(); } processUserRequests(); processNewFrame(); } /** * @brief This method is called by the SERIAL interrupt handler. * \warning This is internal API! */ void _onSerial() { if (!isEnabled) return; linkSPI._onSerial(); u32 newData = linkSPI.getAsyncData(); if (state == State::NEEDS_RESET) return; if (asyncCommand.isActive) { if (asyncCommand.state == AsyncCommand::State::PENDING) { if (isSIO32Mode()) { if (asyncCommand.direction == AsyncCommand::Direction::SENDING) sendAsyncCommandSIO32(newData); else receiveAsyncCommandSIO32(newData); } else { if (asyncCommand.direction == AsyncCommand::Direction::SENDING) sendAsyncCommandSIO8(newData); else receiveAsyncCommandSIO8(newData); } if (asyncCommand.state == AsyncCommand::State::COMPLETED) { asyncCommand.isActive = false; processAsyncCommand(); } } } else { processLoosePacket(newData); } } /** * @brief This method is called by the TIMER interrupt handler. * \warning This is internal API! */ void _onTimer() { if (!isEnabled || !hasPendingTransfer) return; linkSPI.transferAsync(pendingTransfer); stopTimer(); hasPendingTransfer = false; } struct Config { u32 timeout; u8 timerId; }; /** * @brief LinkMobile configuration. * \warning `deactivate()` first, change the config, and `activate()` again! */ Config config; private: enum class AdapterType { BLUE, YELLOW, GREEN, RED, UNKNOWN }; struct UserRequest { enum class Type { CALL, PPP_LOGIN, DNS_QUERY, OPEN_CONNECTION, CLOSE_CONNECTION, TRANSFER, HANG_UP, SHUTDOWN }; Type type; char phoneNumber[LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH + 1]; char loginId[LINK_MOBILE_MAX_LOGIN_ID_LENGTH + 1]; char password[LINK_MOBILE_MAX_PASSWORD_LENGTH + 1]; DNSQuery* dns; OpenConn* open; CloseConn* close; u8 ip[4]; u16 port; ConnectionType connectionType; u8 connectionId; DataTransfer send; DataTransfer* receive; bool commandSent; u32 timeout; bool finished; void cleanup() { if (finished) return; AsyncRequest* metadata = type == UserRequest::Type::DNS_QUERY ? (AsyncRequest*)dns : type == UserRequest::Type::OPEN_CONNECTION ? (AsyncRequest*)open : type == UserRequest::Type::CLOSE_CONNECTION ? (AsyncRequest*)close : type == UserRequest::Type::TRANSFER ? (AsyncRequest*)receive : nullptr; if (metadata != nullptr) { metadata->success = false; metadata->completed = true; } } }; union AdapterConfiguration { ConfigurationData fields; char bytes[CONFIGURATION_DATA_SIZE]; bool isValid() { return fields.magic[0] == 'M' && fields.magic[1] == 'A' && (fields.registrationState & 1) == 1 && calculatedChecksum() == reportedChecksum(); } u16 calculatedChecksum() { u16 result = 0; for (u32 i = 0; i < CONFIGURATION_DATA_SIZE - 2; i++) result += bytes[i]; return result; } u16 reportedChecksum() { return Link::buildU16(fields.checksumHigh, fields.checksumLow); } }; struct MagicBytes { u8 magic1 = COMMAND_MAGIC_VALUE1; u8 magic2 = COMMAND_MAGIC_VALUE2; } LINK_PACKED; struct PacketData { u8 bytes[LINK_MOBILE_COMMAND_TRANSFER_BUFFER] = {}; } LINK_PACKED; struct PacketHeader { u8 commandId = 0; u8 _unused_ = 0; u8 _unusedSizeHigh_ = 0; u8 size = 0; u16 sum() { return commandId + _unused_ + _unusedSizeHigh_ + size; } u8 pureCommandId() { return commandId & (~OR_VALUE); } } LINK_PACKED; struct PacketChecksum { u8 high = 0; u8 low = 0; } LINK_PACKED; struct Command { MagicBytes magicBytes; PacketHeader header; PacketData data; PacketChecksum checksum; }; struct CommandResponse { CommandResult result = CommandResult::PENDING; Command command; }; struct AsyncCommand { enum class State { PENDING, COMPLETED }; enum class Direction { SENDING, RECEIVING }; State state; CommandResult result; u32 transferred; Command cmd; Direction direction; u16 expectedChecksum; u8 errorCommandId; u8 errorCode; bool isActive = false; void reset() { state = AsyncCommand::State::PENDING; result = CommandResult::PENDING; transferred = 0; cmd = Command{}; direction = AsyncCommand::Direction::SENDING; expectedChecksum = 0; errorCommandId = 0; errorCode = 0; isActive = false; } u8 relatedCommandId() { return result == CommandResult::ERROR_CODE ? errorCommandId : cmd.header.pureCommandId(); } bool respondsTo(u8 commandId) { return direction == AsyncCommand::Direction::RECEIVING && (result == CommandResult::ERROR_CODE ? errorCommandId == commandId : cmd.header.commandId == (commandId | OR_VALUE)); } void finish() { if (cmd.header.commandId == COMMAND_ERROR_STATUS) { if (cmd.header.size != 2) { result = CommandResult::WEIRD_ERROR_CODE; } else { result = CommandResult::ERROR_CODE; errorCommandId = cmd.data.bytes[0]; errorCode = cmd.data.bytes[1]; } } else { result = CommandResult::SUCCESS; } state = AsyncCommand::State::COMPLETED; } void fail(CommandResult _result) { result = _result; state = AsyncCommand::State::COMPLETED; } }; static constexpr u32 PREAMBLE_SIZE = sizeof(MagicBytes) + sizeof(PacketHeader); static constexpr u32 CHECKSUM_SIZE = sizeof(PacketChecksum); using RequestQueue = Link::Queue; LinkSPI linkSPI; RequestQueue userRequests; AdapterConfiguration adapterConfiguration; AsyncCommand asyncCommand; u32 waitFrames = 0; u32 timeoutStateFrames = 0; u32 pingFrameCount = 0; volatile Role role = Role::NO_P2P_CONNECTION; volatile State state = State::NEEDS_RESET; PacketData nextCommandData; u32 nextCommandDataSize = 0; bool hasPendingTransfer = false; u32 pendingTransfer = 0; AdapterType adapterType = AdapterType::UNKNOWN; Error error = {}; volatile bool isEnabled = false; void processUserRequests() { if (!userRequests.canMutate() || userRequests.isEmpty()) return; if (!isSessionActive()) { userRequests.clear(); return; } if (userRequests.peek().finished) userRequests.pop(); if (userRequests.isEmpty()) return; auto request = userRequests.peek(); request.timeout++; if (shouldAbortOnRequestTimeout() && request.timeout >= config.timeout) return abort(Error::Type::TIMEOUT); switch (request.type) { case UserRequest::Type::CALL: { if (state != State::SESSION_ACTIVE && state != State::CALL_REQUESTED) { popRequest(); return; } if (state != State::CALL_REQUESTED) setState(State::CALL_REQUESTED); if (!asyncCommand.isActive) { setState(State::CALLING); cmdDialTelephone(request.phoneNumber); popRequest(); } break; } case UserRequest::Type::PPP_LOGIN: { if (state != State::SESSION_ACTIVE && state != State::ISP_CALL_REQUESTED && state != State::ISP_CALLING) { popRequest(); return; } if (state == State::SESSION_ACTIVE) setState(State::ISP_CALL_REQUESTED); if (!asyncCommand.isActive && state == State::ISP_CALL_REQUESTED) { setState(State::ISP_CALLING); cmdDialTelephone(adapterConfiguration.isValid() ? adapterConfiguration.fields._ispNumber1 : FALLBACK_ISP_NUMBER); } break; } case UserRequest::Type::DNS_QUERY: { if (state != State::PPP_ACTIVE) { popRequest(); return; } if (!asyncCommand.isActive && !request.commandSent) { cmdDNSQuery(request.send.data, request.send.size); request.commandSent = true; } break; } case UserRequest::Type::OPEN_CONNECTION: { if (state != State::PPP_ACTIVE) { popRequest(); return; } if (!asyncCommand.isActive && !request.commandSent) { if (request.connectionType == ConnectionType::TCP) cmdOpenTCPConnection(request.ip, request.port); else cmdOpenUDPConnection(request.ip, request.port); request.commandSent = true; } break; } case UserRequest::Type::CLOSE_CONNECTION: { if (state != State::PPP_ACTIVE) { popRequest(); return; } if (!asyncCommand.isActive && !request.commandSent) { if (request.connectionType == ConnectionType::TCP) cmdCloseTCPConnection(request.connectionId); else cmdCloseUDPConnection(request.connectionId); request.commandSent = true; } break; } case UserRequest::Type::TRANSFER: { if (state != State::CALL_ESTABLISHED && state != State::PPP_ACTIVE) { popRequest(); return; } if (!asyncCommand.isActive && !request.commandSent) { cmdTransferData(request.connectionId, request.send.data, request.send.size); request.commandSent = true; } break; } case UserRequest::Type::HANG_UP: { if (state != State::CALL_ESTABLISHED && state != State::PPP_ACTIVE) { popRequest(); return; } if (!asyncCommand.isActive) cmdHangUpTelephone(); break; } case UserRequest::Type::SHUTDOWN: { if (state != State::SHUTDOWN_REQUESTED) setState(State::SHUTDOWN_REQUESTED); if (!asyncCommand.isActive) { setState(State::ENDING_SESSION); cmdEndSession(); popRequest(); } break; } default: { } } } void processNewFrame() { switch (state) { case State::WAITING_TO_START: { waitFrames--; if (waitFrames == 0) { setState(State::STARTING_SESSION); cmdBeginSession(); } break; } case State::WAITING_32BIT_SWITCH: { waitFrames--; if (waitFrames == 0) { setState(State::READING_CONFIGURATION); cmdReadConfigurationData(0, CONFIGURATION_DATA_CHUNK); } break; } case State::SESSION_ACTIVE: { if (!asyncCommand.isActive) cmdWaitForTelephoneCall(); break; } case State::WAITING_8BIT_SWITCH: { waitFrames--; if (waitFrames == 0) { error = {}; setState(State::SHUTDOWN); } break; } default: { } } } void processAsyncCommand() { if (asyncCommand.result != CommandResult::SUCCESS) { if (shouldAbortOnCommandFailure()) return abort(Error::Type::COMMAND_FAILED); else abort(Error::Type::COMMAND_FAILED, false); // (log the error) } _LMLOG_("%s $%X [%d]", asyncCommand.direction == AsyncCommand::Direction::SENDING ? ">!" : "type != UserRequest::Type::PPP_LOGIN) return abort(Error::Type::WTF); if (asyncCommand.result == CommandResult::SUCCESS) { setState(State::PPP_LOGIN); cmdISPLogin(request->loginId, request->password); } else { // (ISP call failed) setState(State::SESSION_ACTIVE); } request->finished = true; break; } case State::PPP_LOGIN: { if (!asyncCommand.respondsTo(COMMAND_ISP_LOGIN)) return; if (asyncCommand.result != CommandResult::SUCCESS) return abort(Error::Type::PPP_LOGIN_FAILED); setState(State::PPP_ACTIVE); break; } case State::PPP_ACTIVE: { if (asyncCommand.respondsTo(COMMAND_HANG_UP_TELEPHONE)) { setState(State::SESSION_ACTIVE); return; } if (userRequests.isEmpty()) return; auto request = userRequests.peekRef(); if (asyncCommand.respondsTo(COMMAND_DNS_QUERY)) { if (request->type != UserRequest::Type::DNS_QUERY) return abort(Error::Type::WTF); if (asyncCommand.result == CommandResult::SUCCESS) { if (asyncCommand.cmd.header.size != 4) return abort(Error::Type::WEIRD_RESPONSE); for (u32 i = 0; i < 4; i++) request->dns->ipv4[i] = asyncCommand.cmd.data.bytes[i]; request->dns->success = true; } else { request->dns->success = false; } request->dns->completed = true; request->finished = true; } else if (asyncCommand.respondsTo(COMMAND_OPEN_TCP_CONNECTION) || asyncCommand.respondsTo(COMMAND_OPEN_UDP_CONNECTION)) { if (request->type != UserRequest::Type::OPEN_CONNECTION) return abort(Error::Type::WTF); if (asyncCommand.result == CommandResult::SUCCESS) { if (asyncCommand.cmd.header.size != 1) return abort(Error::Type::WEIRD_RESPONSE); request->open->connectionId = asyncCommand.cmd.data.bytes[0]; request->open->success = true; } else { request->open->success = false; } request->open->completed = true; request->finished = true; } else if (asyncCommand.respondsTo(COMMAND_CLOSE_TCP_CONNECTION) || asyncCommand.respondsTo(COMMAND_CLOSE_UDP_CONNECTION)) { if (request->type != UserRequest::Type::CLOSE_CONNECTION) return abort(Error::Type::WTF); request->close->success = asyncCommand.result == CommandResult::SUCCESS; request->close->completed = true; request->finished = true; } else if (asyncCommand.respondsTo(COMMAND_TRANSFER_DATA)) { if (request->type != UserRequest::Type::TRANSFER) return abort(Error::Type::WTF); handleTransferDataResponse(request); } else if (asyncCommand.respondsTo(COMMAND_CONNECTION_CLOSED)) { if (request->type != UserRequest::Type::TRANSFER) return abort(Error::Type::WTF); // (connection closed) request->receive->success = false; request->receive->completed = true; request->finished = true; } break; } case State::ENDING_SESSION: { if (!asyncCommand.respondsTo(COMMAND_END_SESSION)) return; setState(State::WAITING_8BIT_SWITCH); waitFrames = INIT_WAIT_FRAMES; linkSPI.activate(LinkSPI::Mode::MASTER_256KBPS, LinkSPI::DataSize::SIZE_8BIT); break; } default: { } } } void handleTransferDataResponse(UserRequest* request) { if (request->type != UserRequest::Type::TRANSFER) return abort(Error::Type::WTF); if (asyncCommand.result == CommandResult::SUCCESS) { if (asyncCommand.cmd.header.size == 0) return abort(Error::Type::WEIRD_RESPONSE); u32 size = asyncCommand.cmd.header.size - 1; for (u32 i = 0; i < size; i++) request->receive->data[i] = asyncCommand.cmd.data.bytes[1 + i]; request->receive->data[size] = '\0'; // (just for convenience when using strings, the buffer is big enough) request->receive->size = size; request->receive->success = true; } else { request->receive->success = false; } request->receive->completed = true; request->finished = true; } void processLoosePacket(u32 newData) { switch (state) { case State::PINGING: { setState(State::WAITING_TO_START); waitFrames = INIT_WAIT_FRAMES; break; } default: { } } } void cmdBeginSession() { for (u32 i = 0; i < LOGIN_PARTS_SIZE; i++) addData(LOGIN_PARTS[i], i == 0); sendCommandAsync(buildCommand(COMMAND_BEGIN_SESSION, true)); } void cmdEndSession() { sendCommandAsync(buildCommand(COMMAND_END_SESSION)); } void cmdDialTelephone(const char* phoneNumber) { addData(DIAL_PHONE_FIRST_BYTE[(int)adapterType], true); for (u32 i = 0; i < Link::strlen(phoneNumber); i++) addData(phoneNumber[i]); sendCommandAsync(buildCommand(COMMAND_DIAL_TELEPHONE, true)); } void cmdHangUpTelephone() { sendCommandAsync(buildCommand(COMMAND_HANG_UP_TELEPHONE, true)); } void cmdWaitForTelephoneCall() { sendCommandAsync(buildCommand(COMMAND_WAIT_FOR_TELEPHONE_CALL)); } void cmdTransferData(u8 connectionId, const u8* data, u8 size) { addData(connectionId, true); for (u32 i = 0; i < size; i++) addData(data[i]); sendCommandAsync(buildCommand(COMMAND_TRANSFER_DATA, true)); } void cmdTelephoneStatus() { sendCommandAsync(buildCommand(COMMAND_TELEPHONE_STATUS)); } void cmdSIO32(bool enabled) { addData(enabled, true); sendCommandAsync(buildCommand(COMMAND_SIO32, true)); } void cmdReadConfigurationData(u8 offset, u8 size) { addData(offset, true); addData(CONFIGURATION_DATA_CHUNK); sendCommandAsync(buildCommand(COMMAND_READ_CONFIGURATION_DATA, true)); } void cmdISPLogin(const char* loginId, const char* password) { u32 loginIdLength = Link::strlen(loginId); addData(loginIdLength, true); for (u32 i = 0; i < loginIdLength; i++) addData(loginId[i]); u32 passwordLength = Link::strlen(password); addData(passwordLength); for (u32 i = 0; i < passwordLength; i++) addData(password[i]); bool isConfigured = isConfigurationValid(); for (u32 i = 0; i < 4; i++) addData(isConfigured ? adapterConfiguration.fields.primaryDNS[i] : 0); for (u32 i = 0; i < 4; i++) addData(isConfigured ? adapterConfiguration.fields.secondaryDNS[i] : 0); sendCommandAsync(buildCommand(COMMAND_ISP_LOGIN, true)); } void cmdOpenTCPConnection(const u8* ip, u16 port) { for (u32 i = 0; i < 4; i++) addData(ip[i], i == 0); addData(Link::msB16(port)); addData(Link::lsB16(port)); sendCommandAsync(buildCommand(COMMAND_OPEN_TCP_CONNECTION, true)); } void cmdCloseTCPConnection(u8 connectionId) { addData(connectionId); sendCommandAsync(buildCommand(COMMAND_CLOSE_TCP_CONNECTION, true)); } void cmdOpenUDPConnection(const u8* ip, u16 port) { for (u32 i = 0; i < 4; i++) addData(ip[i], i == 0); addData(Link::msB16(port)); addData(Link::lsB16(port)); sendCommandAsync(buildCommand(COMMAND_OPEN_UDP_CONNECTION, true)); } void cmdCloseUDPConnection(u8 connectionId) { addData(connectionId); sendCommandAsync(buildCommand(COMMAND_CLOSE_UDP_CONNECTION, true)); } void cmdDNSQuery(const u8* data, u8 size) { for (u32 i = 0; i < size; i++) addData(data[i], i == 0); sendCommandAsync(buildCommand(COMMAND_DNS_QUERY, true)); } void setISPNumber() { static const char BCD[16] = "0123456789#*cde"; for (u32 i = 0; i < 8; i++) { u8 b = adapterConfiguration.fields.configurationSlot1[i]; adapterConfiguration.fields._ispNumber1[i * 2] = BCD[b >> 4]; adapterConfiguration.fields._ispNumber1[i * 2 + 1] = BCD[b & 0xF]; } } void pushRequest(UserRequest request) { request.timeout = 0; request.finished = false; userRequests.syncPush(request); } void popRequest() { auto request = userRequests.peekRef(); request->cleanup(); request->finished = true; userRequests.pop(); } bool shouldAbortOnStateTimeout() { return state > State::NEEDS_RESET && state < State::SESSION_ACTIVE; } bool shouldAbortOnRequestTimeout() { return true; } bool shouldAbortOnCommandFailure() { u8 commandId = asyncCommand.relatedCommandId(); return asyncCommand.direction == AsyncCommand::Direction::SENDING || (commandId != COMMAND_WAIT_FOR_TELEPHONE_CALL && commandId != COMMAND_DIAL_TELEPHONE && !isAsyncRequest(commandId)); } bool isAsyncRequest(u8 commandId) { return commandId == COMMAND_DNS_QUERY || commandId == COMMAND_OPEN_TCP_CONNECTION || commandId == COMMAND_CLOSE_TCP_CONNECTION || commandId == COMMAND_OPEN_UDP_CONNECTION || commandId == COMMAND_CLOSE_UDP_CONNECTION || commandId == COMMAND_TRANSFER_DATA; } void addData(u8 value, bool start = false) { if (start) { nextCommandDataSize = 0; nextCommandData = PacketData{}; } nextCommandData.bytes[nextCommandDataSize] = value; nextCommandDataSize++; } void copyString(char* target, const char* source, u32 length) { u32 len = Link::strlen(source); for (u32 i = 0; i < length + 1; i++) if (i < len) target[i] = source[i]; else target[i] = '\0'; } void setState(State newState) { role = Role::NO_P2P_CONNECTION; State oldState = state; state = newState; timeoutStateFrames = 0; pingFrameCount = 0; _LMLOG_("!! new state: %d -> %d", oldState, newState); (void)oldState; } void abort(Error::Type errorType, bool fatal = true) { auto newError = Error{ .type = errorType, .state = state, .cmdId = asyncCommand.relatedCommandId(), .cmdResult = asyncCommand.result, .cmdErrorCode = asyncCommand.errorCode, .cmdIsSending = asyncCommand.direction == AsyncCommand::Direction::SENDING, .reqType = userRequests.isEmpty() ? -1 : (int)userRequests.peek().type}; _LMLOG_( "!! %s:\n error: %d\n cmdId: %s$%X\n cmdResult: %d\n " "cmdErrorCode: %d", fatal ? "aborted" : "failed", newError.type, newError.cmdIsSending ? ">" : "<", newError.cmdId, newError.cmdResult, newError.cmdErrorCode); (void)newError; if (fatal) { error = newError; resetState(); stop(); } } void reset() { resetState(); stop(); start(); } void resetState() { LINK_BARRIER; setState(State::NEEDS_RESET); adapterConfiguration = AdapterConfiguration{}; userRequests.clear(); asyncCommand.reset(); waitFrames = 0; timeoutStateFrames = 0; role = Role::NO_P2P_CONNECTION; nextCommandDataSize = 0; hasPendingTransfer = false; pendingTransfer = 0; adapterType = AdapterType::UNKNOWN; userRequests.syncClear(); LINK_BARRIER; } void stop() { stopTimer(); linkSPI.deactivate(); } void start() { linkSPI.activate(LinkSPI::Mode::MASTER_256KBPS, LinkSPI::DataSize::SIZE_8BIT); setState(State::PINGING); transferAsync(0); } void stopTimer() { Link::_REG_TM[config.timerId].cnt = Link::_REG_TM[config.timerId].cnt & (~Link::_TM_ENABLE); } void startTimer(u16 interval) { Link::_REG_TM[config.timerId].start = -interval; Link::_REG_TM[config.timerId].cnt = Link::_TM_ENABLE | Link::_TM_IRQ | BASE_FREQUENCY; } void sendCommandAsync(Command command) { _LMLOG_(">> $%X [%d] (...)", command.header.commandId, command.header.size); asyncCommand.reset(); asyncCommand.cmd = command; asyncCommand.isActive = true; if (isSIO32Mode()) // Magic+Header advance32( Link::buildU32(command.magicBytes.magic1, command.magicBytes.magic2, command.header.commandId, command.header._unused_)); else // Magic Bytes (1) advance8(command.magicBytes.magic1); } void receiveCommandAsync() { _LMLOG_("<< ..."); asyncCommand.reset(); asyncCommand.direction = AsyncCommand::Direction::RECEIVING; asyncCommand.isActive = true; if (isSIO32Mode()) transferAsync(GBA_WAITING_32BIT); else transferAsync(GBA_WAITING); } void sendAsyncCommandSIO8(u32 newData) { const u8* commandBytes = (const u8*)&asyncCommand.cmd; u32 mainSize = PREAMBLE_SIZE + asyncCommand.cmd.header.size; if (asyncCommand.transferred < mainSize) { // Magic Bytes (2) + Packet Header + Packet Data advance8(commandBytes[asyncCommand.transferred]); } else if (asyncCommand.transferred < mainSize + CHECKSUM_SIZE) { // Packet Checksum commandBytes += PREAMBLE_SIZE + LINK_MOBILE_COMMAND_TRANSFER_BUFFER; advance8(commandBytes[asyncCommand.transferred - mainSize]); } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE) { // Acknowledgement Signal (1) advance8(DEVICE_GBA | OR_VALUE); } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 1) { // Acknowledgement Signal (2) if (!isSupportedAdapter(newData)) return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID); advance8(ACK_SENDER); } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 2) { // Acknowledgement Signal (3) if (newData != (asyncCommand.cmd.header.commandId ^ OR_VALUE)) return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK); asyncCommand.finish(); } } void sendAsyncCommandSIO32(u32 newData) { u32 dataSize = asyncCommand.cmd.header.size; u32 alignment = dataSize % 4; u32 padding = alignment != 0 ? 4 - alignment : 0; u32 mainSize = PREAMBLE_SIZE + dataSize + padding; if (asyncCommand.transferred == 4) { // Header+Data || Header+Checksum advance32(dataSize > 0 ? Link::buildU32(asyncCommand.cmd.header._unusedSizeHigh_, asyncCommand.cmd.header.size, asyncCommand.cmd.data.bytes[0], asyncCommand.cmd.data.bytes[1]) : Link::buildU32(asyncCommand.cmd.header._unusedSizeHigh_, asyncCommand.cmd.header.size, asyncCommand.cmd.checksum.high, asyncCommand.cmd.checksum.low)); } else if (asyncCommand.transferred < mainSize) { // Data || Data+Checksum u32 transferredDataCount = asyncCommand.transferred - PREAMBLE_SIZE; u32 pendingDataCount = (dataSize + padding) - transferredDataCount; advance32(pendingDataCount > 2 ? Link::buildU32( asyncCommand.cmd.data.bytes[transferredDataCount], asyncCommand.cmd.data.bytes[transferredDataCount + 1], asyncCommand.cmd.data.bytes[transferredDataCount + 2], asyncCommand.cmd.data.bytes[transferredDataCount + 3]) : Link::buildU32( asyncCommand.cmd.data.bytes[transferredDataCount], asyncCommand.cmd.data.bytes[transferredDataCount + 1], asyncCommand.cmd.checksum.high, asyncCommand.cmd.checksum.low)); } else if (asyncCommand.transferred < mainSize + 4) { // Acknowledgement Signal (1) advance32(Link::buildU32(DEVICE_GBA | OR_VALUE, ACK_SENDER, 0, 0)); } else { // Acknowledgement Signal (2) u16 ackData = Link::msB32(newData); if (!isSupportedAdapter(Link::msB16(ackData))) return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID); if (Link::lsB16(ackData) != (asyncCommand.cmd.header.commandId ^ OR_VALUE)) return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK); asyncCommand.finish(); } } void receiveAsyncCommandSIO8(u32 newData) { u8* commandBytes = (u8*)&asyncCommand.cmd; u32 mainSize = PREAMBLE_SIZE + asyncCommand.cmd.header.size; if (asyncCommand.transferred == 0) { // Magic Bytes (1) if (newData == ADAPTER_WAITING) return transferAsync(GBA_WAITING); if (newData != COMMAND_MAGIC_VALUE1) return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES); advance8(GBA_WAITING); } else if (asyncCommand.transferred == 1) { // Magic Bytes (1) if (newData != COMMAND_MAGIC_VALUE2) return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES); advance8(GBA_WAITING); } else if (asyncCommand.transferred < PREAMBLE_SIZE) { // Packet Header commandBytes[asyncCommand.transferred] = newData; if (asyncCommand.cmd.header._unusedSizeHigh_ != 0 || asyncCommand.cmd.header.size > LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH) return asyncCommand.fail(CommandResult::WEIRD_DATA_SIZE); advance8(GBA_WAITING); if (asyncCommand.transferred == PREAMBLE_SIZE) asyncCommand.expectedChecksum = asyncCommand.cmd.header.sum(); } else if (asyncCommand.transferred < mainSize) { // Packet Data commandBytes[asyncCommand.transferred] = newData; asyncCommand.expectedChecksum += newData; advance8(GBA_WAITING); } else if (asyncCommand.transferred == mainSize) { // Packet Checksum (1) if (newData != Link::msB16(asyncCommand.expectedChecksum)) return asyncCommand.fail(CommandResult::WRONG_CHECKSUM); advance8(GBA_WAITING); } else if (asyncCommand.transferred == mainSize + 1) { // Packet Checksum (2) if (newData != Link::lsB16(asyncCommand.expectedChecksum)) return asyncCommand.fail(CommandResult::WRONG_CHECKSUM); advance8(DEVICE_GBA | OR_VALUE); } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE) { // Acknowledgement Signal (1) if (!isSupportedAdapter(newData)) return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID); advance8(asyncCommand.cmd.header.commandId ^ OR_VALUE); } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 1) { // Acknowledgement Signal (2) if (newData != ACK_SENDER) return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK); asyncCommand.finish(); } } void receiveAsyncCommandSIO32(u32 newData) { u32 dataSize = asyncCommand.cmd.header.size; u32 alignment = dataSize % 4; u32 padding = alignment != 0 ? 4 - alignment : 0; u32 mainSize = PREAMBLE_SIZE + dataSize + padding; if (asyncCommand.transferred == 0) { // Magic+Header if (newData == ADAPTER_WAITING || newData == ADAPTER_WAITING_32BIT) return transferAsync(GBA_WAITING_32BIT); u16 magic = Link::msB32(newData); u16 firstHalfHeader = Link::lsB32(newData); if (Link::msB16(magic) != COMMAND_MAGIC_VALUE1 || Link::lsB16(magic) != COMMAND_MAGIC_VALUE2) return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES); asyncCommand.cmd.header.commandId = Link::msB16(firstHalfHeader); asyncCommand.cmd.header._unused_ = Link::lsB16(firstHalfHeader); advance32(GBA_WAITING_32BIT); } else if (asyncCommand.transferred == 4) { // Header+Data || Header+Checksum u16 secondHalfHeader = Link::msB32(newData); asyncCommand.cmd.header._unusedSizeHigh_ = Link::msB16(secondHalfHeader); asyncCommand.cmd.header.size = Link::lsB16(secondHalfHeader); if (asyncCommand.cmd.header._unusedSizeHigh_ != 0 || asyncCommand.cmd.header.size > LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH) return asyncCommand.fail(CommandResult::WEIRD_DATA_SIZE); asyncCommand.expectedChecksum = asyncCommand.cmd.header.sum(); if (asyncCommand.cmd.header.size > 0) { u16 firstData = Link::lsB32(newData); u8 b0 = Link::msB16(firstData), b1 = Link::lsB16(firstData); asyncCommand.cmd.data.bytes[0] = b0; asyncCommand.cmd.data.bytes[1] = b1; asyncCommand.expectedChecksum += b0 + b1; } else { u16 checksum = Link::lsB32(newData); if (checksum != asyncCommand.expectedChecksum) return asyncCommand.fail(CommandResult::WRONG_CHECKSUM); asyncCommand.cmd.checksum.high = Link::msB16(checksum); asyncCommand.cmd.checksum.low = Link::lsB16(checksum); } advance32(GBA_WAITING_32BIT); } else if (asyncCommand.transferred < mainSize) { // Data || Data+Checksum u32 transferredDataCount = asyncCommand.transferred - PREAMBLE_SIZE; u32 pendingDataCount = (dataSize + padding) - transferredDataCount; if (pendingDataCount > 2) { u16 dataHigh = Link::msB32(newData); u16 dataLow = Link::lsB32(newData); u8 b0 = Link::msB16(dataHigh), b1 = Link::lsB16(dataHigh), b2 = Link::msB16(dataLow), b3 = Link::lsB16(dataLow); asyncCommand.cmd.data.bytes[transferredDataCount] = b0; asyncCommand.cmd.data.bytes[transferredDataCount + 1] = b1; asyncCommand.cmd.data.bytes[transferredDataCount + 2] = b2; asyncCommand.cmd.data.bytes[transferredDataCount + 3] = b3; asyncCommand.expectedChecksum += b0 + b1 + b2 + b3; advance32(GBA_WAITING_32BIT); } else { u16 lastData = Link::msB32(newData); u8 b0 = Link::msB16(lastData), b1 = Link::lsB16(lastData); asyncCommand.cmd.data.bytes[transferredDataCount] = b0; asyncCommand.cmd.data.bytes[transferredDataCount + 1] = b1; asyncCommand.expectedChecksum += b0 + b1; u16 checksum = Link::lsB32(newData); if (checksum != asyncCommand.expectedChecksum) return asyncCommand.fail(CommandResult::WRONG_CHECKSUM); asyncCommand.cmd.checksum.high = Link::msB16(checksum); asyncCommand.cmd.checksum.low = Link::lsB16(checksum); advance32(Link::buildU32(DEVICE_GBA | OR_VALUE, asyncCommand.cmd.header.commandId ^ OR_VALUE, 0, 0)); } } else { // Acknowledgement Signal u32 ackData = Link::msB32(newData); if (!isSupportedAdapter(Link::msB16(ackData)) || Link::lsB16(ackData) != ACK_SENDER) return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID); asyncCommand.finish(); } } bool isSupportedAdapter(u8 ack) { for (u32 i = 0; i < SUPPORTED_DEVICES_SIZE; i++) { if ((SUPPORTED_DEVICES[i] | OR_VALUE) == ack) { if (adapterType == AdapterType::UNKNOWN) adapterType = static_cast(i); return true; } } return false; } Command buildCommand(u8 type, bool withData = false) { Command command; command.header.commandId = type; command.header._unused_ = 0; command.header._unusedSizeHigh_ = 0; command.header.size = withData ? (u8)nextCommandDataSize : 0; if (withData) command.data = nextCommandData; u16 checksum = command.header.sum(); for (u32 i = 0; i < command.header.size; i++) checksum += command.data.bytes[i]; command.checksum.high = Link::msB16(checksum); command.checksum.low = Link::lsB16(checksum); return command; } void advance8(u32 data) { transferAsync(data); asyncCommand.transferred++; } void advance32(u32 data) { transferAsync(data); asyncCommand.transferred += 4; } void transferAsync(u32 data) { hasPendingTransfer = true; pendingTransfer = data; startTimer(WAIT_TICKS[isSIO32Mode()]); } bool isSIO32Mode() { return linkSPI.getDataSize() == LinkSPI::DataSize::SIZE_32BIT; } bool isBitHigh(u8 byte, u8 bit) { return (byte >> bit) & 1; } }; extern LinkMobile* linkMobile; /** * @brief VBLANK interrupt handler. */ inline void LINK_MOBILE_ISR_VBLANK() { linkMobile->_onVBlank(); } /** * @brief SERIAL interrupt handler. */ inline void LINK_MOBILE_ISR_SERIAL() { linkMobile->_onSerial(); } /** * @brief TIMER interrupt handler. */ inline void LINK_MOBILE_ISR_TIMER() { linkMobile->_onTimer(); } #undef _LMLOG_ #endif // LINK_MOBILE_H