mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-03-21 17:44:21 -05:00
746 lines
23 KiB
C++
746 lines
23 KiB
C++
#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.0";
|
|
|
|
/**
|
|
* @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 <class T>
|
|
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<ServerSDKHeader> createServerBuffer(const u8* fullPayload,
|
|
u32 fullPayloadSize,
|
|
SequenceNumber sequence,
|
|
u8 targetSlots = 0b1111,
|
|
u32 offset = 0) {
|
|
SendBuffer<ServerSDKHeader> 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<ServerSDKHeader> createServerACKBuffer(
|
|
ClientSDKHeader clientHeader,
|
|
u8 clientNumber) {
|
|
SendBuffer<ServerSDKHeader> 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<ClientSDKHeader> createClientBuffer(const u8* fullPayload,
|
|
u32 fullPayloadSize,
|
|
SequenceNumber sequence,
|
|
u32 offset = 0) {
|
|
SendBuffer<ClientSDKHeader> 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<ClientSDKHeader> createClientACKBuffer(
|
|
ServerSDKHeader serverHeader) {
|
|
SendBuffer<ClientSDKHeader> 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 <u32 MaxInflightPackets>
|
|
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 <u32 MaxInflightPackets>
|
|
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<ServerSDKHeader> createNextSendBuffer(const u8* fileBytes) {
|
|
if (finished)
|
|
return SendBuffer<ServerSDKHeader>{};
|
|
|
|
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<MaxInflightPackets> 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
|