#ifndef LINK_WIRELESS_OPEN_SDK_H #define LINK_WIRELESS_OPEN_SDK_H // -------------------------------------------------------------------------- // An open-source implementation of the "official" Wireless Adapter protocol. // -------------------------------------------------------------------------- // - advanced usage only; you only need this if you want to interact with N // software! // -------------------------------------------------------------------------- #ifndef LINK_DEVELOPMENT #pragma GCC system_header #endif #include "_link_common.hpp" #include "LinkRawWireless.hpp" LINK_VERSION_TAG LINK_WIRELESS_OPEN_SDK_VERSION = "LinkWirelessOpenSDK/v8.0.3"; /** * @brief An open-source implementation of the "official" Wireless Adapter * protocol. */ class LinkWirelessOpenSDK { private: using u32 = Link::u32; using u16 = Link::u16; using u8 = Link::u8; public: static constexpr int MAX_TRANSFER_WORDS = 23; static constexpr int MAX_TRANSFER_BYTES_SERVER = LinkRawWireless::MAX_TRANSFER_BYTES_SERVER; static constexpr int MAX_TRANSFER_BYTES_CLIENT = LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT; static constexpr int HEADER_SIZE_SERVER = 3; static constexpr int HEADER_SIZE_CLIENT = 2; static constexpr int HEADER_MASK_SERVER = 0b1111111111111111111111; static constexpr int HEADER_MASK_CLIENT = 0b11111111111111; static constexpr int MAX_PAYLOAD_SERVER = MAX_TRANSFER_BYTES_SERVER - HEADER_SIZE_SERVER; static constexpr int MAX_PAYLOAD_CLIENT = MAX_TRANSFER_BYTES_CLIENT - HEADER_SIZE_CLIENT; static constexpr int MAX_PACKETS_SERVER = MAX_TRANSFER_BYTES_SERVER / HEADER_SIZE_SERVER; static constexpr int MAX_PACKETS_CLIENT = MAX_TRANSFER_BYTES_CLIENT / HEADER_SIZE_CLIENT; public: template struct SendBuffer { T header; u32 data[MAX_TRANSFER_WORDS]; u32 dataSize = 0; u32 totalByteCount = 0; }; enum class CommState : unsigned int { OFF = 0, STARTING = 1, COMMUNICATING = 2, ENDING = 3, DIRECT = 4 }; struct SequenceNumber { u32 n = 0; u32 phase = 0; CommState commState = CommState::OFF; static SequenceNumber fromPacketId(u32 packetId) { return SequenceNumber{.n = ((packetId + 4) / 4) % 4, .phase = packetId % 4, .commState = CommState::COMMUNICATING}; } bool operator==(const SequenceNumber& other) { return n == other.n && phase == other.phase && commState == other.commState; } bool operator!=(const SequenceNumber& other) { return !(*this == other); } }; struct ServerSDKHeader { unsigned int payloadSize : 7; unsigned int _unused_ : 2; unsigned int phase : 2; unsigned int n : 2; unsigned int isACK : 1; CommState commState : 4; unsigned int targetSlots : 4; SequenceNumber sequence() { return SequenceNumber{.n = n, .phase = phase, .commState = commState}; } }; union ServerSDKHeaderPacker { ServerSDKHeader asStruct; u32 asInt; }; struct ServerPacket { ServerSDKHeader header; u8 payload[MAX_PAYLOAD_SERVER]; }; struct ServerResponse { ServerPacket packets[MAX_PACKETS_SERVER]; u32 packetsSize = 0; }; struct ParentData { ServerResponse response; }; struct ClientSDKHeader { unsigned int payloadSize : 5; unsigned int phase : 2; unsigned int n : 2; unsigned int isACK : 1; CommState commState : 4; SequenceNumber sequence() { return SequenceNumber{.n = n, .phase = phase, .commState = commState}; } }; union ClientSDKHeaderPacker { ClientSDKHeader asStruct; u16 asInt; }; struct ClientPacket { ClientSDKHeader header; u8 payload[MAX_PAYLOAD_CLIENT]; }; struct ClientResponse { ClientPacket packets[MAX_PACKETS_CLIENT]; u32 packetsSize = 0; }; struct ChildrenData { ClientResponse responses[4]; }; /** * @brief Parses the `response` and returns a struct containing all the * received packets from the connected clients. * @param response The response to be parsed. */ [[nodiscard]] ChildrenData getChildrenData(LinkRawWireless::ReceiveDataResponse response) { u8* buffer = (u8*)response.data; u32 cursor = 0; ChildrenData childrenData; if (response.sentBytes[1] + response.sentBytes[2] + response.sentBytes[3] + response.sentBytes[4] > response.dataSize * 4) return childrenData; for (u32 i = 1; i < LINK_RAW_WIRELESS_MAX_PLAYERS; i++) { ClientResponse* clientResponse = &childrenData.responses[i - 1]; u32 remainingBytes = response.sentBytes[i]; while (remainingBytes >= HEADER_SIZE_CLIENT) { ClientPacket* packet = &clientResponse->packets[clientResponse->packetsSize]; u32 headerInt = (buffer[cursor + 1] << 8) | buffer[cursor]; packet->header = parseClientHeader(headerInt); cursor += HEADER_SIZE_CLIENT; remainingBytes -= HEADER_SIZE_CLIENT; if (packet->header.payloadSize > 0 && packet->header.payloadSize <= MAX_PAYLOAD_CLIENT && remainingBytes >= packet->header.payloadSize) { for (u32 j = 0; j < packet->header.payloadSize; j++) packet->payload[j] = buffer[cursor++]; remainingBytes -= packet->header.payloadSize; } clientResponse->packetsSize++; } } return childrenData; } /** * @brief Parses the `response` and returns a struct containing all the * received packets from the host. * @param response The response to be parsed. */ [[nodiscard]] ParentData getParentData(LinkRawWireless::ReceiveDataResponse response) { u8* buffer = (u8*)response.data; u32 cursor = 0; ParentData parentData; if (response.sentBytes[0] > response.dataSize * 4) return parentData; ServerResponse* serverResponse = &parentData.response; u32 remainingBytes = response.sentBytes[0]; while (remainingBytes >= HEADER_SIZE_SERVER) { ServerPacket* packet = &serverResponse->packets[serverResponse->packetsSize]; u32 headerInt = (buffer[cursor + 2] << 16) | (buffer[cursor + 1] << 8) | buffer[cursor]; packet->header = parseServerHeader(headerInt); cursor += HEADER_SIZE_SERVER; remainingBytes -= HEADER_SIZE_SERVER; if (packet->header.payloadSize > 0 && packet->header.payloadSize <= MAX_PAYLOAD_SERVER && remainingBytes >= packet->header.payloadSize) { for (u32 j = 0; j < packet->header.payloadSize; j++) packet->payload[j] = buffer[cursor++]; remainingBytes -= packet->header.payloadSize; } serverResponse->packetsSize++; } return parentData; } /** * @brief Creates a buffer for the host to send a `fullPayload` with a valid * header. If `fullPayloadSize` is higher than `84` (the maximum payload * size), the buffer will only contain the **first** `84` bytes (unless an * `offset` > 0 is used). A `sequence` number must be created by using * `LinkWirelessOpenSDK::SequenceNumber::fromPacketId(...)`. Optionally, a * `targetSlots` bit array can be used to exclude some clients from the * transmissions (the default is `0b1111`). * @param fullPayload A pointer to the payload buffer. * @param fullPayloadSize Total size of the payload. * @param sequence A sequence number created using * `LinkWirelessOpenSDK::SequenceNumber::fromPacketId(...)`. * @param targetSlots A bit array that can be used to exclude some clients * (the default is `0b1111`). * @param offset The offset within the `fullPayload` pointer. Defaults to `0`. */ [[nodiscard]] SendBuffer createServerBuffer(const u8* fullPayload, u32 fullPayloadSize, SequenceNumber sequence, u8 targetSlots = 0b1111, u32 offset = 0) { SendBuffer buffer; u32 payloadSize = Link::_min(fullPayloadSize, MAX_PAYLOAD_SERVER); buffer.header.isACK = 0; buffer.header.targetSlots = targetSlots; buffer.header.payloadSize = payloadSize; buffer.header.n = sequence.n; buffer.header.phase = sequence.phase; buffer.header.commState = sequence.commState; buffer.header._unused_ = 0; u32 headerInt = serializeServerHeader(buffer.header); buffer.data[buffer.dataSize++] = headerInt; if (offset < fullPayloadSize) buffer.data[0] |= fullPayload[offset] << 24; for (u32 i = 1; i < payloadSize; i += 4) { u32 word = 0; for (u32 j = 0; j < 4; j++) { if (offset + i + j < fullPayloadSize && i + j < MAX_PAYLOAD_SERVER) { u8 byte = fullPayload[offset + i + j]; word |= byte << (j * 8); } } buffer.data[buffer.dataSize++] = word; } buffer.totalByteCount = HEADER_SIZE_SERVER + payloadSize; return buffer; } /** * @brief Creates a buffer for the host to acknowledge a header received from * a certain `clientNumber`. * @param clientHeader The header of the received packet. * @param clientNumber `(0~3)` The client number that sent the packet. */ [[nodiscard]] SendBuffer createServerACKBuffer( ClientSDKHeader clientHeader, u8 clientNumber) { SendBuffer buffer; buffer.header = createACKHeaderFor(clientHeader, clientNumber); u32 headerInt = serializeServerHeader(buffer.header); buffer.data[buffer.dataSize++] = headerInt; buffer.totalByteCount = HEADER_SIZE_SERVER; return buffer; } /** * @brief Creates a buffer for the client to send a `fullPayload` with a valid * header. If `fullPayloadSize` is higher than `14` (the maximum payload * size), the buffer will only contain the **first** `14` bytes (unless an * `offset` > 0 is used). A `sequence` number must be created by using * `LinkWirelessOpenSDK::SequenceNumber::fromPacketId(...)`. * @param fullPayload A pointer to the payload buffer. * @param fullPayloadSize Total size of the payload. * @param sequence A sequence number created using * `LinkWirelessOpenSDK::SequenceNumber::fromPacketId(...)`. * @param offset The offset within the `fullPayload` pointer. Defaults to `0`. */ [[nodiscard]] SendBuffer createClientBuffer(const u8* fullPayload, u32 fullPayloadSize, SequenceNumber sequence, u32 offset = 0) { SendBuffer buffer; u32 payloadSize = Link::_min(fullPayloadSize, MAX_PAYLOAD_CLIENT); buffer.header.isACK = 0; buffer.header.payloadSize = payloadSize; buffer.header.n = sequence.n; buffer.header.phase = sequence.phase; buffer.header.commState = sequence.commState; u16 headerInt = serializeClientHeader(buffer.header); buffer.data[buffer.dataSize++] = headerInt; if (offset < fullPayloadSize) buffer.data[0] |= fullPayload[offset] << 16; if (offset + 1 < fullPayloadSize) buffer.data[0] |= fullPayload[offset + 1] << 24; for (u32 i = 2; i < payloadSize; i += 4) { u32 word = 0; for (u32 j = 0; j < 4; j++) { if (offset + i + j < fullPayloadSize && i + j < MAX_PAYLOAD_CLIENT) { u8 byte = fullPayload[offset + i + j]; word |= byte << (j * 8); } } buffer.data[buffer.dataSize++] = word; } buffer.totalByteCount = HEADER_SIZE_CLIENT + payloadSize; return buffer; } /** * @brief Creates a buffer for the client to acknowledge a header received * from the host. * @param serverHeader The header of the received packet. */ [[nodiscard]] SendBuffer createClientACKBuffer( ServerSDKHeader serverHeader) { SendBuffer buffer; buffer.header = createACKHeaderFor(serverHeader); u16 headerInt = serializeClientHeader(buffer.header); buffer.data[buffer.dataSize++] = headerInt; buffer.totalByteCount = HEADER_SIZE_CLIENT; return buffer; } private: [[nodiscard]] ServerSDKHeader createACKHeaderFor(ClientSDKHeader clientHeader, u8 clientNumber) { ServerSDKHeader serverHeader; serverHeader.isACK = 1; serverHeader.targetSlots = 1 << clientNumber; serverHeader.payloadSize = 0; serverHeader.n = clientHeader.n; serverHeader.phase = clientHeader.phase; serverHeader.commState = clientHeader.commState; serverHeader._unused_ = 0; return serverHeader; } [[nodiscard]] ClientSDKHeader createACKHeaderFor(ServerSDKHeader serverHeader) { ClientSDKHeader clientHeader; clientHeader.isACK = 1; clientHeader.payloadSize = 0; clientHeader.n = serverHeader.n; clientHeader.phase = serverHeader.phase; clientHeader.commState = serverHeader.commState; return clientHeader; } [[nodiscard]] ClientSDKHeader parseClientHeader(u32 clientHeaderInt) { ClientSDKHeaderPacker clientPacker; clientPacker.asInt = clientHeaderInt & HEADER_MASK_CLIENT; return clientPacker.asStruct; } [[nodiscard]] u16 serializeClientHeader(ClientSDKHeader clientHeader) { ClientSDKHeaderPacker clientPacker; clientPacker.asStruct = clientHeader; return clientPacker.asInt & HEADER_MASK_CLIENT; } [[nodiscard]] ServerSDKHeader parseServerHeader(u32 serverHeaderInt) { ServerSDKHeaderPacker serverPacker; serverPacker.asInt = serverHeaderInt & HEADER_MASK_SERVER; return serverPacker.asStruct; } [[nodiscard]] u32 serializeServerHeader(ServerSDKHeader serverHeader) { ServerSDKHeaderPacker serverPacker; serverPacker.asStruct = serverHeader; return serverPacker.asInt & HEADER_MASK_SERVER; } template struct Transfer { private: struct PendingTransfer { u32 cursor; bool ack; bool isActive = false; void reset() { isActive = false; } }; struct PendingTransferList { PendingTransfer transfers[MaxInflightPackets] = {}; void reset() { for (u32 i = 0; i < MaxInflightPackets; i++) transfers[i].reset(); } [[nodiscard]] PendingTransfer* max(bool ack = false) { int maxCursor = -1; int maxI = -1; for (u32 i = 0; i < MaxInflightPackets; i++) { if (transfers[i].isActive && (int)transfers[i].cursor > maxCursor && (!ack || transfers[i].ack)) { maxCursor = transfers[i].cursor; maxI = i; } } return maxI > -1 ? &transfers[maxI] : nullptr; } [[nodiscard]] PendingTransfer* minWithoutACK() { u32 minCursor = 0xFFFFFFFF; int minI = -1; for (u32 i = 0; i < MaxInflightPackets; i++) { if (transfers[i].isActive && transfers[i].cursor < minCursor && !transfers[i].ack) { minCursor = transfers[i].cursor; minI = i; } } return minI > -1 ? &transfers[minI] : nullptr; } void addIfNeeded(u32 newCursor) { auto maxTransfer = max(); if (maxTransfer != nullptr && newCursor <= maxTransfer->cursor) return; for (u32 i = 0; i < MaxInflightPackets; i++) { if (!transfers[i].isActive) { transfers[i].cursor = newCursor; transfers[i].ack = false; transfers[i].isActive = true; break; } } } int ack(SequenceNumber sequence) { int index = findIndex(sequence); if (index == -1) return -1; transfers[index].ack = true; auto maxACKTransfer = max(true); bool canUpdateCursor = maxACKTransfer != nullptr && isACKCompleteUpTo(maxACKTransfer->cursor); if (canUpdateCursor) cleanup(); return canUpdateCursor ? maxACKTransfer->cursor + 1 : -1; } void cleanup() { for (u32 i = 0; i < MaxInflightPackets; i++) { if (transfers[i].isActive && transfers[i].ack) transfers[i].isActive = false; } } [[nodiscard]] bool isFull() { return size() == MaxInflightPackets; } [[nodiscard]] u32 size() { u32 size = 0; for (u32 i = 0; i < MaxInflightPackets; i++) if (transfers[i].isActive) size++; return size; } private: [[nodiscard]] bool isACKCompleteUpTo(u32 cursor) { for (u32 i = 0; i < MaxInflightPackets; i++) if (transfers[i].isActive && !transfers[i].ack && transfers[i].cursor < cursor) return false; return true; } [[nodiscard]] int findIndex(SequenceNumber sequence) { for (u32 i = 0; i < MaxInflightPackets; i++) { if (transfers[i].isActive && SequenceNumber::fromPacketId(transfers[i].cursor) == sequence) { return i; } } return -1; } }; public: u32 cursor = 0; PendingTransferList pendingTransferList = {}; void reset() { cursor = 0; pendingTransferList.reset(); } [[nodiscard]] u32 nextCursor(bool canSendInflightPackets) { u32 pendingCount = pendingTransferList.size(); if (canSendInflightPackets && pendingCount > 0 && pendingCount < MaxInflightPackets) { auto max = pendingTransferList.max(); // (`max` is never null here! but the compiler complains...) return max != nullptr ? max->cursor + 1 : 0; } else { auto minWithoutACK = pendingTransferList.minWithoutACK(); return minWithoutACK != nullptr ? minWithoutACK->cursor : cursor; } } void addIfNeeded(u32 newCursor) { if (newCursor >= cursor) pendingTransferList.addIfNeeded(newCursor); } [[nodiscard]] u32 transferred() { return cursor * MAX_PAYLOAD_SERVER; } [[nodiscard]] SequenceNumber sequence() { return SequenceNumber::fromPacketId(cursor); } }; public: /** * @brief A file transfer from a host to N clients. * @tparam MaxInflightPackets Maximum number of packets that can be sent * without a confirmation. */ template class MultiTransfer { public: /** * @brief Constructs a new MultiTransfer object. * @param linkWirelessOpenSDK An pointer to a `LinkWirelessOpenSDK`. */ explicit MultiTransfer(LinkWirelessOpenSDK* linkWirelessOpenSDK) { this->linkWirelessOpenSDK = linkWirelessOpenSDK; } /** * @brief Configures the file transfer and resets the state. * @param fileSize Size of the file. * @param connectedClients Number of clients. */ void configure(u32 fileSize, u32 connectedClients) { this->fileSize = fileSize; this->connectedClients = connectedClients; for (u32 i = 0; i < LINK_RAW_WIRELESS_MAX_PLAYERS - 1; i++) transfers[i].reset(); this->finished = false; this->cursor = 0; } /** * @brief Returns whether the transfer has completed or not. */ [[nodiscard]] bool hasFinished() { return finished; } /** * @brief Returns the current cursor (packet number). */ [[nodiscard]] u32 getCursor() { return cursor; } /** * @brief Returns a `SendBuffer`, ready for use with * `LinkRawWireless::sendData(...)` to send the next packet. The internal * state is updated to keep track of the transfer. * @param fileBytes The pointer to the file bytes. It should always be the * same across all calls unless you're changing it on the fly. */ [[nodiscard]] SendBuffer createNextSendBuffer(const u8* fileBytes) { if (finished) return SendBuffer{}; u32 offset = cursor * LinkWirelessOpenSDK::MAX_PAYLOAD_SERVER; auto sequence = SequenceNumber::fromPacketId(cursor); auto sendBuffer = linkWirelessOpenSDK->createServerBuffer( fileBytes, fileSize, sequence, 0b1111, offset); for (u32 i = 0; i < connectedClients; i++) transfers[i].addIfNeeded(cursor); return sendBuffer; } /** * @brief Processes a response from `LinkRawWireless::receiveData(...)`, * updating the cursor and the internal state. * @param response The received response from the adapter. * @return The completion percentage (0~100). */ u8 processResponse(LinkRawWireless::ReceiveDataResponse response) { if (finished) return 100; auto childrenData = linkWirelessOpenSDK->getChildrenData(response); updateACKs(childrenData); auto transferredBytes = minClientTransferredBytes(); finished = transferredBytes >= fileSize; cursor = findMinCursor(); return Link::_min(transferredBytes * 100 / fileSize, 100); } private: Transfer transfers[LINK_RAW_WIRELESS_MAX_PLAYERS - 1] = {}; LinkWirelessOpenSDK* linkWirelessOpenSDK; u32 fileSize = 0; u32 connectedClients = 0; bool finished = false; u32 cursor = 0; void updateACKs(ChildrenData childrenData) { for (u32 i = 0; i < connectedClients; i++) { for (u32 j = 0; j < childrenData.responses[i].packetsSize; j++) { auto header = childrenData.responses[i].packets[j].header; if (header.isACK) { int newACKCursor = transfers[i].pendingTransferList.ack(header.sequence()); if (newACKCursor > -1) transfers[i].cursor = newACKCursor; } } } } [[nodiscard]] u32 minClientTransferredBytes() { return transfers[findMinClient()].transferred(); } [[nodiscard]] u32 findMinClient() { u32 minTransferredBytes = 0xFFFFFFFF; u32 minClient = 0; for (u32 i = 0; i < connectedClients; i++) { u32 transferred = transfers[i].transferred(); if (transferred < minTransferredBytes) { minTransferredBytes = transferred; minClient = i; } } return minClient; } [[nodiscard]] u32 findMinCursor() { u32 minNextCursor = 0xFFFFFFFF; bool canSendInflightPackets = true; for (u32 i = 0; i < connectedClients; i++) { if (transfers[i].pendingTransferList.isFull()) canSendInflightPackets = false; } for (u32 i = 0; i < connectedClients; i++) { u32 nextCursor = transfers[i].nextCursor(canSendInflightPackets); if (nextCursor < minNextCursor) minNextCursor = nextCursor; } return minNextCursor; } }; }; #endif // LINK_WIRELESS_OPEN_SDK_H