Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 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
18c2877
Fix self-announce filtering, resolve iteration, and multicast loop
TrentHouliston Jun 17, 2026
c1f9619
Remove SO_REUSEPORT from announce socket and document loopback addresses
TrentHouliston Jun 18, 2026
d4c22e6
Enable macOS multi-bind with SO_REUSEPORT and document fan-out limits
TrentHouliston Jun 18, 2026
8f2c8e3
Pair SO_REUSEADDR and SO_REUSEPORT on all platforms where available.
TrentHouliston Jun 18, 2026
b7caa31
Route the NUClearNet logs through a settable handler
Aug 14, 2026
c619402
Dispatch network logs on an explicit level
Aug 14, 2026
2b5df7b
Document what each network log level adds
Aug 14, 2026
b83ca08
Fix data packets with an empty payload being rejected
Aug 14, 2026
ffd7231
Remove build artifacts committed by mistake
Aug 14, 2026
62bc83a
Stop one undeliverable packet becoming a permanent retransmit load
Aug 14, 2026
2053a37
Drop the peer instead of the packet when retries are exhausted
Aug 14, 2026
e689ffd
Stop a peer sharing our ephemeral port being mistaken for ourselves
Aug 14, 2026
9271193
Don't send to a peer before its handshake has completed
Aug 14, 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*.a

# Build & CMake files
build/
build*/
CMakeCache.txt
CMakeFiles
Makefile
Expand Down
768 changes: 648 additions & 120 deletions docs/explanation/nuclearnet.md

Large diffs are not rendered by default.

