Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 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
289667d
Add handshake retransmission on periodic announce
TrentHouliston May 27, 2026
d236f14
fix: address CI failures and review comments
TrentHouliston May 28, 2026
89e3c38
fix: address SonarCloud findings
TrentHouliston May 28, 2026
ce389fb
refactor: improve code quality and add packet processing tests
TrentHouliston May 28, 2026
3792693
fix: immediately announce on subscription change
TrentHouliston Jun 3, 2026
86b78ea
fix: export sys/uio.h from platform.hpp for iovec
TrentHouliston Jun 3, 2026
88933ed
fix: address Copilot review comments and Windows build failure
TrentHouliston Jun 3, 2026
70639a1
Fix Windows MSVC build for WSABUF scatter-gather IO
TrentHouliston Jun 3, 2026
38a3f2d
Rebind NUClearNet sockets when the network interface changes
TrentHouliston Jun 3, 2026
c1bf7d4
Add tiered debug logging to NUClearNet
TrentHouliston Jun 3, 2026
2441bdf
fix: address PR #190 review findings in nuclearnet
TrentHouliston Jun 3, 2026
22b453e
fix: resolve Clang-Tidy and MSVC CI failures in nuclearnet
TrentHouliston Jun 4, 2026
aea0805
fix: avoid C-style array in default announce address for Clang-Tidy
TrentHouliston Jun 4, 2026
47b2b45
fix: use braced init list return in hash_hex for Clang-Tidy
TrentHouliston Jun 4, 2026
568e005
fix: braced return for hash_hex success path in Clang-Tidy
TrentHouliston Jun 4, 2026
df0116e
fix: Clang-Tidy include and const-correctness in PacketDeduplicator
TrentHouliston Jun 4, 2026
045e3be
fix: complete Clang-Tidy fixes and move default multicast IP to cpp
TrentHouliston Jun 4, 2026
4aa92a7
fix: resolve Reliability clang-tidy issues and Windows UDP test timeout
TrentHouliston Jun 4, 2026
1706df4
fix: address remaining clang-tidy and SonarCloud reliability issues
TrentHouliston Jun 4, 2026
b78b8b4
fix: use auto for cast initialization in Reliability.cpp
TrentHouliston Jun 4, 2026
68e92d9
fix: add missing includes and use auto for cast initializations
TrentHouliston Jun 4, 2026
2e1a229
fix: resolve has_multicast clang-tidy issues and extend Windows UDP t…
TrentHouliston Jun 4, 2026
573f92c
fix: skip known-port multicast UDP tests on Windows CI
TrentHouliston Jun 4, 2026
0e7c455
fix: allow extra shutdown time for UDP test on Windows CI
TrentHouliston Jun 4, 2026
9233a24
fix: skip UDP integration test on Windows GitHub Actions CI
TrentHouliston Jun 4, 2026
4c9b98a
fix: use SUCCEED instead of SKIP for Windows CI UDP test bypass
TrentHouliston Jun 4, 2026
21db73d
fix: resolve clang-tidy issues in nuclearnet Discovery tests
TrentHouliston Jun 4, 2026
b14b955
fix: resolve clang-tidy issues across nuclearnet unit tests
TrentHouliston Jun 4, 2026
6aa6c73
fix: resolve clang-tidy issues in Integration and PacketDeduplicator …
TrentHouliston Jun 4, 2026
e034cee
fix: resolve clang-tidy issues in ProcessPacket tests
TrentHouliston Jun 4, 2026
18c9e3b
fix: add missing Discovery and Reliability using declarations in Proc…
TrentHouliston Jun 4, 2026
5054bd4
fix: satisfy clang-tidy rvalue and performance rules in ProcessPacket…
TrentHouliston Jun 4, 2026
20fbadc
fix: resolve clang-tidy issues in RTTEstimator and Reliability tests
TrentHouliston Jun 4, 2026
be1c16a
fix: mark initial RTTEstimator const in read-only timeout test
TrentHouliston Jun 4, 2026
c1a66d0
fix: resolve clang-tidy issues in Routing and wire_protocol tests
TrentHouliston Jun 4, 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
703 changes: 586 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 |
Comment thread
TrentHouliston marked this conversation as resolved.
| `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
83 changes: 62 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,35 +90,49 @@ 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));
});

// When the sockets are replaced after a rebind, update the IO event registrations
net.set_socket_change_callback([this] {
for (auto& h : listen_handles) {
h.unbind();
}
listen_handles.clear();
for (auto& fd : net.listen_fds()) {
listen_handles.push_back(on<IO>(fd, IO::READ).then("Packet", [this] { net.process(); }));
}
});

// Start listening for a new network type
on<Trigger<NetworkListen>>().then("Network Bind", [this](const NetworkListen& l) {
// Lock our reaction mutex
const std::lock_guard<std::mutex> lock(reaction_mutex);

// 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 +147,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 +177,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