Skip to content

Commit

Permalink
Merge pull request #5 from rodri042/retransmit
Browse files Browse the repository at this point in the history
📻 Wireless retransmission protocol
  • Loading branch information
afska authored Feb 3, 2023
2 parents 30cb789 + e839e6f commit 283ebab
Show file tree
Hide file tree
Showing 4 changed files with 509 additions and 130 deletions.
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The library uses message queues to send/receive data and transmits when it's pos

Name | Type | Default | Description
--- | --- | --- | ---
`baudRate` | **BaudRate** | `BaudRate::BAUD_RATE_1` | Sets a specific baud rate.
`baudRate` | **BaudRate** | `LinkCable::BaudRate::BAUD_RATE_1` | Sets a specific baud rate.
`timeout` | **u32** | `3` | Number of *frames* without an `II_SERIAL` IRQ to reset the connection.
`remoteTimeout` | **u32** | `5` | Number of *messages* with `0xFFFF` to mark a player as disconnected.
`bufferSize` | **u32** | `30` | Number of *messages* that the queues will be able to store.
Expand Down Expand Up @@ -75,7 +75,7 @@ This tool allows sending Multiboot ROMs (small 256KiB programs that fit in EWRAM

Name | Return type | Description
--- | --- | ---
`sendRom(rom, romSize, cancel)` | **LinkCableMultiboot::Result** | Sends the `rom`. During the handshake process, the library will continuously invoke `cancel`, and abort the transfer if it returns `true`. The `romSize` must be a number between `448` and `262144`, and a multiple of `16`.
`sendRom(rom, romSize, cancel)` | **LinkCableMultiboot::Result** | Sends the `rom`. During the handshake process, the library will continuously invoke `cancel`, and abort the transfer if it returns `true`. The `romSize` must be a number between `448` and `262144`, and a multiple of `16`. Once completed, the return value should be `LinkCableMultiboot::Result::SUCCESS`.

⚠️ for better results, turn on the GBAs **after** calling the `sendRom` method!

Expand Down Expand Up @@ -140,6 +140,8 @@ Name | Return type | Description

This is a driver for an accessory that enables wireless games up to 5 players. The inner workings of the adapter are highly unknown, but [this article](docs/wireless_adapter.md) is very helpful. I've updated the blog post to add more details about the things I learnt by the means of ~~reverse engineering~~ brute force and trial&error.

The library, by default, implements a lightweight protocol on top of the adapter's message system. This allows detecting disconnections, forwarding messages to all nodes, and retransmitting to prevent packet loss.

![photo](https://user-images.githubusercontent.com/1631752/216233248-1f8ee26e-c8c1-418a-ad02-ad7c283dc49f.png)

## Constructor
Expand All @@ -148,12 +150,16 @@ This is a driver for an accessory that enables wireless games up to 5 players. T

Name | Type | Default | Description
--- | --- | --- | ---
`msgTimeout` | **u32** | `5` | Number of *`receive(...)` calls* without a message from other connected player to disconnect.
`forwarding` | **bool** | `true` | If `true`, the server forwards all messages to the clients. Otherwise, clients only see messages sent from the server (ignoring other peers).
`retransmission` | **bool** | `true` | If `true`, the library handles retransmission for you, so there should be no packet loss.
`maxPlayers` | **u8** | `5` | Maximum number of allowed players.
`msgTimeout` | **u32** | `5` | Timeout used by `receive(messages)`. It's the maximum number of *receive calls* without a message from other connected player to disconnect.
`multiReceiveTimeout` | **u32** | `1140` | An extra timeout used by `receive(messages, times)`. It's the maximum number of *vertical lines* without a message from anybody to disconnect *(228 vertical lines = 1 frame)*.
`bufferSize` | **u32** | `30` | Number of *messages* that the queues will be able to store.

## Methods

✔️ Most of the methods return a boolean, indicating if the action was successful. If not, the connection with the adapter is reset and the game needs to start again. All actions are synchronic.
✔️ Most of the methods return a boolean, indicating if the action was successful. If not, you can call `getLastError()` to know the reason. Usually, unless it's a trivial error (like buffers being full), the connection with the adapter is reset and the game needs to start again. You can check the connection state with `getState()`. All actions are synchronic.

Name | Return type | Description
--- | --- | ---
Expand All @@ -167,11 +173,18 @@ Name | Return type | Description
`getServerIds(serverIds)` | **bool** | Fills the `serverIds` vector with all the currently broadcasting servers.
`send(data)` | **bool** | Enqueues `data` to be sent to other nodes. Note that this data will be sent in the next `receive(...)` call.
`receive(messages)` | **bool** | Sends the pending data and fills the `messages` vector with incoming messages, checking for timeouts and forwarding if needed. This call doesn't block the hardware waiting for messages, it returns if there are no incoming messages.
`receive(messages, times)` | **bool** | Performs multiple `receive(...)` calls until successfully exchanging data a number of `times`. This can only be called if `retransmission` is on.
`receive(messages, times, cancel)` | **bool** | Like `receive(messages, times)` but accepts a `cancel` function. The library will continuously invoke it, and abort the transfer if it returns `true`.
`disconnect()` | **bool** | Disconnects and resets the adapter.
`getState()` | **LinkWireless::State** | Returns the current state (one of `LinkWireless::State::NEEDS_RESET`, `LinkWireless::State::AUTHENTICATED`, `LinkWireless::State::SERVING`, `LinkWireless::State::CONNECTING`, or `LinkWireless::State::CONNECTED`).
`getPlayerId()` | **u8** *(0~4)* | Returns the current player id.
`getPlayerCount()` | **u8** *(1~5)* | Returns the connected players.

⚠️ packet loss can occur, so always send the full game state or implement retransmission on top of this!

⚠️ the adapter can transfer a maximum of twenty 32-bit words at a time, and messages are often concatenated together, so keep things way below this limit (specially when `forwarding` is on)!
`canSend()` | **bool** | Returns `false` only if the next `send(...)` call would fail due to full buffers.
`getPendingCount()` | **u32** | Returns the number of outgoing messages ready to be sent. It will always be lower than `bufferSize`.
`didReceiveBytes()` | **bool** | Returns whether the last `receive(...)` call gathered any bytes or not.
`getLastError()` | **LinkWireless::Error** | If one of the other methods returns `false`, you can inspect this to know the cause. After this call, the last error is cleared.

⚠️ servers can send up to `19` words of 32 bits at a time!
⚠️ clients can send up to `3` words of 32 bits at a time!
⚠️ if `retransmission` is on, these limits drop to `14` and `1`!
⚠️ you can workaround these limits by doing multiple exchanges with `receive(messages, times)`!
9 changes: 7 additions & 2 deletions docs/wireless_adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,6 @@ Whenever either side expects something to be sent from the other (as SPI is alwa

* Send N 32 bit values to connected adapter.

⚠️ Each `SendData` can send up to 90 bytes (or 22 values).

⚠️ The first byte **is a header**, and it has to be correct. Otherwise, the adapter will ignore the command and won't send any data. The header is as follows:
- For hosts: the number of `bytes` that comes next. For example, if we want to send `0xaabbccdd` and `0x12345678` in the same command, we need to send:
* `0x00000008`, `0xaabbccdd`, `0x12345678`.
Expand All @@ -328,6 +326,11 @@ Whenever either side expects something to be sent from the other (as SPI is alwa
* The third client should send: `0x100000`, `0xaabbccdd`
* The fourth client should send: `0x2000000`, `0xaabbccdd`

⚠️ Each `SendData` can send up to:
- **Host:** 90 bytes (or 22 values)
- **Guests:** 16 bytes (or 4 values)
- *(the header doesn't count)*

⚠️ Note that when having more than 2 connected adapters, data is not transferred between different guests. If a guest wants to tell something to another guest, it has to talk first with the host with `SendData`, and then the host needs to relay that information to the other guest.

⚠️ The command "overrides" previous data, so if one node is using `ReceiveData`, but before the receive call the other node uses two consecutive `SendData`s, the receiving end will only get the last stream.
Expand All @@ -350,6 +353,8 @@ Whenever either side expects something to be sent from the other (as SPI is alwa
* Responds with all the data from all adapters. No IDs are included, this is just what was sent concatenated together.
* Once data has been pulled out, it clears the data buffer, so calling this again can only get new data.

⚠️ When the data is concatenated, only one **header** (see [SendData](#senddata---0x24)) is included at the first value of the response.

#### Wait - `0x27`

[![Image without alt text or caption](img/0x27.png)](img/0x27.png)
Expand Down
165 changes: 108 additions & 57 deletions examples/LinkWireless_demo/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
// (0) Include the header
#include "../../_lib/LinkWireless.h"

#define TRANSFERS_PER_FRAME 4

#define CHECK_ERRORS(MESSAGE) \
if ((lastError = linkWireless->getLastError())) { \
log(std::string(MESSAGE) + " (" + std::to_string(lastError) + ") [" + \
std::to_string(linkWireless->getState()) + "]"); \
hang(); \
linkWireless->activate(); \
return; \
}

void activate();
void serve();
void connect();
Expand All @@ -12,32 +23,57 @@ void log(std::string text);
void waitFor(u16 key);
void hang();

// (1) Create a LinkWireless instance
LinkWireless* linkWireless = new LinkWireless();
LinkWireless::Error lastError;
LinkWireless* linkWireless = NULL;
bool forwarding = true;
bool retransmission = true;

void init() {
REG_DISPCNT = DCNT_MODE0 | DCNT_BG0;
tte_init_se_default(0, BG_CBB(0) | BG_SBB(31));

irq_init(NULL);
irq_add(II_VBLANK, NULL);

// (2) Initialize the library
linkWireless->activate();
}

int main() {
init();

start:
// Options
log("Press A to start\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nhold LEFT on start:\n -> "
"disable forwarding\n\nhold UP on start:\n -> disable retransmission");
waitFor(KEY_A);
u16 initialKeys = ~REG_KEYS & KEY_ANY;
forwarding = !(initialKeys & KEY_LEFT);
retransmission = !(initialKeys & KEY_UP);

// (1) Create a LinkWireless instance
linkWireless = new LinkWireless(forwarding, retransmission);

// (2) Initialize the library
linkWireless->activate();

bool activating = false;
bool serving = false;
bool connecting = false;

while (true) {
u16 keys = ~REG_KEYS & KEY_ANY;

log("START = Activate\nL = Serve\nR = Connect\n\n (DOWN = ok)\n "
"(SELECT = cancel)");
// Menu
log(std::string("") +
"L = Serve\nR = Connect\n\n (DOWN = ok)\n "
"(SELECT = cancel)\n (START = activate)\n\n-> forwarding: " +
(forwarding ? "ON" : "OFF") + "\n" +
"-> retransmission: " + (retransmission ? "ON" : "OFF"));

// SELECT = back
if (keys & KEY_SELECT) {
linkWireless->deactivate();
linkWireless = NULL;
goto start;
}

// START = Activate
if ((keys & KEY_START) && !activating) {
Expand Down Expand Up @@ -84,11 +120,8 @@ void serve() {
log("Serving...");

// (3) Start a server
if (!linkWireless->serve()) {
log("Serve failed :(");
hang();
return;
}
linkWireless->serve();
CHECK_ERRORS("Serve failed :(")

log("Listening...");

Expand All @@ -101,11 +134,8 @@ void serve() {
return;
}

if (!linkWireless->acceptConnections()) {
log("Accept failed :(");
hang();
return;
}
linkWireless->acceptConnections();
CHECK_ERRORS("Accept failed :(")
} while (linkWireless->getPlayerCount() <= 1);

log("Connection accepted!");
Expand All @@ -118,11 +148,8 @@ void connect() {

// (4) Connect to a server
std::vector<u16> serverIds;
if (!linkWireless->getServerIds(serverIds)) {
log("Search failed :(");
hang();
return;
}
linkWireless->getServerIds(serverIds);
CHECK_ERRORS("Search failed :(")

if (serverIds.size() == 0) {
log("Nothing found :(");
Expand All @@ -135,14 +162,15 @@ void connect() {
log(str);
}

waitFor(KEY_START);

if (!linkWireless->connect(serverIds[0])) {
log("Connect failed :(");
hang();
waitFor(KEY_START | KEY_SELECT);
if ((~REG_KEYS & KEY_ANY) & KEY_SELECT) {
linkWireless->disconnect();
return;
}

linkWireless->connect(serverIds[0]);
CHECK_ERRORS("Connect failed :(")

while (linkWireless->getState() == LinkWireless::State::CONNECTING) {
u16 keys = ~REG_KEYS & KEY_ANY;
if (keys & KEY_SELECT) {
Expand All @@ -152,11 +180,8 @@ void connect() {
return;
}

if (!linkWireless->keepConnecting()) {
log("Finish connection failed :(");
hang();
return;
}
linkWireless->keepConnecting();
CHECK_ERRORS("Finish failed :(")
}

log("Connected! " + std::to_string(linkWireless->getPlayerId()));
Expand All @@ -170,35 +195,52 @@ void messageLoop() {
std::vector<u32> counters;
for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++)
counters.push_back(1 + i * 10);
bool sending = false;

bool sending = false;
bool packetLossCheck = false;
bool switching = false;

u32 lostPackets = 0;
u32 lastLostPacketPlayerId = 0;
u32 lastLostPacketExpected = 0;
u32 lastLostPacketReceived = 0;

while (true) {
u16 keys = ~REG_KEYS & KEY_ANY;

// (5) Send data
if ((keys & KEY_B) || (!sending && (keys & KEY_A))) {
if (linkWireless->canSend() &&
((keys & KEY_B) || (!sending && (keys & KEY_A)))) {
bool doubleSend = false;
sending = true;

again:
counters[linkWireless->getPlayerId()]++;
if (!linkWireless->send(
std::vector<u32>{counters[linkWireless->getPlayerId()]})) {
log("Send failed :(");
hang();
return;
linkWireless->send(
std::vector<u32>{counters[linkWireless->getPlayerId()]});
CHECK_ERRORS("Send failed :(")

if (!doubleSend && (keys & KEY_LEFT) && linkWireless->canSend()) {
doubleSend = true;
goto again;
}
}
if (sending && (!(keys & KEY_A)))
sending = false;

// (6) Receive data
std::vector<LinkWireless::Message> messages;
if (!linkWireless->receive(messages)) {
log("Receive failed :(");
hang();
return;
if (retransmission) {
// (exchanging data 4 times, just for speed purposes)
linkWireless->receive(messages, TRANSFERS_PER_FRAME, []() {
u16 keys = ~REG_KEYS & KEY_ANY;
return keys & KEY_SELECT;
});
} else {
// (exchanging data one time)
linkWireless->receive(messages);
}
CHECK_ERRORS("Receive failed :(")
if (messages.size() > 0) {
for (auto& message : messages) {
u32 expected = counters[message.playerId] + 1;
Expand All @@ -207,29 +249,24 @@ void messageLoop() {

// Check for packet loss
if (packetLossCheck && message.data[0] != expected) {
log("Wait... p" + std::to_string(message.playerId) + "\n" +
"\nExpected: " + std::to_string(expected) + "\nReceived: " +
std::to_string(message.data[0]) + "\n\npacket loss? :(");
linkWireless->disconnect();
hang();
return;
lostPackets++;
lastLostPacketPlayerId = message.playerId;
lastLostPacketExpected = expected;
lastLostPacketReceived = message.data[0];
}
}
}

// Accept new connections
if (linkWireless->getState() == LinkWireless::State::SERVING) {
if (!linkWireless->acceptConnections()) {
log("Accept failed :(");
hang();
return;
}
linkWireless->acceptConnections();
CHECK_ERRORS("Accept failed :(")
}

// (7) Disconnect
if ((keys & KEY_SELECT)) {
if (!linkWireless->disconnect()) {
log("Disconnect failed :(");
log("Disconn failed :(");
hang();
return;
}
Expand All @@ -240,19 +277,33 @@ void messageLoop() {
if (!switching && (keys & KEY_UP)) {
switching = true;
packetLossCheck = !packetLossCheck;
if (!packetLossCheck) {
lostPackets = 0;
lastLostPacketPlayerId = 0;
lastLostPacketExpected = 0;
lastLostPacketReceived = 0;
}
}
if (switching && (!(keys & KEY_UP)))
switching = false;

std::string output =
"Players: " + std::to_string(linkWireless->getPlayerCount()) +
"Player #" + std::to_string(linkWireless->getPlayerId()) + " (" +
std::to_string(linkWireless->getPlayerCount()) + " total)" +
"\n\n(press A to increment counter)\n(hold B to do it "
"continuously)\n\nPacket loss check: " +
"continuously)\n(hold LEFT for double send)\n\nPacket loss check: " +
(packetLossCheck ? "ON" : "OFF") + "\n(switch with UP)\n\n";
for (u32 i = 0; i < linkWireless->getPlayerCount(); i++) {
output +=
"p" + std::to_string(i) + ": " + std::to_string(counters[i]) + "\n";
}
output += "\n_buffer: " + std::to_string(linkWireless->getPendingCount());
if (packetLossCheck && lostPackets > 0) {
output += "\n\n_lostPackets: " + std::to_string(lostPackets) + "\n";
output += "_last: (" + std::to_string(lastLostPacketPlayerId) + ") " +
std::to_string(lastLostPacketReceived) + " [vs " +
std::to_string(lastLostPacketExpected) + "]";
}

// Print
VBlankIntrWait();
Expand Down
Loading

0 comments on commit 283ebab

Please sign in to comment.