Skip to content

Commit ff59eca

Browse files
Address documentation review feedback
- Fix escaped \< in headings (data-stores, tcp-udp) - Fix log level usage: use log<WARN> inside reactors, NUClear::LogLevel::WARN outside - Expand logging how-to with system architecture, custom handlers, display_level - Fix 'thinking out loud' text in periodic-tasks tutorial - Add CSS fix for inline code angle bracket spacing - Expand networking how-to for broadcast/unicast/multicast modes - Fix reliable delivery: guarantees delivery but not order - Add watchdog tip: use Reactor type itself as group - Fix sync mermaid diagram: scheduler as first column - Add Always shutdown warning: don't block forever - Reorder tracing how-to: install TraceController before usage - Add thread context table to extending-dsl guide - Improve architecture explanation hierarchy
1 parent 8c73dad commit ff59eca

15 files changed

Lines changed: 224 additions & 46 deletions

File tree

‎docs2/explanation/architecture.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,14 @@ graph TB
129129
S -->|dispatch| TP3
130130
```
131131

132-
The **PowerPlant** is the container for the entire system. It:
132+
The hierarchy is straightforward:
133133

134-
- Holds all **Reactors** (your components)
135-
- Reactors register **Reactions** (event handlers declared with `on<>().then()`)
136-
- When data is emitted, the **Scheduler** creates tasks and dispatches them to **Thread Pools**
134+
- A **PowerPlant** is the top-level container for the entire system
135+
- A PowerPlant contains **Reactors** — your self-contained components
136+
- Each Reactor declares **Reactions** — event handlers registered with `on<>().then()`
137+
- When a Reaction runs, it can **emit messages** that trigger other Reactions
138+
139+
This creates a data-driven execution model: components don't call each other directly. Instead, they emit data, and the scheduler dispatches reactions in response.
137140

138141
## Design Philosophy
139142

‎docs2/how-to/extending-dsl.md‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ struct LogTiming {
5858
auto us = std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
5959

6060
if (us > 1000) { // Only log if > 1ms
61-
NUClear::log<NUClear::WARN>("Slow reaction:",
61+
NUClear::log<NUClear::LogLevel::WARN>("Slow reaction:",
6262
task.reaction->identifiers->name,
6363
"took", us, "µs");
6464
}
@@ -190,9 +190,26 @@ The Fusion Engine walks the inheritance tree and collects all extension points f
190190

191191
See [Extension Points Reference](../reference/extensions/extension-points.md) and [Fusion Engine](../reference/extensions/fusion-engine.md) for full details.
192192

193+
## Thread Context
194+
195+
Different extension points run in different thread contexts. This is critical to understand when using `thread_local` storage or sharing state:
196+
197+
| Point | Runs on | Notes |
198+
| -------------- | ------------------------------------------- | -------------------------------------------------- |
199+
| `bind` | The thread that calls `on<>()` | Usually the main thread during reactor construction |
200+
| `get` | The thread that **created** the task | Often different from the execution thread |
201+
| `precondition` | The thread that **created** the task | Same thread as `get` |
202+
| `pre_run` | The **execution** thread | Same thread as the callback |
203+
| `post_run` | The **execution** thread | Same thread as the callback |
204+
| `scope` | The **execution** thread | RAII object lives for callback duration |
205+
206+
!!! warning "thread_local in get vs pre_run/post_run"
207+
Because `get` runs on the task-creation thread (not the execution thread), `thread_local` variables set in `get` will **not** be visible in `pre_run`, `post_run`, or the callback itself. If you need per-execution state, use `pre_run`/`post_run` or the `scope` extension point, which provides RAII objects that persist for the lifetime of the reaction execution.
208+
193209
## Tips
194210

195211
- Words are never instantiated — delete the constructor to make this clear.
196-
- Use `thread_local` storage for per-execution state in `pre_run`/`post_run`.
212+
- Use `thread_local` storage for per-execution state in `pre_run`/`post_run` only — not in `get`.
213+
- Use the `scope` extension point if you need state that persists across the reaction execution with RAII semantics.
197214
- Template parameters on your word become compile-time configuration (like `RateLimit<10, seconds>`).
198215
- Test custom words the same way you test any reactor — single-threaded plant with assertions.

‎docs2/how-to/logging.md‎

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,97 @@ public:
135135

136136
!!! note "Default log level"
137137

138-
Reactors default to `log_level = INFO`. Set it in your constructor to change the threshold.
138+
Reactors default to `log_level = DEBUG`. Set it in your constructor to change the threshold.
139+
140+
## How the Log System Works
141+
142+
```mermaid
143+
sequenceDiagram
144+
participant R as Reactor
145+
participant P as PowerPlant
146+
participant H as Log Handler Reactions
147+
148+
R->>R: log<WARN>("message")
149+
Note over R: Check: WARN >= reactor.log_level?
150+
R->>P: Emit LogMessage
151+
P->>H: Trigger all on<Trigger<LogMessage>> reactions
152+
Note over H: Handler decides what to display/store
153+
```
154+
155+
The log system is entirely message-driven:
156+
157+
1. A `log<Level>(...)` call in a reactor checks whether `Level` meets the reactor's `log_level` threshold
158+
2. If it passes, a `LogMessage` is emitted into the system
159+
3. Any reaction bound to `Trigger<LogMessage>` receives it
160+
4. **Without a log handler installed, no output appears** — you must install a reactor that handles `LogMessage`
161+
162+
### Per-Reactor vs System Log Level
163+
164+
There are two filtering levels:
165+
166+
- **`log_level`** (per-reactor) — set in each reactor's constructor. Messages below this level are not emitted by that reactor.
167+
- **`min_log_level`** (system-wide) — set in `NUClear::Configuration`. Messages below this level are discarded regardless of the reactor's setting.
168+
169+
A message is emitted only if its level meets **both** thresholds.
170+
171+
### The `display_level` Field
172+
173+
Each `LogMessage` carries a `display_level` field equal to the emitting reactor's `log_level`. This allows handlers to implement display filtering — a handler can choose to only display messages where `msg.level >= msg.display_level`, letting each reactor control its own verbosity without affecting other reactors.
174+
175+
## Writing Custom Log Handlers
176+
177+
Since log handling is just a reaction to `LogMessage`, you can write handlers that log to files, send to a network service, buffer output, or anything else:
178+
179+
### File Logger
180+
181+
```cpp
182+
#include <nuclear>
183+
#include <fstream>
184+
185+
class FileLogger : public NUClear::Reactor {
186+
public:
187+
explicit FileLogger(std::unique_ptr<NUClear::Environment> environment) : Reactor(std::move(environment)) {
188+
189+
on<Trigger<NUClear::message::LogMessage>>().then([this](const NUClear::message::LogMessage& msg) {
190+
if (msg.level >= msg.display_level) {
191+
file << "[" << msg.level << "] "
192+
<< msg.reactor_name << ": "
193+
<< msg.message << "\n";
194+
file.flush();
195+
}
196+
});
197+
}
198+
199+
private:
200+
std::ofstream file{"application.log"};
201+
};
202+
```
203+
204+
### Filtered Handler
205+
206+
You can create handlers that only process certain severity levels:
207+
208+
```cpp
209+
class ErrorReporter : public NUClear::Reactor {
210+
public:
211+
explicit ErrorReporter(std::unique_ptr<NUClear::Environment> environment) : Reactor(std::move(environment)) {
212+
213+
on<Trigger<NUClear::message::LogMessage>>().then([this](const NUClear::message::LogMessage& msg) {
214+
if (msg.level >= ERROR) {
215+
// Send errors to external monitoring
216+
send_to_monitoring_service(msg.message);
217+
}
218+
});
219+
}
220+
};
221+
```
222+
223+
### Logging from Outside a Reactor
224+
225+
If you need to log from code that isn't inside a reactor (e.g., a utility function called from `main()`), use the free function with full qualification:
226+
227+
```cpp
228+
NUClear::log<NUClear::LogLevel::INFO>("Starting up...");
229+
```
230+
231+
This requires a PowerPlant to be running. The message will have no associated reactor name.

‎docs2/how-to/networking.md‎

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,57 @@ public:
5252
| Field | Type | Default | Description |
5353
| ------------------ | ---------- | ------------------ | ------------------------------------------------ |
5454
| `name` | `string` | — | Unique name for this node on the network |
55-
| `announce_address` | `string` | — | Multicast address for node discovery |
55+
| `announce_address` | `string` | — | Address for node discovery announcements |
5656
| `announce_port` | `uint16_t` | — | Port for announce messages |
5757
| `bind_address` | `string` | `""` (all) | Local interface to bind to |
58-
| `mtu` | `uint16_t` | `1500` | Maximum transmission unit (fragments if larger) |
58+
| `mtu` | `uint16_t` | `1500` | Maximum transmission unit (fragments if larger) |
5959

60-
The default multicast address `239.226.152.162` with port `7447` is a conventional choice for LAN discovery. All nodes that share the same announce address and port will discover each other.
60+
### Network Modes
61+
62+
NUClearNet supports several discovery modes depending on the `announce_address` you configure:
63+
64+
| Mode | Address Type | Example | Use Case |
65+
|------|-------------|---------|----------|
66+
| **Multicast IPv4** | `239.x.x.x` | `239.226.152.162` | LAN discovery, multiple nodes |
67+
| **Multicast IPv6** | `ff02::x` | `ff02::1` | IPv6 LAN discovery |
68+
| **Broadcast IPv4** | `x.x.x.255` | `192.168.1.255` | Simple LAN, all nodes on subnet |
69+
| **Unicast IPv4/IPv6** | Specific IP | `192.168.1.50` | Point-to-point, two nodes |
70+
71+
#### Multicast (Default)
72+
73+
Multicast is the most common mode. All nodes join a multicast group and discover each other automatically:
74+
75+
```cpp
76+
emit(std::make_unique<NUClear::message::NetworkConfiguration>(
77+
"my-node", "239.226.152.162", 7447));
78+
```
79+
80+
The default multicast address `239.226.152.162` with port `7447` is a conventional choice. All nodes that share the same announce address and port will discover each other.
81+
82+
#### Broadcast
83+
84+
For simpler networks, use a broadcast address. All nodes on the subnet will receive announce messages:
85+
86+
```cpp
87+
emit(std::make_unique<NUClear::message::NetworkConfiguration>(
88+
"my-node", "192.168.1.255", 7447));
89+
```
90+
91+
#### Unicast (Point-to-Point)
92+
93+
For direct connections between exactly two nodes, use unicast. Each node sets its announce address to the other node's IP:
94+
95+
```cpp
96+
// On Node A (IP: 192.168.1.10)
97+
emit(std::make_unique<NUClear::message::NetworkConfiguration>(
98+
"node-a", "192.168.1.20", 7447)); // Point to Node B
99+
100+
// On Node B (IP: 192.168.1.20)
101+
emit(std::make_unique<NUClear::message::NetworkConfiguration>(
102+
"node-b", "192.168.1.10", 7447)); // Point to Node A
103+
```
104+
105+
In unicast mode, each peer announces directly to the other. This is useful when multicast/broadcast is unavailable (e.g., across subnets or VPNs).
61106
62107
## 2. Send Messages
63108
@@ -180,7 +225,7 @@ public:
180225
| Mode | Behavior | Use when |
181226
| ------------ | ------------------------------------------------------------ | --------------------------------- |
182227
| Unreliable | Fire-and-forget. No retransmission. Lowest latency. | Streaming data, periodic updates |
183-
| Reliable | Retransmits until acknowledged. Ordered delivery guaranteed. | Commands, configuration, events |
228+
| Reliable | Retransmits until acknowledged. Delivery guaranteed. | Commands, configuration, events |
184229

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

‎docs2/how-to/synchronization.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ private:
7676

7777
```mermaid
7878
sequenceDiagram
79-
participant T1 as Task A (Sync<X>)
8079
participant S as Scheduler
80+
participant T1 as Task A (Sync<X>)
8181
participant T2 as Task B (Sync<X>)
8282
participant T3 as Task C (Sync<X>)
8383
84-
T1->>S: Start executing
84+
S->>T1: Execute
8585
T2->>S: Request execution
8686
Note over S: X is locked, queue B
8787
T3->>S: Request execution

