Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6696c29
Redesign NUClearNet and add integration tests
TrentHouliston May 25, 2026
f536179
Rename nuclearnet paths and restore C++14 compatibility
TrentHouliston May 26, 2026
1df030f
Fix CI: clang-tidy const warning, stabilise timing-sensitive tests
TrentHouliston May 26, 2026
618b472
Make tests event-based by injecting time into Discovery, Reliability,…
TrentHouliston May 26, 2026
c5f1f92
Make multicast detection test actual packet delivery
TrentHouliston May 26, 2026
a443f6f
Fix clang-tidy errors and handle skipped tests in CI
TrentHouliston May 26, 2026
d6874e6
Update sonarqube-scan-action from v5 to v8
TrentHouliston May 26, 2026
7de9e2b
Fix CI failures and address review feedback
TrentHouliston May 26, 2026
2bc6910
Merge remote-tracking branch 'origin/main' into houliston/nuclearnet-v2
TrentHouliston May 27, 2026
7855152
docs: update networking documentation for NUClearNet v2
TrentHouliston May 27, 2026
1ee5f58
feat: reliable packets retransmit until peer disconnects
TrentHouliston May 27, 2026
8584669
refactor: remove DATA_RETRANSMISSION packet type and document announc…
TrentHouliston May 27, 2026
ad97beb
Make a clean triangle
TrentHouliston May 27, 2026
f37fdcc
Explain Acronyms
TrentHouliston May 27, 2026
0c9374a
Better lines
TrentHouliston May 27, 2026
4e52e5a
Improve context on data
TrentHouliston May 27, 2026
9989ec3
refactor: remove NACK packet type (dead code)
TrentHouliston May 27, 2026
3c475df
Implement two-flag connection model and multicast broadcast
TrentHouliston May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/sonarcloud.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
overwrite: true

- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v5
uses: SonarSource/sonarqube-scan-action@v8
if: ${{ !cancelled() }}
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand Down
659 changes: 542 additions & 117 deletions docs/explanation/nuclearnet.md

Large diffs are not rendered by default.

39 changes: 24 additions & 15 deletions docs/how-to/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ public:
};
```

### NetworkConfiguration Fields
### NetworkConfiguration fields

| Field | Type | Default | Description |
| ------------------ | ---------- | ---------- | ----------------------------------------------- |
| `name` | `string` | — | Unique name for this node on the network |
| `announce_address` | `string` | | Address for node discovery announcements |
| `announce_port` | `uint16_t` | | Port for announce messages |
| `bind_address` | `string` | `""` (all) | Local interface to bind to |
| `mtu` | `uint16_t` | `1500` | Maximum transmission unit (fragments if larger) |
| Field | Type | Default | Description |
| ------------------ | ---------- | ------------------- | ----------------------------------------------- |
| `name` | `string` | — | Unique name for this node on the network |
| `announce_address` | `string` | `"239.226.152.162"` | Address for node discovery announcements |
| `announce_port` | `uint16_t` | `7447` | Port for announce messages |
| `bind_address` | `string` | `""` (all) | Local interface to bind to |
| `mtu` | `uint16_t` | `1500` | Maximum transmission unit (fragments if larger) |

### Network Modes
### Network modes

NUClearNet supports several discovery modes depending on the `announce_address` you configure:

Expand Down Expand Up @@ -226,12 +226,12 @@ public:
};
```

## Reliable vs Unreliable Delivery
## Reliable vs unreliable delivery

| Mode | Behavior | Use when |
| ---------- | ---------------------------------------------------- | -------------------------------- |
| Unreliable | Fire-and-forget. No retransmission. Lowest latency. | Streaming data, periodic updates |
| Reliable | Retransmits until acknowledged. Delivery guaranteed. | Commands, configuration, events |
| Mode | Behavior | Use when |
| ---------- | --------------------------------------------------------------------------------- | -------------------------------- |
| Unreliable | Fire-and-forget. No retransmission. Lowest latency. | Streaming data, periodic updates |
| Reliable | Retransmits until acknowledged (ACK bitset). Uses Jacobson/Karels RTT estimation. | Commands, configuration, events |

Pass `true` as the reliability argument to `emit<Scope::NETWORK>`:

