gba-link-connection/lib/LinkRawWireless.hpp
2025-04-19 08:22:33 -03:00

1775 lines
52 KiB
C++

#ifndef LINK_RAW_WIRELESS_H
#define LINK_RAW_WIRELESS_H
// --------------------------------------------------------------------------
// A low level driver for the GBA Wireless Adapter.
// --------------------------------------------------------------------------
// Usage:
// - There's one method for every supported Wireless Adapter command:
// - `setup` = `0x17`
// - `getSystemStatus` = `0x13`
// - `broadcast` = `0x16`
// - `startHost` = `0x19`
// - `getSignalLevel` = `0x11`
// - `getSlotStatus` = `0x14`
// - `pollConnections` = `0x1A`
// - `endHost` = `0x1B`
// - `broadcastReadStart` = `0x1C`
// - `broadcastReadPoll` = `0x1D`
// - `broadcastReadEnd` = `0x1E`
// - `connect` = `0x1F`
// - `keepConnecting` = `0x20`
// - `finishConnection` = `0x21`
// - `sendData` = `0x24`
// - `sendDataAndWait` = `0x25`
// - `receiveData` = `0x26`
// - `wait` = `0x27`
// - `disconnectClient` = `0x30`
// - `bye` = `0x3D`
// - Use `sendCommand(...)` to send arbitrary commands.
// - Use `sendCommandAsync(...)` to send arbitrary commands asynchronously.
// - This requires setting `LINK_RAW_WIRELESS_ISR_SERIAL` as the `SERIAL`
// interrupt handler.
// - After calling this method, call `getAsyncState()` and
// `getAsyncCommandResult()`.
// - Do not call any other methods until the async state is `IDLE` again, or
// the adapter will desync!
// - When sending arbitrary commands, the responses are not parsed. The
// exceptions are SendData and ReceiveData, which have these helpers:
// - `getSendDataHeaderFor(...)`
// - `getReceiveDataResponse(...)`
// --------------------------------------------------------------------------
// considerations:
// - advanced usage only; if you're building a game, use `LinkWireless`!
// --------------------------------------------------------------------------
#ifndef LINK_DEVELOPMENT
#pragma GCC system_header
#endif
#include "_link_common.hpp"
#include "LinkGPIO.hpp"
#include "LinkSPI.hpp"
#ifndef LINK_RAW_WIRELESS_ENABLE_LOGGING
/**
* @brief Enable logging.
* \warning Set `linkRawWireless->logger` and uncomment to enable!
* \warning This option #include`s std::string!
*/
// #define LINK_RAW_WIRELESS_ENABLE_LOGGING
#endif
LINK_VERSION_TAG LINK_RAW_WIRELESS_VERSION = "vLinkRawWireless/v8.0.3";
#define LINK_RAW_WIRELESS_MAX_PLAYERS 5
#define LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH 30
#define LINK_RAW_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH 4
#define LINK_RAW_WIRELESS_MAX_GAME_ID 0x7FFF
#define LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH 14
#define LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH 8
#define LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 23
#define LINK_RAW_WIRELESS_BROADCAST_LENGTH 6
#define LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH \
(1 + LINK_RAW_WIRELESS_BROADCAST_LENGTH)
#define LINK_RAW_WIRELESS_MAX_SERVERS \
(LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH / \
LINK_RAW_WIRELESS_BROADCAST_LENGTH)
#ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING
#include <string>
#define _LRWLOG_(str) logger(str)
#else
#define _LRWLOG_(str)
#endif
/**
* @brief A low level driver for the GBA Wireless Adapter.
*/
class LinkRawWireless {
private:
using u32 = Link::u32;
using u16 = Link::u16;
using u8 = Link::u8;
using vu8 = Link::vu8;
public:
static constexpr int PING_WAIT = 50;
static constexpr int TRANSFER_WAIT = 15;
static constexpr int MICRO_WAIT = 2;
#ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING
static constexpr int CMD_TIMEOUT = 228;
#else
static constexpr int CMD_TIMEOUT = 15;
#endif
static constexpr int MAX_TRANSFER_BYTES_SERVER = 87;
static constexpr int MAX_TRANSFER_BYTES_CLIENT = 16;
static constexpr int LOGIN_STEPS = 10;
static constexpr int LOGIN_JUNK_STEPS = 2;
static constexpr int COMMAND_HEADER_VALUE = 0x9966;
static constexpr int RESPONSE_ACK = 0x80;
static constexpr u32 DATA_REQUEST_VALUE = 0x80000000;
static constexpr int SETUP_MAGIC = 0x003c0000;
static constexpr int WAIT_STILL_CONNECTING = 0x01000000;
static constexpr int COMMAND_HELLO = 0x10;
static constexpr int COMMAND_SETUP = 0x17;
static constexpr int COMMAND_SYSTEM_STATUS = 0x13;
static constexpr int COMMAND_BROADCAST = 0x16;
static constexpr int COMMAND_START_HOST = 0x19;
static constexpr int COMMAND_SIGNAL_LEVEL = 0x11;
static constexpr int COMMAND_SLOT_STATUS = 0x14;
static constexpr int COMMAND_POLL_CONNECTIONS = 0x1A;
static constexpr int COMMAND_END_HOST = 0x1B;
static constexpr int COMMAND_BROADCAST_READ_START = 0x1C;
static constexpr int COMMAND_BROADCAST_READ_POLL = 0x1D;
static constexpr int COMMAND_BROADCAST_READ_END = 0x1E;
static constexpr int COMMAND_CONNECT = 0x1F;
static constexpr int COMMAND_IS_CONNECTION_COMPLETE = 0x20;
static constexpr int COMMAND_FINISH_CONNECTION = 0x21;
static constexpr int COMMAND_SEND_DATA = 0x24;
static constexpr int COMMAND_SEND_DATA_AND_WAIT = 0x25;
static constexpr int COMMAND_RECEIVE_DATA = 0x26;
static constexpr int COMMAND_WAIT = 0x27;
static constexpr int COMMAND_DISCONNECT_CLIENT = 0x30;
static constexpr int COMMAND_BYE = 0x3d;
static constexpr int EVENT_WAIT_TIMEOUT = 0x27;
static constexpr int EVENT_DATA_AVAILABLE = 0x28;
static constexpr int EVENT_DISCONNECTED = 0x29;
static constexpr u16 LOGIN_PARTS[] = {0x494E, 0x494E, 0x494E, 0x544E, 0x544E,
0x4E45, 0x4E45, 0x4F44, 0x4F44, 0x8001};
#ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING
typedef void (*Logger)(std::string);
Logger logger = [](std::string str) {};
#endif
enum class State {
NEEDS_RESET = 0,
AUTHENTICATED = 1,
SEARCHING = 2,
SERVING = 3,
CONNECTING = 4,
CONNECTED = 5
};
struct CommandResult {
bool success = false;
u8 commandId = 0;
u32 data[LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH];
u32 dataSize = 0;
};
struct Server {
u16 id = 0;
u16 gameId;
char gameName[LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH + 1];
char userName[LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH + 1];
u8 nextClientNumber;
bool isFull() { return nextClientNumber == 0xFF; }
};
struct ConnectedClient {
u16 deviceId = 0;
u8 clientNumber = 0;
};
struct SystemStatusResponse {
u16 deviceId = 0;
u8 currentPlayerId = 0;
State adapterState = State::AUTHENTICATED;
bool isServerClosed = false;
};
struct SignalLevelResponse {
u8 signalLevels[LINK_RAW_WIRELESS_MAX_PLAYERS] = {};
};
struct SlotStatusResponse {
u8 nextClientNumber = 0;
ConnectedClient connectedClients[LINK_RAW_WIRELESS_MAX_PLAYERS] = {};
u32 connectedClientsSize = 0;
};
struct PollConnectionsResponse {
ConnectedClient connectedClients[LINK_RAW_WIRELESS_MAX_PLAYERS] = {};
u32 connectedClientsSize = 0;
};
struct BroadcastReadPollResponse {
Server servers[LINK_RAW_WIRELESS_MAX_SERVERS] = {};
u32 serversSize = 0;
};
enum class ConnectionPhase { STILL_CONNECTING, ERROR, SUCCESS };
struct ConnectionStatus {
ConnectionPhase phase = ConnectionPhase::STILL_CONNECTING;
u8 assignedClientNumber = 0;
};
struct ReceiveDataResponse {
u32 sentBytes[LINK_RAW_WIRELESS_MAX_PLAYERS];
u32 data[LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH];
u32 dataSize = 0;
};
enum class AsyncState { IDLE, WORKING, READY };
/**
* @brief Returns whether the library is active or not.
*/
[[nodiscard]] bool isActive() { return isEnabled; }
/**
* @brief Activates the library.
* Returns whether initialization was successful or not.
*/
bool activate(bool _stopFirst = true) {
LINK_READ_TAG(LINK_RAW_WIRELESS_VERSION);
LINK_BARRIER;
isEnabled = false;
LINK_BARRIER;
bool success = reset(_stopFirst);
LINK_BARRIER;
isEnabled = true;
LINK_BARRIER;
return success;
}
/**
* @brief Restores the state from an existing connection on the Wireless
* Adapter hardware. This is useful, for example, after a fresh launch of a
* Multiboot game, to synchronize the library with the current state and
* avoid a reconnection. Returns whether the restoration was successful.
* On success, the state should be either `SERVING` or `CONNECTED`.
* \warning This should be used as a replacement for `activate()`.
*/
bool restoreExistingConnection() {
LINK_BARRIER;
isEnabled = false;
LINK_BARRIER;
_resetState();
LINK_BARRIER;
isEnabled = true;
LINK_BARRIER;
_LRWLOG_("setting SPI to 2Mbps");
linkSPI.activate(LinkSPI::Mode::MASTER_2MBPS);
_LRWLOG_("analyzing system status");
SystemStatusResponse systemStatus;
if (!getSystemStatus(systemStatus)) {
deactivate();
return false;
}
if (systemStatus.adapterState == State::SERVING) {
_LRWLOG_("restoring SERVING state");
SlotStatusResponse slotStatus;
if (!getSlotStatus(slotStatus)) {
deactivate();
return false;
}
state = State::SERVING;
sessionState.isServerClosed = systemStatus.isServerClosed;
} else if (systemStatus.adapterState == State::CONNECTED) {
_LRWLOG_("restoring CONNECTED state");
state = State::CONNECTED;
} else {
_LRWLOG_("! invalid adapter state");
deactivate();
return false;
}
LINK_BARRIER;
sessionState.currentPlayerId = systemStatus.currentPlayerId;
LINK_BARRIER;
_LRWLOG_("restored ok!");
return true;
}
/**
* @brief Deactivates the library.
*/
void deactivate() {
isEnabled = false;
_resetState();
stop();
}
/**
* @brief Calls the Setup (`0x17`) command.
* @param maxPlayers `(2~5)` Maximum players in hosted rooms. Clients should
* set this to `0`.
* @param maxTransmissions Number of transmissions before marking a player as
* disconnected. `0` means infinite retransmissions.
* @param waitTimeout Timeout of the *waiting commands*, in frames (16.6ms).
* `0` means no timeout.
* @param magic A part of the protocol that hasn't been reverse-engineered
* yet. For now, it's magic (`0x003C0000`).
*/
bool setup(u8 maxPlayers = LINK_RAW_WIRELESS_MAX_PLAYERS,
u8 maxTransmissions = 4,
u8 waitTimeout = 32,
u32 magic = SETUP_MAGIC) {
if (!isEnabled)
return false;
u32 config =
(u32)(magic |
(((LINK_RAW_WIRELESS_MAX_PLAYERS - maxPlayers) & 0b11) << 16) |
(maxTransmissions << 8) | waitTimeout);
u32 params[1] = {config};
return sendCommand(COMMAND_SETUP, params, 1).success;
}
/**
* @brief Calls the SystemStatus (`0x13`) command.
* @param response A structure that will be filled with the response data.
*/
bool getSystemStatus(SystemStatusResponse& response) {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_SYSTEM_STATUS);
if (!result.success || result.dataSize == 0) {
if (result.dataSize == 0)
_LRWLOG_("! empty response");
_resetState();
return false;
}
u32 status = result.data[0];
response.deviceId = Link::lsB32(status);
u8 slot = Link::lsB16(Link::msB32(status)) & 0b1111;
LINK_BARRIER;
response.currentPlayerId = slot == 0b0001 ? 1
: slot == 0b0010 ? 2
: slot == 0b0100 ? 3
: slot == 0b1000 ? 4
: 0;
LINK_BARRIER;
u8 adapterState = Link::msB16(Link::msB32(status));
response.isServerClosed = false;
switch (adapterState) {
case 1: {
response.adapterState = State::SERVING;
response.isServerClosed = true;
break;
}
case 2: {
response.adapterState = State::SERVING;
break;
}
case 3: {
response.adapterState = State::SEARCHING;
break;
}
case 4: {
response.adapterState = State::CONNECTING;
break;
}
case 5: {
response.adapterState = State::CONNECTED;
break;
}
default: {
response.adapterState = State::AUTHENTICATED;
break;
}
}
return true;
}
/**
* @brief Calls the Broadcast (`0x16`) command.
* @param gameName Game name. Maximum `14` characters + null terminator.
* @param userName User name. Maximum `8` characters + null terminator.
* @param gameId `(0 ~ 0x7FFF)` Game ID.
*/
bool broadcast(const char* gameName = "",
const char* userName = "",
u16 gameId = LINK_RAW_WIRELESS_MAX_GAME_ID,
bool _validateNames = true) {
if (!isEnabled)
return false;
if (_validateNames &&
Link::strlen(gameName) > LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH) {
_LRWLOG_("! game name too long");
return false;
}
if (_validateNames &&
Link::strlen(userName) > LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH) {
_LRWLOG_("! user name too long");
return false;
}
char finalGameName[LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH + 1];
char finalUserName[LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH + 1];
copyName(finalGameName, gameName, LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH);
copyName(finalUserName, userName, LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH);
u32 params[LINK_RAW_WIRELESS_BROADCAST_LENGTH] = {
Link::buildU32(Link::buildU16(finalGameName[1], finalGameName[0]),
gameId),
Link::buildU32(Link::buildU16(finalGameName[5], finalGameName[4]),
Link::buildU16(finalGameName[3], finalGameName[2])),
Link::buildU32(Link::buildU16(finalGameName[9], finalGameName[8]),
Link::buildU16(finalGameName[7], finalGameName[6])),
Link::buildU32(Link::buildU16(finalGameName[13], finalGameName[12]),
Link::buildU16(finalGameName[11], finalGameName[10])),
Link::buildU32(Link::buildU16(finalUserName[3], finalUserName[2]),
Link::buildU16(finalUserName[1], finalUserName[0])),
Link::buildU32(Link::buildU16(finalUserName[7], finalUserName[6]),
Link::buildU16(finalUserName[5], finalUserName[4]))};
bool success = sendCommand(COMMAND_BROADCAST, params,
LINK_RAW_WIRELESS_BROADCAST_LENGTH)
.success;
if (!success) {
_resetState();
return false;
}
return true;
}
/**
* @brief Calls the StartHost (`0x19`) command.
* @param wait Whether the function should wait the recommended time or not.
*/
bool startHost(bool wait = true) {
if (!isEnabled)
return false;
bool success = sendCommand(COMMAND_START_HOST).success;
if (!success) {
_resetState();
return false;
}
if (wait)
Link::wait(TRANSFER_WAIT);
_LRWLOG_("state = SERVING");
state = State::SERVING;
_LRWLOG_("server OPEN");
sessionState.isServerClosed = false;
return true;
}
/**
* @brief Calls the SignalLevel (`0x11`) command.
* @param response A structure that will be filled with the response data.
*/
bool getSignalLevel(SignalLevelResponse& response) {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_SIGNAL_LEVEL);
if (!result.success || result.dataSize == 0) {
if (result.dataSize == 0)
_LRWLOG_("! empty response");
_resetState();
return false;
}
u32 levels = result.data[0];
for (u32 i = 1; i < LINK_RAW_WIRELESS_MAX_PLAYERS; i++)
response.signalLevels[i] = (levels >> ((i - 1) * 8)) & 0xFF;
return true;
}
/**
* @brief Calls the SlotStatus (`0x14`) command.
* @param response A structure that will be filled with the response data.
*/
bool getSlotStatus(SlotStatusResponse& response) {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_SLOT_STATUS);
if (!result.success) {
_resetState();
return false;
}
response.connectedClientsSize = 0;
for (u32 i = 0; i < result.dataSize; i++) {
if (i == 0) {
response.nextClientNumber = (u8)Link::lsB32(result.data[i]);
} else {
response.connectedClients[response.connectedClientsSize++] =
ConnectedClient{.deviceId = Link::lsB32(result.data[i]),
.clientNumber = (u8)Link::msB32(result.data[i])};
}
}
LINK_BARRIER;
u8 oldPlayerCount = sessionState.playerCount;
sessionState.playerCount = 1 + response.connectedClientsSize;
if (sessionState.playerCount != oldPlayerCount)
_LRWLOG_("now: " + std::to_string(sessionState.playerCount) + " players");
LINK_BARRIER;
return true;
}
/**
* @brief Calls the PollConnections (`0x1A`) command.
* @param response A structure that will be filled with the response data.
*/
bool pollConnections(PollConnectionsResponse& response) {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_POLL_CONNECTIONS);
if (!result.success) {
_resetState();
return false;
}
response.connectedClientsSize = 0;
for (u32 i = 0; i < result.dataSize; i++) {
response.connectedClients[response.connectedClientsSize++] =
ConnectedClient{.deviceId = Link::lsB32(result.data[i]),
.clientNumber = (u8)Link::msB32(result.data[i])};
}
LINK_BARRIER;
u8 oldPlayerCount = sessionState.playerCount;
sessionState.playerCount = 1 + result.dataSize;
if (sessionState.playerCount != oldPlayerCount)
_LRWLOG_("now: " + std::to_string(sessionState.playerCount) + " players");
LINK_BARRIER;
return true;
}
/**
* @brief Calls the EndHost (`0x1B`) command.
* @param response A structure that will be filled with the response data.
*/
bool endHost(PollConnectionsResponse& response) {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_END_HOST);
if (!result.success) {
_resetState();
return false;
}
response.connectedClientsSize = 0;
for (u32 i = 0; i < result.dataSize; i++) {
response.connectedClients[response.connectedClientsSize++] =
ConnectedClient{.deviceId = Link::lsB32(result.data[i]),
.clientNumber = (u8)Link::msB32(result.data[i])};
}
LINK_BARRIER;
u8 oldPlayerCount = sessionState.playerCount;
sessionState.playerCount = 1 + result.dataSize;
if (sessionState.playerCount != oldPlayerCount)
_LRWLOG_("now: " + std::to_string(sessionState.playerCount) + " players");
LINK_BARRIER;
_LRWLOG_("server CLOSED");
sessionState.isServerClosed = true;
return true;
}
/**
* @brief Calls the BroadcastReadStart (`0x1C`) command.
*/
bool broadcastReadStart() {
if (!isEnabled)
return false;
bool success = sendCommand(COMMAND_BROADCAST_READ_START).success;
if (!success) {
_resetState();
return false;
}
_LRWLOG_("state = SEARCHING");
state = State::SEARCHING;
return true;
}
/**
* @brief Calls the BroadcastReadPoll (`0x1D`) command.
* @param response A structure that will be filled with the response data.
*/
bool broadcastReadPoll(BroadcastReadPollResponse& response) {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_BROADCAST_READ_POLL);
bool success =
result.success &&
result.dataSize % LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH == 0;
if (!success) {
_resetState();
return false;
}
u32 totalBroadcasts =
result.dataSize / LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH;
response.serversSize = 0;
for (u32 i = 0; i < totalBroadcasts; i++) {
u32 start = LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH * i;
Server server;
server.id = (u16)result.data[start];
server.gameId = result.data[start + 1] & LINK_RAW_WIRELESS_MAX_GAME_ID;
u32 gameI = 0, userI = 0;
recoverName(server.gameName, gameI, result.data[start + 1], false);
recoverName(server.gameName, gameI, result.data[start + 2]);
recoverName(server.gameName, gameI, result.data[start + 3]);
recoverName(server.gameName, gameI, result.data[start + 4]);
recoverName(server.userName, userI, result.data[start + 5]);
recoverName(server.userName, userI, result.data[start + 6]);
server.gameName[gameI] = '\0';
server.userName[userI] = '\0';
server.nextClientNumber = (result.data[start] >> 16) & 0xFF;
response.servers[response.serversSize++] = server;
}
return true;
}
/**
* @brief Calls the BroadcastReadEnd (`0x1E`) command.
*/
bool broadcastReadEnd() {
if (!isEnabled)
return false;
bool success = sendCommand(COMMAND_BROADCAST_READ_END).success;
if (!success) {
_resetState();
return false;
}
_LRWLOG_("state = AUTHENTICATED");
state = State::AUTHENTICATED;
return true;
}
/**
* @brief Calls the Connect (`0x1F`) command.
* @param serverId Device ID of the server.
*/
bool connect(u16 serverId) {
if (!isEnabled)
return false;
u32 params[1] = {serverId};
bool success = sendCommand(COMMAND_CONNECT, params, 1).success;
if (!success) {
_resetState();
return false;
}
_LRWLOG_("state = CONNECTING");
state = State::CONNECTING;
return true;
}
/**
* @brief Calls the IsConnectionComplete (`0x20`) command.
* @param response A structure that will be filled with the response data.
*/
bool keepConnecting(ConnectionStatus& response) {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_IS_CONNECTION_COMPLETE);
if (!result.success || result.dataSize == 0) {
if (result.dataSize == 0)
_LRWLOG_("! empty response");
_resetState();
return false;
}
if (result.data[0] == WAIT_STILL_CONNECTING) {
response.phase = ConnectionPhase::STILL_CONNECTING;
return true;
}
u8 assignedPlayerId = 1 + (u8)Link::msB32(result.data[0]);
if (assignedPlayerId >= LINK_RAW_WIRELESS_MAX_PLAYERS) {
_LRWLOG_("! connection failed (1)");
_resetState();
response.phase = ConnectionPhase::ERROR;
return false;
}
response.phase = ConnectionPhase::SUCCESS;
response.assignedClientNumber = (u8)Link::msB32(result.data[0]);
return true;
}
/**
* @brief Calls the FinishConnection (`0x21`) command.
*/
bool finishConnection() {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_FINISH_CONNECTION);
if (!result.success || result.dataSize == 0) {
if (result.dataSize == 0)
_LRWLOG_("! empty response");
_resetState();
return false;
}
u16 status = Link::msB32(result.data[0]);
if ((Link::msB16(status) & 1) == 1) {
_LRWLOG_("! connection failed (2)");
_resetState();
return false;
}
LINK_BARRIER;
u8 assignedPlayerId = 1 + (u8)status;
sessionState.currentPlayerId = assignedPlayerId;
LINK_BARRIER;
_LRWLOG_("state = CONNECTED");
state = State::CONNECTED;
return true;
}
/**
* @brief Calls the SendData (`0x24`) command.
* @param data The values to be sent.
* @param dataSize The number of 32-bit values in the `data` array.
* @param _bytes The number of BYTES to send. If `0`, the method will use
* `dataSize * 4` instead.
*/
bool sendData(const u32* data, u32 dataSize, u32 _bytes = 0) {
if (!isEnabled)
return false;
u32 bytes = _bytes == 0 ? dataSize * 4 : _bytes;
u32 header = getSendDataHeaderFor(bytes);
_LRWLOG_("using header " + toHex(header));
u32 rawData[LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH];
rawData[0] = header;
for (u32 i = 0; i < dataSize; i++)
rawData[i + 1] = data[i];
bool success =
sendCommand(COMMAND_SEND_DATA, rawData, 1 + dataSize).success;
if (!success) {
_resetState();
return false;
}
return true;
}
/**
* @brief Calls the SendDataAndWait (`0x25`) command.
* @param data The values to be sent.
* @param dataSize The number of 32-bit values in the `data` array.
* @param remoteCommand A structure that will be filled with the remote
* command from the adapter.
* @param _bytes The number of BYTES to send. If `0`, the method will use
* `dataSize * 4` instead.
*/
bool sendDataAndWait(const u32* data,
u32 dataSize,
CommandResult& remoteCommand,
u32 _bytes = 0) {
if (!isEnabled)
return false;
u32 bytes = _bytes == 0 ? dataSize * 4 : _bytes;
u32 header = getSendDataHeaderFor(bytes);
_LRWLOG_("using header " + toHex(header));
u32 rawData[LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH];
rawData[0] = header;
for (u32 i = 0; i < dataSize; i++)
rawData[i + 1] = data[i];
if (!sendCommand(COMMAND_SEND_DATA_AND_WAIT, rawData, 1 + dataSize, true)
.success) {
_resetState();
return false;
}
remoteCommand = receiveCommandFromAdapter();
return remoteCommand.success;
}
/**
* @brief Calls the ReceiveData (`0x26`) command.
* @param response A structure that will be filled with the response data.
*/
bool receiveData(ReceiveDataResponse& response) {
if (!isEnabled)
return false;
auto result = sendCommand(COMMAND_RECEIVE_DATA);
if (!result.success) {
_resetState();
return false;
}
return getReceiveDataResponse(result, response);
}
/**
* @brief Calls the Wait (`0x27`) command.
* @param remoteCommand A structure that will be filled with the remote
* command from the adapter.
*/
bool wait(CommandResult& remoteCommand) {
if (!isEnabled)
return false;
if (!sendCommand(COMMAND_WAIT, {}, 0, true).success) {
_resetState();
return false;
}
remoteCommand = receiveCommandFromAdapter();
return remoteCommand.success;
}
/**
* @brief Calls the DisconnectClient (`0x30`) command.
*/
bool disconnectClient(bool client0,
bool client1,
bool client2,
bool client3) {
if (!isEnabled)
return false;
u32 bitfield =
(client0 << 0) | (client1 << 1) | (client2 << 2) | (client3 << 3);
return sendCommand(COMMAND_DISCONNECT_CLIENT, &bitfield, 1).success;
}
/**
* @brief Calls the Bye (`0x3D`) command.
*/
bool bye() {
if (!isEnabled)
return false;
return sendCommand(COMMAND_BYE).success;
}
/**
* @brief Returns the header for the commands 0x24 and 0x25.
* @param bytes The number of bytes of the command.
*/
u32 getSendDataHeaderFor(u32 bytes) {
return sessionState.currentPlayerId == 0
? bytes
: (bytes << (3 + sessionState.currentPlayerId * 5));
}
/**
* @brief Returns the parsed response of a 0x26 command.
* @param result The raw response returned by the command call.
* @param response A structure that will be filled with the response data.
*/
bool getReceiveDataResponse(CommandResult& result,
ReceiveDataResponse& response) {
for (u32 i = 0; i < LINK_RAW_WIRELESS_MAX_PLAYERS; i++)
response.sentBytes[i] = 0;
response.dataSize = result.dataSize > 0 ? result.dataSize - 1 : 0;
if (result.dataSize == 0)
return true;
for (u32 i = 1; i < result.dataSize; i++)
response.data[i - 1] = result.data[i];
u32 header = result.data[0];
response.sentBytes[0] =
Link::_min(header & 0b1111111, MAX_TRANSFER_BYTES_SERVER);
response.sentBytes[1] =
Link::_min((header >> 8) & 0b11111, MAX_TRANSFER_BYTES_CLIENT);
response.sentBytes[2] =
Link::_min((header >> 13) & 0b11111, MAX_TRANSFER_BYTES_CLIENT);
response.sentBytes[3] =
Link::_min((header >> 18) & 0b11111, MAX_TRANSFER_BYTES_CLIENT);
response.sentBytes[4] =
Link::_min((header >> 23) & 0b11111, MAX_TRANSFER_BYTES_CLIENT);
return true;
}
/**
* @brief Calls an arbitrary command and returns the response.
* @param type The ID of the command.
* @param params The command parameters.
* @param length The number of 32-bit values in the `params` array.
* @param invertsClock Whether this command inverts the clock or not (Wait).
* \warning If it `invertsClock`, call `receiveCommandFromAdapter()` on
* finish.
*/
CommandResult sendCommand(u8 type,
const u32* params = {},
u16 length = 0,
bool invertsClock = false) {
CommandResult result;
u32 command = buildCommand(type, length);
u32 r;
_LRWLOG_("sending command 0x" + toHex(command));
if ((r = transfer(command)) != DATA_REQUEST_VALUE) {
logExpectedButReceived(DATA_REQUEST_VALUE, r);
return result;
}
u32 parameterCount = 0;
for (u32 i = 0; i < length; i++) {
u32 param = params[i];
_LRWLOG_("sending param" + std::to_string(parameterCount) + ": 0x" +
toHex(param));
if ((r = transfer(param)) != DATA_REQUEST_VALUE) {
logExpectedButReceived(DATA_REQUEST_VALUE, r);
return result;
}
parameterCount++;
}
_LRWLOG_("sending response request");
u32 response = transfer(DATA_REQUEST_VALUE);
u16 header = Link::msB32(response);
u16 data = Link::lsB32(response);
u8 responses = Link::_min(Link::msB16(data),
LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH);
u8 ack = Link::lsB16(data);
if (header != COMMAND_HEADER_VALUE) {
_LRWLOG_("! expected HEADER 0x9966");
_LRWLOG_("! but received 0x" + toHex(header));
return result;
}
if (ack != type + RESPONSE_ACK) {
if (ack == 0xEE && responses == 1 && !invertsClock) {
u8 LINK_UNUSED code = (u8)transfer(DATA_REQUEST_VALUE);
_LRWLOG_("! error received");
_LRWLOG_(code == 1 ? "! invalid state" : "! unknown cmd");
} else {
_LRWLOG_("! expected ACK 0x" + toHex(type + RESPONSE_ACK));
_LRWLOG_("! but received 0x" + toHex(ack));
}
return result;
}
_LRWLOG_("ack ok! " + std::to_string(responses) + " responses");
if (!invertsClock) {
for (u32 i = 0; i < responses; i++) {
_LRWLOG_("response " + std::to_string(i + 1) + "/" +
std::to_string(responses) + ":");
u32 responseData = transfer(DATA_REQUEST_VALUE);
result.data[result.dataSize++] = responseData;
_LRWLOG_("<< " + toHex(responseData));
}
}
result.success = true;
return result;
}
/**
* @brief Inverts the clock and waits until the adapter sends a command.
* Returns the remote command.
*/
CommandResult receiveCommandFromAdapter() {
CommandResult remoteCommand;
if (!isEnabled)
return remoteCommand;
_LRWLOG_("setting SPI to SLAVE");
linkSPI.activate(LinkSPI::Mode::SLAVE);
_LRWLOG_("WAITING for adapter cmd");
u32 command = linkSPI.transfer(
DATA_REQUEST_VALUE, []() { return false; }, false, true);
if (!reverseAcknowledge()) {
_resetState();
return remoteCommand;
}
u32 lines = 0;
u32 vCount = Link::_REG_VCOUNT;
u16 header = Link::msB32(command);
u16 data = Link::lsB32(command);
u8 params = Link::msB16(data);
u8 commandId = Link::lsB16(data);
if (header != COMMAND_HEADER_VALUE) {
_LRWLOG_("! expected HEADER 0x9966");
_LRWLOG_("! but received 0x" + toHex(header));
_resetState();
return remoteCommand;
}
_LRWLOG_("received cmd: " + toHex(commandId) + " (" +
std::to_string(params) + " params)");
for (u32 i = 0; i < params; i++) {
_LRWLOG_("param " + std::to_string(i + 1) + "/" + std::to_string(params) +
":");
u32 paramData = linkSPI.transfer(
DATA_REQUEST_VALUE,
[this, &lines, &vCount]() { return cmdTimeout(lines, vCount); },
false, true);
if (!reverseAcknowledge()) {
_resetState();
return remoteCommand;
}
remoteCommand.data[remoteCommand.dataSize++] = paramData;
_LRWLOG_("<< " + toHex(paramData));
}
_LRWLOG_("sending ack");
command = linkSPI.transfer(
(COMMAND_HEADER_VALUE << 16) | ((commandId + RESPONSE_ACK) & 0xFF),
[this, &lines, &vCount]() { return cmdTimeout(lines, vCount); }, false,
true);
if (!reverseAcknowledge(true)) {
_resetState();
return remoteCommand;
}
if (command != DATA_REQUEST_VALUE) {
_LRWLOG_("! expected CMD request");
_LRWLOG_("! but received 0x" + toHex(command));
_resetState();
return remoteCommand;
}
_LRWLOG_("setting SPI to MASTER");
linkSPI.activate(LinkSPI::Mode::MASTER_2MBPS);
remoteCommand.success = true;
remoteCommand.commandId = commandId;
return remoteCommand;
}
/**
* @brief Schedules an arbitrary command and returns the response. After
* this, call `getAsyncState()` and `getAsyncCommandResult()`. Note that until
* you retrieve the async response, next command requests will fail!
* @param type The ID of the command.
* @param params The command parameters.
* @param length The number of 32-bit values in the `params` array.
* @param invertsClock Whether this command inverts the clock or not (Wait).
* \warning If it `invertsClock`, the command result will be the one sent by
* the adapter.
*/
bool sendCommandAsync(u8 type,
const u32* params = {},
u16 length = 0,
bool invertsClock = false,
bool _fromIRQ = false) {
if ((!_fromIRQ && !isEnabled) || asyncState != AsyncState::IDLE)
return false;
asyncCommand.type = type;
asyncCommand.invertsClock = invertsClock;
asyncCommand.direction = AsyncCommand::Direction::SENDING;
for (u32 i = 0; i < length; i++)
asyncCommand.parameters[i] = params[i];
asyncCommand.result = CommandResult{};
asyncCommand.result.commandId = type;
asyncCommand.state = AsyncCommand::State::PENDING;
asyncCommand.step = AsyncCommand::Step::COMMAND_HEADER;
asyncCommand.sentParameters = 0;
asyncCommand.totalParameters = length;
asyncCommand.receivedResponses = 0;
asyncCommand.totalResponses = 0;
asyncState = AsyncState::WORKING;
u32 command = buildCommand(type, asyncCommand.totalParameters);
_LRWLOG_("sending command 0x" + toHex(command));
transferAsync(command, _fromIRQ);
return true;
}
/**
* @brief Returns the state of the last async command.
* @return One of the enum values from `LinkRawWireless::AsyncState`.
*/
[[nodiscard]] AsyncState getAsyncState() { return asyncState; }
/**
* @brief If the async state is `READY`, returns the result of the command and
* switches the state back to `IDLE`. If not, returns an empty result.
*/
[[nodiscard]] CommandResult getAsyncCommandResult() {
if (asyncState != AsyncState::READY)
return CommandResult{};
CommandResult data = asyncCommand.result;
asyncState = AsyncState::IDLE;
return data;
}
/**
* @brief Returns the maximum number of transferrable 32-bit values.
* It's 23 for servers and 4 for clients.
*/
[[nodiscard]] u32 getDeviceTransferLength() {
return state == State::SERVING
? LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH
: LINK_RAW_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH;
}
/**
* @brief Returns the current state.
*/
[[nodiscard]] State getState() { return state; }
/**
* @brief Returns `true` if the player count is higher than `1`.
*/
[[nodiscard]] bool isConnected() { return sessionState.playerCount > 1; }
/**
* @brief Returns `true` if the state is `SERVING` or `CONNECTED`.
*/
[[nodiscard]] bool isSessionActive() {
return state == State::SERVING || state == State::CONNECTED;
}
/**
* @brief Returns `true` if the server was closed with `endHost()`.
*/
[[nodiscard]] bool isServerClosed() { return sessionState.isServerClosed; }
/**
* @brief Returns the number of connected players.
*/
[[nodiscard]] u8 playerCount() { return sessionState.playerCount; }
/**
* @brief Returns the current player ID.
*/
[[nodiscard]] u8 currentPlayerId() { return sessionState.currentPlayerId; }
/**
* @brief Resets all the state.
* \warning This is internal API!
*/
void _resetState() {
LINK_BARRIER;
_LRWLOG_("state = NEEDS_RESET");
state = State::NEEDS_RESET;
asyncState = AsyncState::IDLE;
sessionState.playerCount = 1;
sessionState.currentPlayerId = 0;
sessionState.isServerClosed = false;
LINK_BARRIER;
}
/**
* @brief Returns a pointer to the internal result of the last async command
* and switches the state back to `IDLE`.
* \warning This is internal API!
*/
[[nodiscard]] CommandResult* _getAsyncCommandResultRef() {
asyncState = AsyncState::IDLE;
return &asyncCommand.result;
}
/**
* @brief This method is called by the SERIAL interrupt handler.
* \warning This is internal API!
*/
LINK_INLINE int _onSerial(bool _clockInversionSupport = true) {
if (!isEnabled)
return -1;
linkSPI._onSerial(true);
bool hasNewData = linkSPI.getAsyncState() == LinkSPI::AsyncState::READY;
if (!hasNewData)
return -2;
u32 newData = linkSPI.getAsyncData();
if (!isSessionActive() || asyncState != AsyncState::WORKING)
return -3;
if (asyncCommand.state == AsyncCommand::State::PENDING) {
if (!_clockInversionSupport ||
asyncCommand.direction == AsyncCommand::Direction::SENDING) {
if (!acknowledge())
return -4;
#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
Link::_REG_IME = 1;
#endif
sendAsyncCommand(newData, _clockInversionSupport);
} else if (_clockInversionSupport) {
if (!reverseAcknowledge(asyncCommand.step ==
AsyncCommand::Step::DATA_REQUEST))
return -5;
receiveAsyncCommand(newData);
}
if (asyncCommand.state == AsyncCommand::State::COMPLETED) {
asyncState = AsyncState::READY;
return 1;
}
}
return 0;
}
#ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING
/**
* @brief Converts `w` to an hexadecimal string.
*/
template <typename I>
[[nodiscard]] std::string toHex(I w, size_t hex_len = sizeof(I) << 1) {
static const char* digits = "0123456789ABCDEF";
std::string rc(hex_len, '0');
for (size_t i = 0, j = (hex_len - 1) * 4; i < hex_len; ++i, j -= 4)
rc[i] = digits[(w >> j) & 0x0F];
return rc;
}
#endif
// -------------
// Low-level API
// -------------
struct SessionState {
vu8 playerCount = 1;
vu8 currentPlayerId = 0;
volatile bool isServerClosed = false;
};
SessionState sessionState;
// ------------
private:
struct LoginMemory {
u16 previousGBAData = 0xFFFF;
u16 previousAdapterData = 0x8000;
};
struct AsyncCommand {
enum class State { PENDING, COMPLETED };
enum class Direction { SENDING, RECEIVING };
enum class Step {
COMMAND_HEADER,
COMMAND_PARAMETERS,
RESPONSE_REQUEST,
DATA_REQUEST
}; // (these are named from the sender's point of view)
u8 type;
bool invertsClock;
Direction direction;
u32 parameters[LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH];
u32 responses[LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH];
CommandResult result;
State state;
Step step;
u32 sentParameters, totalParameters;
u32 receivedResponses, totalResponses;
};
LinkSPI linkSPI;
LinkGPIO linkGPIO;
volatile State state = State::NEEDS_RESET;
volatile AsyncState asyncState = AsyncState::IDLE;
AsyncCommand asyncCommand;
volatile bool isEnabled = false;
void copyName(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 recoverName(char* name,
u32& nameCursor,
u32 word,
bool includeFirstTwoBytes = true) {
u32 character = 0;
if (includeFirstTwoBytes) {
character = Link::lsB16(Link::lsB32(word));
if (character > 0)
name[nameCursor++] = character;
character = Link::msB16(Link::lsB32(word));
if (character > 0)
name[nameCursor++] = character;
}
character = Link::lsB16(Link::msB32(word));
if (character > 0)
name[nameCursor++] = character;
character = Link::msB16(Link::msB32(word));
if (character > 0)
name[nameCursor++] = character;
}
bool reset(bool stopFirst) {
_resetState();
if (stopFirst)
stop();
return start();
}
void stop() { linkSPI.deactivate(); }
bool start() {
pingAdapter();
_LRWLOG_("setting SPI to 256Kbps");
linkSPI.activate(LinkSPI::Mode::MASTER_256KBPS);
if (!login())
return false;
Link::wait(TRANSFER_WAIT);
_LRWLOG_("sending HELLO command");
if (!sendCommand(COMMAND_HELLO).success)
return false;
_LRWLOG_("setting SPI to 2Mbps");
linkSPI.activate(LinkSPI::Mode::MASTER_2MBPS);
_LRWLOG_("state = AUTHENTICATED");
state = State::AUTHENTICATED;
return true;
}
void pingAdapter() {
linkGPIO.reset();
_LRWLOG_("setting SO as OUTPUT");
linkGPIO.setMode(LinkGPIO::Pin::SO, LinkGPIO::Direction::OUTPUT);
_LRWLOG_("setting SD as OUTPUT");
linkGPIO.setMode(LinkGPIO::Pin::SD, LinkGPIO::Direction::OUTPUT);
_LRWLOG_("setting SD = HIGH");
linkGPIO.writePin(LinkGPIO::Pin::SD, true);
Link::wait(PING_WAIT);
_LRWLOG_("setting SD = LOW");
linkGPIO.writePin(LinkGPIO::Pin::SD, false);
}
bool login() {
LoginMemory memory;
for (u32 i = 0; i < LOGIN_STEPS; i++) {
_LRWLOG_("sending login packet " + std::to_string(i + 1) + "/" +
std::to_string(LOGIN_STEPS));
if (!exchangeLoginPacket(LOGIN_PARTS[i],
i < LOGIN_JUNK_STEPS ? 0 : LOGIN_PARTS[i],
memory))
return false;
}
return true;
}
bool exchangeLoginPacket(u16 data,
u16 expectedResponse,
LoginMemory& memory) {
u32 packet = Link::buildU32(~memory.previousAdapterData, data);
u32 response = transfer(packet, false);
u32 adapterData = Link::msB32(response);
if (expectedResponse != 0 &&
(adapterData != expectedResponse ||
Link::lsB32(response) != (u16)~memory.previousGBAData)) {
logExpectedButReceived(
Link::buildU32(expectedResponse, (u16)~memory.previousGBAData),
response);
return false;
}
memory.previousGBAData = data;
memory.previousAdapterData = adapterData;
return true;
}
u32 buildCommand(u8 type, u8 length = 0) {
return Link::buildU32(COMMAND_HEADER_VALUE, Link::buildU16(length, type));
}
u32 transfer(u32 data, bool customAck = true) {
if (!customAck)
Link::wait(TRANSFER_WAIT);
u32 lines = 0;
u32 vCount = Link::_REG_VCOUNT;
u32 receivedData = linkSPI.transfer(
data, [this, &lines, &vCount]() { return cmdTimeout(lines, vCount); },
false, customAck);
if (customAck && !acknowledge())
return LINK_SPI_NO_DATA_32;
return receivedData;
}
bool acknowledge() {
u32 lines = 0;
u32 vCount = Link::_REG_VCOUNT;
linkSPI._setSOLow();
while (!linkSPI._isSIHigh()) {
if (cmdTimeout(lines, vCount)) {
_LRWLOG_("! ACK 1 failed. I put SO=LOW,");
_LRWLOG_("! but SI didn't become HIGH.");
return false;
}
}
linkSPI._setSOHigh();
while (linkSPI._isSIHigh()) {
if (cmdTimeout(lines, vCount)) {
_LRWLOG_("! ACK 2 failed. I put SO=HIGH,");
_LRWLOG_("! but SI didn't become LOW.");
return false;
}
}
linkSPI._setSOLow();
return true;
}
bool reverseAcknowledge(bool isLastPart = false) {
// `isLastPart` is required when there's no subsequent
// `linkSPI.transfer(...)` call.
u32 lines = 0;
u32 vCount = Link::_REG_VCOUNT;
linkSPI._setSOLow();
while (linkSPI._isSIHigh()) {
if (cmdTimeout(lines, vCount)) {
_LRWLOG_("! RevAck0 failed. I put SO=LOW,");
_LRWLOG_("! but SI didn't become LOW.");
return false;
}
}
linkSPI._setSOHigh();
while (!linkSPI._isSIHigh()) {
if (cmdTimeout(lines, vCount)) {
_LRWLOG_("! RevAck1 failed. I put SO=HIGH,");
_LRWLOG_("! but SI didn't become HIGH.");
return false;
}
}
Link::wait(MICRO_WAIT); // this wait is VERY important to avoid desyncs!
// wait at least 40us; monitoring VCOUNT to avoid requiring a timer or
// putting some wait code in IWRAM
// (normally, this occurs on the next linkSPI.transfer(...) call)
if (isLastPart) {
linkSPI._setSOLow();
while (linkSPI._isSIHigh()) {
if (cmdTimeout(lines, vCount)) {
_LRWLOG_("! RevAck2 failed. I put SO=LOW,");
_LRWLOG_("! but SI didn't become LOW.");
return false;
}
}
}
return true;
}
bool cmdTimeout(u32& lines, u32& vCount) {
return timeout(CMD_TIMEOUT, lines, vCount);
}
bool timeout(u32 limit, u32& lines, u32& vCount) {
if (Link::_REG_VCOUNT != vCount) {
lines += Link::_max((int)Link::_REG_VCOUNT - (int)vCount, 0);
vCount = Link::_REG_VCOUNT;
}
return lines > limit;
}
LINK_INLINE void sendAsyncCommand(
u32 newData,
bool _clockInversionSupport = true) { // (irq only)
switch (asyncCommand.step) {
case AsyncCommand::Step::COMMAND_HEADER: {
if (newData != DATA_REQUEST_VALUE) {
asyncCommand.state = AsyncCommand::State::COMPLETED;
return;
}
sendParametersOrRequestResponse();
break;
}
case AsyncCommand::Step::COMMAND_PARAMETERS: {
if (newData != DATA_REQUEST_VALUE) {
asyncCommand.state = AsyncCommand::State::COMPLETED;
return;
}
sendParametersOrRequestResponse();
break;
}
case AsyncCommand::Step::RESPONSE_REQUEST: {
u16 header = Link::msB32(newData);
u16 data = Link::lsB32(newData);
u8 responses = Link::msB16(data);
u8 ack = Link::lsB16(data);
if (header != COMMAND_HEADER_VALUE ||
ack != asyncCommand.type + RESPONSE_ACK ||
responses > LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH) {
if (header != COMMAND_HEADER_VALUE) {
_LRWLOG_("! expected HEADER 0x9966");
_LRWLOG_("! but received 0x" + toHex(header));
}
if (ack != asyncCommand.type + RESPONSE_ACK) {
if (ack == 0xEE) {
_LRWLOG_("! error received");
} else {
_LRWLOG_("! expected ACK 0x" +
toHex(asyncCommand.type + RESPONSE_ACK));
_LRWLOG_("! but received 0x" + toHex(ack));
}
}
asyncCommand.state = AsyncCommand::State::COMPLETED;
return;
}
_LRWLOG_("ack ok! " + std::to_string(responses) + " responses");
asyncCommand.totalResponses = responses;
asyncCommand.result.dataSize = responses;
receiveResponseOrFinish(_clockInversionSupport);
break;
}
case AsyncCommand::Step::DATA_REQUEST: {
_LRWLOG_("response " +
std::to_string(asyncCommand.receivedResponses + 1) + "/" +
std::to_string(asyncCommand.totalResponses) + ":");
_LRWLOG_("<< " + toHex(newData));
asyncCommand.result.data[asyncCommand.receivedResponses] = newData;
asyncCommand.receivedResponses++;
receiveResponseOrFinish(_clockInversionSupport);
break;
}
default: {
}
}
}
void sendParametersOrRequestResponse() { // (irq only)
if (asyncCommand.sentParameters < asyncCommand.totalParameters) {
asyncCommand.step = AsyncCommand::Step::COMMAND_PARAMETERS;
_LRWLOG_("sending param" + std::to_string(asyncCommand.sentParameters) +
": 0x" +
toHex(asyncCommand.parameters[asyncCommand.sentParameters]));
transferAsync(asyncCommand.parameters[asyncCommand.sentParameters], true);
asyncCommand.sentParameters++;
} else {
_LRWLOG_("sending response request");
asyncCommand.step = AsyncCommand::Step::RESPONSE_REQUEST;
transferAsync(DATA_REQUEST_VALUE, true);
}
}
void receiveResponseOrFinish(
bool _clockInversionSupport = true) { // (irq only)
if (asyncCommand.receivedResponses < asyncCommand.totalResponses) {
asyncCommand.step = AsyncCommand::Step::DATA_REQUEST;
transferAsync(DATA_REQUEST_VALUE, true);
} else {
if (_clockInversionSupport && asyncCommand.invertsClock) {
_LRWLOG_("setting SPI to SLAVE");
linkSPI.activate(LinkSPI::Mode::SLAVE);
asyncCommand.type = 0;
asyncCommand.invertsClock = true;
asyncCommand.direction = AsyncCommand::Direction::RECEIVING;
asyncCommand.result = CommandResult{};
asyncCommand.state = AsyncCommand::State::PENDING;
asyncCommand.step = AsyncCommand::Step::COMMAND_HEADER;
asyncCommand.sentParameters = 0;
asyncCommand.totalParameters = 0;
asyncCommand.receivedResponses = 0;
asyncCommand.totalResponses = 0;
_LRWLOG_("WAITING for adapter cmd");
transferAsync(DATA_REQUEST_VALUE, true);
} else {
asyncCommand.result.success = true;
asyncCommand.state = AsyncCommand::State::COMPLETED;
}
}
}
LINK_INLINE void receiveAsyncCommand(u32 newData) { // (irq only)
switch (asyncCommand.step) {
case AsyncCommand::Step::COMMAND_HEADER: {
u16 header = Link::msB32(newData);
u16 data = Link::lsB32(newData);
u8 params = Link::msB16(data);
u8 commandId = Link::lsB16(data);
if (header != COMMAND_HEADER_VALUE) {
_LRWLOG_("! expected HEADER 0x9966");
_LRWLOG_("! but received 0x" + toHex(header));
asyncCommand.state = AsyncCommand::State::COMPLETED;
return;
}
_LRWLOG_("received cmd: " + toHex(commandId) + " (" +
std::to_string(params) + " params)");
asyncCommand.type = commandId;
asyncCommand.result.commandId = asyncCommand.type;
asyncCommand.result.dataSize = params;
if (params > 0) {
asyncCommand.step = AsyncCommand::Step::COMMAND_PARAMETERS;
_LRWLOG_("param 1/" + std::to_string(params) + ":");
transferAsync(DATA_REQUEST_VALUE, true);
} else {
acknowledgeRemoteCommand();
}
break;
}
case AsyncCommand::Step::COMMAND_PARAMETERS: {
asyncCommand.result.data[asyncCommand.sentParameters++] = newData;
_LRWLOG_("param " + std::to_string(asyncCommand.sentParameters + 1) +
"/" + std::to_string(asyncCommand.totalParameters) + ":");
_LRWLOG_("<< " + toHex(newData));
if (asyncCommand.sentParameters < asyncCommand.result.dataSize)
transferAsync(DATA_REQUEST_VALUE, true);
else
acknowledgeRemoteCommand();
break;
}
case AsyncCommand::Step::RESPONSE_REQUEST: {
break; // (unused)
}
case AsyncCommand::Step::DATA_REQUEST: {
if (newData != DATA_REQUEST_VALUE) {
_LRWLOG_("! expected CMD request");
_LRWLOG_("! but received 0x" + toHex(newData));
asyncCommand.state = AsyncCommand::State::COMPLETED;
return;
}
_LRWLOG_("setting SPI to MASTER");
linkSPI.activate(LinkSPI::Mode::MASTER_2MBPS);
asyncCommand.result.success = true;
asyncCommand.state = AsyncCommand::State::COMPLETED;
asyncState = AsyncState::READY;
}
}
}
void acknowledgeRemoteCommand() { // (irq only)
_LRWLOG_("sending ack");
asyncCommand.step = AsyncCommand::Step::DATA_REQUEST;
u32 ack = (COMMAND_HEADER_VALUE << 16) |
((asyncCommand.type + RESPONSE_ACK) & 0xFF);
transferAsync(ack, true);
}
void transferAsync(u32 data, bool fromIRQ) {
#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
if (fromIRQ)
Link::_REG_IME = 0;
#endif
linkSPI.transfer(data, []() { return false; }, true, true);
}
void logExpectedButReceived(u32 expected, u32 received) {
_LRWLOG_("! expected 0x" + toHex(expected));
_LRWLOG_("! but received 0x" + toHex(received));
}
};
extern LinkRawWireless* linkRawWireless;
/**
* @brief SERIAL interrupt handler.
*/
inline void LINK_RAW_WIRELESS_ISR_SERIAL() {
linkRawWireless->_onSerial();
}
#undef _LRWLOG_
#endif // LINK_RAW_WIRELESS_H