#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 #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 [[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