gba-link-connection/lib/LinkMobile.hpp
2025-03-18 20:14:35 -03:00

1921 lines
62 KiB
C++

#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.2";
#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<UserRequest, LINK_MOBILE_QUEUE_SIZE>;
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 ? ">!"
: "<!",
asyncCommand.cmd.header.pureCommandId(),
asyncCommand.cmd.header.size);
if (asyncCommand.direction == AsyncCommand::Direction::SENDING) {
receiveCommandAsync();
return;
}
if (asyncCommand.respondsTo(COMMAND_TELEPHONE_STATUS)) {
if (asyncCommand.cmd.header.size != 3)
return abort(Error::Type::WEIRD_RESPONSE);
if (state == State::CALL_ESTABLISHED) {
if (!isBitHigh(asyncCommand.cmd.data.bytes[0], 2)) {
// (call terminated)
setState(State::SESSION_ACTIVE);
}
}
return;
}
switch (state) {
case State::STARTING_SESSION: {
if (!asyncCommand.respondsTo(COMMAND_BEGIN_SESSION))
return;
if (asyncCommand.cmd.header.size != LOGIN_PARTS_SIZE)
return abort(Error::Type::WEIRD_RESPONSE);
for (u32 i = 0; i < LOGIN_PARTS_SIZE; i++) {
if (asyncCommand.cmd.data.bytes[i] != LOGIN_PARTS[i])
return abort(Error::Type::WEIRD_RESPONSE);
}
setState(State::ACTIVATING_SIO32);
cmdSIO32(true);
break;
}
case State::ACTIVATING_SIO32: {
if (asyncCommand.respondsTo(COMMAND_RESET)) {
// If the adapter responds to a 0x16 instead of 0x18,
// it's libmobile telling us that SIO32 is not supported.
// In that case, we continue using SIO8.
setState(State::READING_CONFIGURATION);
cmdReadConfigurationData(0, CONFIGURATION_DATA_CHUNK);
return;
}
if (!asyncCommand.respondsTo(COMMAND_SIO32))
return;
setState(State::WAITING_32BIT_SWITCH);
waitFrames = INIT_WAIT_FRAMES;
linkSPI.activate(LinkSPI::Mode::MASTER_256KBPS,
LinkSPI::DataSize::SIZE_32BIT);
break;
}
case State::READING_CONFIGURATION: {
if (!asyncCommand.respondsTo(COMMAND_READ_CONFIGURATION_DATA))
return;
u32 offset = asyncCommand.cmd.data.bytes[0];
u32 sizeWithOffsetByte = asyncCommand.cmd.header.size;
if (asyncCommand.result != CommandResult::SUCCESS ||
sizeWithOffsetByte != CONFIGURATION_DATA_CHUNK + 1 ||
(offset != 0 && offset != CONFIGURATION_DATA_CHUNK))
return abort(Error::Type::WEIRD_RESPONSE);
for (u32 i = 0; i < CONFIGURATION_DATA_CHUNK; i++)
adapterConfiguration.bytes[offset + i] =
asyncCommand.cmd.data.bytes[1 + i];
if (offset == 0) {
cmdReadConfigurationData(CONFIGURATION_DATA_CHUNK,
CONFIGURATION_DATA_CHUNK);
} else {
setISPNumber();
setState(State::SESSION_ACTIVE);
}
break;
}
case State::SESSION_ACTIVE: {
if (!asyncCommand.respondsTo(COMMAND_WAIT_FOR_TELEPHONE_CALL))
return;
if (asyncCommand.result == CommandResult::SUCCESS) {
setState(State::CALL_ESTABLISHED);
role = Role::RECEIVER;
} else {
// (no call received)
}
break;
}
case State::CALLING: {
if (!asyncCommand.respondsTo(COMMAND_DIAL_TELEPHONE))
return;
if (asyncCommand.result == CommandResult::SUCCESS) {
setState(State::CALL_ESTABLISHED);
role = Role::CALLER;
} else {
// (call failed)
setState(State::SESSION_ACTIVE);
}
break;
}
case State::CALL_ESTABLISHED: {
if (asyncCommand.respondsTo(COMMAND_HANG_UP_TELEPHONE)) {
setState(State::SESSION_ACTIVE);
return;
}
if (!asyncCommand.respondsTo(COMMAND_TRANSFER_DATA))
return;
if (userRequests.isEmpty())
return abort(Error::Type::WTF);
auto request = userRequests.peekRef();
handleTransferDataResponse(request);
break;
}
case State::ISP_CALLING: {
if (!asyncCommand.respondsTo(COMMAND_DIAL_TELEPHONE))
return;
if (userRequests.isEmpty())
return abort(Error::Type::WTF);
auto request = userRequests.peekRef();
if (request->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<AdapterType>(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