Expand All @@ -243,11 +243,20 @@ emit<Scope::NETWORK>(std::make_unique<Command>(cmd));
emit<Scope::NETWORK>(std::make_unique<Command>(cmd), true);
```

## Serialization Requirements
## Serialization requirements

Types sent over the network must be serializable.
NUClear handles this automatically for **trivially copyable** types (POD structs with no pointers or dynamic memory).

For complex types, specialize `NUClear::util::serialise::Serialise<T>` to provide custom `serialise()`, `deserialise()`, and `hash()` methods.

Type safety across nodes is ensured by hash matching — if a type's hash doesn't match between sender and receiver, the message is silently discarded.

## Subscription-based routing

NUClearNet automatically advertises which message types your node is interested in.
When you register an `on<Network<T>>` reaction, the type hash is added to your node's subscription set and announced to peers.
Peers will only send messages to your node if you are subscribed to that message type.

If a node has no subscriptions (no `on<Network<T>>` reactions), it receives all messages by default.
This is useful for debugging or gateway nodes that need to observe all traffic.
4 changes: 4 additions & 0 deletions docs/reference/dsl/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ sequenceDiagram
```

**Bind phase:** Emits a `NetworkListen` message with the type hash of `T` to register interest with the `NetworkController`.
The hash is also added to this node's subscription set, which is advertised to peers via announce packets.
Peers use this subscription information to avoid sending messages that no local reaction is listening for.

**Get phase:** Deserializes the message from `ThreadStore` data populated by `NetworkController`, using `Serialise<T>::deserialise()`.