147 changes: 132 additions & 15 deletions docs/how-to/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,66 @@ 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 (see [Forming a mesh](#forming-a-mesh) — default `INADDR_ANY` is required for broadcast fan-out on macOS) |
| `mtu` | `uint16_t` | `1500` | Maximum transmission unit (fragments if larger) |
| `log_level` | `LogLevel` | `UNKNOWN` (off) | Level to log the networking internals at (see [Logging](#logging)) |

### Network Modes
### Logging

NUClearNet logs what it is doing internally — discovery, handshakes, fragmentation, retransmission.
Set `log_level` on the `NetworkConfiguration` to turn it on:

```cpp
emit(std::make_unique<NUClear::message::NetworkConfiguration>(
"alice", // Node name
"239.226.152.162", // Multicast announce address
7447, // Announce port
"", // Bind address
1500, // MTU
NUClear::LogLevel::DEBUG
));
```

This sets both the log level inside the NUClearNet library and the log level of the `NetworkController` reactor
that emits the messages, so the two cannot disagree.
The messages come out through the normal NUClear logging system as `LogMessage`s, so your existing log handlers
see them alongside everything else.
Leaving `log_level` as `UNKNOWN` disables the networking logs entirely, which is the default.

What each level adds:

| Level | What is logged |
| ------- | --------------------------------------------------------------------------------------------------------------------- |
| `INFO` | Startup and teardown: `reset` with the resolved name, announce address and MTU, and `shutdown` |
| `WARN` | Recoverable problems: packets that could not be sent or parsed, peers that could not be resolved |
| `DEBUG` | State changes: peers appearing and leaving, handshake transitions, subscription changes, dropped sends |
| `TRACE` | Per poll cycle instrumentation: `process` begin and end, announces sent, socket reads, and the next scheduled wake up |

`DEBUG` is event driven, so it stays quiet on an idle network.
`TRACE` fires on every call to `process()` regardless of whether anything happened, so it is several times noisier
again and is intended for debugging the networking itself.

When NUClearNet is used as a standalone library (without the reactor framework) the messages are written to
stderr instead. Call `NUClearNet::set_log_handler` to redirect them into your own logging system:

```cpp
NUClear::network::NUClearNet::set_log_level(NUClear::network::LogLevel::Debug);
NUClear::network::NUClearNet::set_log_handler(
[](NUClear::network::LogLevel level, const char* component, const std::string& message) {
my_logger.write(level, component, message);
});
```

Pass an empty handler to go back to the stderr default.

### Network modes

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

Expand All @@ -67,6 +116,8 @@ NUClearNet supports several discovery modes depending on the `announce_address`
| **Multicast IPv4** | `239.x.x.x` | `239.226.152.162` | LAN discovery, multiple nodes |
| **Multicast IPv6** | `ff02::x` | `ff02::1` | IPv6 LAN discovery |
| **Broadcast IPv4** | `x.x.x.255` | `192.168.1.255` | Simple LAN, all nodes on subnet |
| **Loopback broadcast** | `127.255.255.255` | `127.255.255.255` | Local dev on Linux only (see [Forming a mesh](#forming-a-mesh)) |
| **Loopback unicast** | `127.0.0.1` | `127.0.0.1` | Point-to-point between two nodes (not multi-peer on shared port) |
| **Unicast IPv4/IPv6** | Specific IP | `192.168.1.50` | Point-to-point, two nodes |

#### Multicast (Default)
Expand Down Expand Up @@ -109,6 +160,63 @@ emit(std::make_unique<NUClear::message::NetworkConfiguration>(

In unicast mode, each peer announces directly to the other.
This is useful when multicast/broadcast is unavailable (e.g., across subnets or VPNs).
Unicast does **not** fan out to every socket bound on the shared announce port, so it cannot form a multi-peer mesh on one host.

### Forming a mesh

A mesh forms when all nodes share the same `announce_address` and `announce_port`.
Each node periodically sends discovery packets to that address; every peer that receives them can discover the others and complete a CONNECT handshake.

For multi-peer discovery on one machine, announce traffic must reach **every** process bound to the shared announce port.
Which addresses satisfy that depends on the announce address and OS stack, not on socket option policy.

#### Socket binding

By default, NUClearNet binds the announce socket to **all interfaces** (`INADDR_ANY`), regardless of the announce address.
This default is required for broadcast fan-out on macOS.
Setting `bind_address` to a specific interface IP can prevent broadcast reception on macOS — leave it empty unless you have a specific reason to bind to one interface.

#### Reuse options

Multiple processes on one host must bind the same UDP announce port.
NUClearNet sets **`SO_REUSEADDR`** on all platforms and **`SO_REUSEPORT`** when the platform provides it — the two options are always paired where `SO_REUSEPORT` exists.
Socket setup is consistent everywhere; what varies is which announce addresses fan out to every bound socket vs one socket.

#### Valid announce addresses

| Address | Linux | macOS |
| ------- | ----- | ----- |
| `239.226.152.162` (multicast, default) | Valid — all sockets join the group | Valid — recommended |
| `127.255.255.255` (loopback broadcast) | Valid — recommended for local dev | **Invalid** — macOS does not deliver UDP to this address locally (stack limitation) |
| `192.168.x.255` (subnet broadcast) | Valid | Valid — requires default `INADDR_ANY` bind |
| `255.255.255.255` (global broadcast) | Valid — noisy | Valid — requires default `INADDR_ANY` bind |
| `127.0.0.1` or specific IP (unicast) | **Invalid** — one bound socket | **Invalid** — load-balanced to one bound socket |

```cpp
// Linux local dev — loopback broadcast fans out to every peer
emit(std::make_unique<NUClear::message::NetworkConfiguration>(
"my-node", "127.255.255.255", 7447));

// macOS local dev — use the default multicast address
emit(std::make_unique<NUClear::message::NetworkConfiguration>(
"my-node", "239.226.152.162", 7447));
```

#### Cross-platform summary

| Scenario | Recommended address |
| -------- | ------------------- |
| LAN, multiple machines | `239.226.152.162` (multicast) or subnet broadcast |
| Local dev, Linux | `127.255.255.255` (loopback broadcast) or multicast |
| Local dev, macOS | `239.226.152.162` (multicast) |
| Two known peers, point-to-point | Unicast to each other's IP (not multi-peer on shared port) |

#### Loopback (local development)

When running multiple NUClearNet processes on one machine, pick an announce address that fans out to every listener on the shared port (see tables above).

On Linux, `127.255.255.255` is the simplest local-dev choice when you want to avoid multicast.
On macOS, use the default multicast address — loopback broadcast is not delivered locally.

## 2. Send Messages

Expand Down Expand Up @@ -226,12 +334,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 +351,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
Loading