‎docs2/how-to/tcp-udp.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> How to use raw TCP and UDP sockets to talk to non-NUClear systems.
44
5-
## When to Use This vs Network\<T\>
5+
## When to Use This vs Network&lt;T&gt;
66

77
| Feature | `Network<T>` | `TCP` / `UDP` |
88
|---------|-------------|---------------|

‎docs2/how-to/tracing.md‎

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@ NUClear includes a built-in tracing system that records reaction execution, sche
77
- The `TraceController` extension must be installed in your PowerPlant
88
- The output `.trace` file can be viewed in [Perfetto UI](https://ui.perfetto.dev/) or Chrome's `chrome://tracing`
99

10+
## Installing the TraceController
11+
12+
The `TraceController` must be installed before you can start tracing. Add it to your PowerPlant setup:
13+
14+
```cpp
15+
#include <nuclear>
16+
17+
int main(int argc, const char* argv[]) {
18+
NUClear::Configuration config;
19+
NUClear::PowerPlant plant(config, argc, argv);
20+
21+
// Install the trace controller
22+
plant.install<NUClear::extension::TraceController>();
23+
24+
// Install your reactors
25+
plant.install<MyApp>();
26+
27+
plant.start();
28+
}
29+
```
30+
1031
## Starting a Trace
1132
1233
To begin recording, emit a `BeginTrace` message. This opens a trace file and starts capturing all reaction events:
@@ -44,27 +65,6 @@ emit(std::make_unique<NUClear::message::EndTrace>());
4465
4566
This closes the trace file cleanly. If you don't emit `EndTrace`, the file will be closed when the PowerPlant shuts down, but may be incomplete.
4667
47-
## Installing the TraceController
48-
49-
The `TraceController` must be installed before you can start tracing. Add it to your PowerPlant setup:
50-
51-
```cpp
52-
#include <nuclear>
53-
54-
int main(int argc, const char* argv[]) {
55-
NUClear::Configuration config;
56-
NUClear::PowerPlant plant(config, argc, argv);
57-
58-
// Install the trace controller
59-
plant.install<NUClear::extension::TraceController>();
60-
61-
// Install your reactors
62-
plant.install<MyApp>();
63-
64-
plant.start();
65-
}
66-
```
67-
6868
## Viewing the Trace
6969
7070
Once you have a `.trace` file:

‎docs2/how-to/watchdog-timeouts.md‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Use `on<`[`Watchdog`](../reference/dsl/watchdog.md)`<Group, ticks, period>>()` t
3636
3737
```cpp
3838
on<Watchdog<HeartbeatMonitor, 5, std::chrono::seconds>>().then([this] {
39-
log<NUClear::WARN>("Heartbeat lost! Attempting recovery...");
39+
log<WARN>("Heartbeat lost! Attempting recovery...");
4040
emit(std::make_unique<RecoveryCommand>());
4141
});
4242
```
@@ -69,7 +69,7 @@ public:
6969

7070
// Fire if no heartbeat for 3 seconds
7171
on<Watchdog<HeartbeatMonitor, 3, std::chrono::seconds>>().then([this] {
72-
log<NUClear::WARN>("Sensor heartbeat lost!");
72+
log<WARN>("Sensor heartbeat lost!");
7373
emit(std::make_unique<RecoveryCommand>());
7474
});
7575

@@ -98,7 +98,7 @@ You can monitor multiple instances of the same group using a runtime argument. E
9898
```cpp
9999
// Monitor each motor independently
100100
on<Watchdog<MotorMonitor, 500, std::chrono::milliseconds>>(motor_id).then([this] {
101-
log<NUClear::WARN>("Motor", motor_id, "stopped responding");
101+
log<WARN>("Motor", motor_id, "stopped responding");
102102
});
103103

104104
// Service a specific motor's watchdog
@@ -144,3 +144,13 @@ Common period types: `std::chrono::milliseconds`, `std::chrono::seconds`, `std::
144144
- The watchdog starts timing from the moment `bind` is called (when the `on<>` statement runs). Service it early if you need a grace period at startup.
145145
- If the watchdog fires, the timer resets automatically — it will fire again after another timeout unless serviced.
146146
- Use specific group types to avoid accidentally servicing the wrong watchdog.
147+
- If a reactor only needs a single watchdog, you can use the reactor type itself as the group instead of creating a separate tag type:
148+
```cpp
149+
class SensorReader : public NUClear::Reactor {
150+
// ...
151+
on<Watchdog<SensorReader, 5, std::chrono::seconds>>().then([this] {
152+
log<WARN>("Sensor timeout!");
153+
});
154+
// Service with: emit<Scope::WATCHDOG>(ServiceWatchdog<SensorReader>());
155+
};
156+
```

‎docs2/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public:
2828

2929
on<Trigger<SensorData>>().then([this](const SensorData& data) {
3030
if (data.temperature > 100.0) {
31-
log<NUClear::LogLevel::WARN>("Temperature critical:", data.temperature);
31+
log<WARN>("Temperature critical:", data.temperature);
3232
}
3333
});
3434
}

‎docs2/reference/dsl/always.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public:
4545
!!! warning "CPU Spinning"
4646
If the callback returns without blocking or sleeping, the reaction will busy-spin and consume 100% CPU on its dedicated thread. Always ensure the callback performs blocking work or includes an explicit sleep/wait.
4747

48+
!!! warning "Don't block forever"
49+
The callback must periodically return or yield so that NUClear can shut down the system gracefully. If you block indefinitely (e.g., an infinite wait with no timeout), the PowerPlant cannot stop the thread when shutdown is requested. Use timeouts on blocking calls and check if the system is shutting down regularly.
50+
4851
- Always gets its own thread — it does not consume a thread from the default pool.
4952
- Good for: polling hardware, running event loops, blocking I/O loops.
5053
- Bad for: work that could be event-driven. Prefer [Trigger](trigger.md) or [Every](every.md) instead.

0 commit comments

Comments
 (0)