Expand Down Expand Up @@ -90,6 +92,8 @@ on<Network<SensorReading>>().then([](const NetworkSource& src, const SensorReadi
- Only reacts to messages received over the network, never to local emits.
- The type hash is computed from the type name string — renaming a type breaks compatibility with peers using the old name.
- Multiple nodes can listen for the same type simultaneously.
- Registering a `Network<T>` reaction causes this node to advertise the type hash as a subscription,
enabling subscription-based routing so peers only send relevant messages.

## See Also

Expand Down
7 changes: 5 additions & 2 deletions docs/reference/emit/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ public:
- Requires `NetworkConfiguration` to be emitted for the network to be active.
- The type must be serializable: either trivially copyable, or provide a `util::serialise::Serialise<T>` specialization.
- Type routing uses a hash — the same type must be defined on both peers.
- If `reliable` is true, delivery is guaranteed (TCP-like semantics).
If false, packets may be lost (UDP-like).
- If `reliable` is true, the message uses ACK-based retransmission with Jacobson/Karels RTO estimation.
Retransmissions continue indefinitely until the peer acknowledges or disconnects.
If false, packets are fire-and-forget (UDP-like).
- If the target peer is not connected, the message is silently dropped even with `reliable = true`.
- Messages are only sent to peers that have subscribed to the type hash (subscription-based routing).
Peers with no subscriptions receive all messages by default.

## See Also

Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ configure_file(nuclear.in ${PROJECT_BINARY_DIR}/nuclear)

# Build the library
find_package(Threads REQUIRED)
file(GLOB_RECURSE src "*.c" "*.cpp" "*.hpp" "*.ipp")
file(GLOB_RECURSE src CONFIGURE_DEPENDS "*.c" "*.cpp" "*.hpp" "*.ipp")
add_library(nuclear STATIC ${src})
add_library(NUClear::nuclear ALIAS nuclear)

Expand Down
72 changes: 51 additions & 21 deletions src/extension/NetworkController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
#include <cstdint>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <utility>
#include <vector>

Expand All @@ -37,6 +39,8 @@
#include "../dsl/word/emit/Network.hpp"
#include "../message/NetworkConfiguration.hpp"
#include "../message/NetworkEvent.hpp"
#include "../nuclearnet/Discovery.hpp"
#include "../nuclearnet/NUClearNet.hpp"
#include "../util/get_hostname.hpp"

namespace NUClear {
Expand All @@ -52,12 +56,13 @@ namespace extension {
: Reactor(std::move(environment)) {

// Set our function callback
network.set_packet_callback([this](const network::NUClearNetwork::NetworkTarget& remote,
const uint64_t& hash,
const bool& reliable,
std::vector<uint8_t>&& payload) {
net.set_packet_callback([this](const network::NUClearNet::sock_t& source,
const std::string& peer_name,
uint64_t hash,
bool reliable,
std::vector<uint8_t>&& payload) {
// Construct our NetworkSource information
const dsl::word::NetworkSource src{remote.name, remote.target, reliable};
const dsl::word::NetworkSource src{peer_name, source, reliable};

// Move the payload in as we are stealing it
const std::vector<uint8_t> p(std::move(payload));
Expand Down Expand Up @@ -85,23 +90,23 @@ namespace extension {
});

// Set our join callback
network.set_join_callback([this](const network::NUClearNetwork::NetworkTarget& remote) {
net.set_join_callback([this](const network::PeerInfo& peer) {
auto l = std::make_unique<message::NetworkJoin>();
l->name = remote.name;
l->address = remote.target;
l->name = peer.name;
l->address = peer.address;
emit(l);
});

// Set our leave callback
network.set_leave_callback([this](const network::NUClearNetwork::NetworkTarget& remote) {
net.set_leave_callback([this](const network::PeerInfo& peer) {
auto l = std::make_unique<message::NetworkLeave>();
l->name = remote.name;
l->address = remote.target;
l->name = peer.name;
l->address = peer.address;
emit(l);
});

// Set our event timer callback
network.set_next_event_callback([this](std::chrono::steady_clock::time_point t) {
net.set_event_callback([this](std::chrono::steady_clock::time_point t) {
const std::chrono::steady_clock::duration emit_offset = t - std::chrono::steady_clock::now();
emit<Scope::DELAY>(std::make_unique<ProcessNetwork>(),
std::chrono::duration_cast<NUClear::clock::duration>(emit_offset));
Expand All @@ -114,6 +119,9 @@ namespace extension {

// Insert our new reaction
reactions.insert(std::make_pair(l.hash, l.reaction));

// Add subscription so peers know to send us this type
net.add_subscription(l.hash);
});

// Stop listening for a network type
Expand All @@ -128,13 +136,20 @@ namespace extension {
if (it != reactions.end()) {
reactions.erase(it);
}

// Rebuild subscriptions from remaining reactions
std::set<uint64_t> subs;
for (const auto& r : reactions) {
subs.insert(r.first);
}
net.set_subscriptions(subs);
});

on<Trigger<NetworkEmit>>().then("Network Emit", [this](const NetworkEmit& emit) {
network.send(emit.hash, emit.payload, emit.target, emit.reliable);
on<Trigger<NetworkEmit>>().then("Network Emit", [this](const NetworkEmit& e) {
net.send(e.hash, e.payload.data(), e.payload.size(), e.target, e.reliable);
});

on<Shutdown>().then("Shutdown Network", [this] { network.shutdown(); });
on<Shutdown>().then("Shutdown Network", [this] { net.shutdown(); });

// Configure the NUClearNetwork options
on<Trigger<NetworkConfiguration>>().then([this](const NetworkConfiguration& config) {
Expand All @@ -151,17 +166,32 @@ namespace extension {
listen_handles.clear();
}

// Name becomes hostname by default if not set
const std::string name = config.name.empty() ? util::get_hostname() : config.name;
// Build configuration
network::NetworkConfig net_config;
net_config.name = config.name.empty() ? util::get_hostname() : config.name;
net_config.announce_address = config.announce_address;
net_config.announce_port = config.announce_port;
net_config.bind_address = config.bind_address;
net_config.mtu = config.mtu;

// Collect current subscriptions
{
const std::lock_guard<std::mutex> lock(reaction_mutex);
std::set<uint64_t> subs;
for (const auto& r : reactions) {
subs.insert(r.first);
}
net.set_subscriptions(subs);
}

// Reset our network using this configuration
network.reset(name, config.announce_address, config.announce_port, config.bind_address, config.mtu);
net.reset(net_config);

// Execution handle
process_handle = on<Trigger<ProcessNetwork>>().then("Network processing", [this] { network.process(); });
process_handle = on<Trigger<ProcessNetwork>>().then("Network processing", [this] { net.process(); });

for (auto& fd : network.listen_fds()) {
listen_handles.push_back(on<IO>(fd, IO::READ).then("Packet", [this] { network.process(); }));
for (auto& fd : net.listen_fds()) {
listen_handles.push_back(on<IO>(fd, IO::READ).then("Packet", [this] { net.process(); }));
}
});
}
Expand Down
6 changes: 3 additions & 3 deletions src/extension/NetworkController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
#include "../PowerPlant.hpp"
#include "../Reactor.hpp"
#include "../message/NetworkConfiguration.hpp"
#include "../nuclearnet/NUClearNet.hpp"
#include "../util/get_hostname.hpp"
#include "network/NUClearNetwork.hpp"

namespace NUClear {
namespace extension {
Expand All @@ -42,8 +42,8 @@ namespace extension {
explicit NetworkController(std::unique_ptr<NUClear::Environment> environment);

private:
/// Our NUClearNetwork object that handles the networking
network::NUClearNetwork network;
/// Our NUClearNet object that handles the networking
network::NUClearNet net;

/// The reaction that handles timed events from the network
ReactionHandle process_handle;
Expand Down
Loading
Loading