@@ -304,16 +304,17 @@ private:
|
||||
};
|
||||
```
|
||||
|
||||
**Factory syntax — `in<>` / `out<>` tag structs:**
|
||||
**Factory syntax — `in<>` / `latch<>` / `out<>` tag structs:**
|
||||
|
||||
A flat name pack `make_node<f, "a", "b", "c">` is ambiguous (where do inputs end?).
|
||||
Option chosen: `in<...>` and `out<...>` tag types that wrap the name packs unambiguously.
|
||||
Both are optional; omitting either means those ports are index-only.
|
||||
Option chosen: `in<...>`, `latch<...>`, and `out<...>` tag types that wrap the name packs unambiguously.
|
||||
All are optional; omitting either means those ports are index-only.
|
||||
|
||||
```cpp
|
||||
// Tag types (trivial, no data):
|
||||
template<fixed_string... Names> struct in {};
|
||||
template<fixed_string... Names> struct out {};
|
||||
template<fixed_string... Names> struct in {};
|
||||
template<fixed_string... Names> struct latch {};
|
||||
template<fixed_string... Names> struct out {};
|
||||
|
||||
// Factory:
|
||||
// No names
|
||||
@@ -324,6 +325,9 @@ auto node = make_node<my_func, in<"img","sigma">>(10);
|
||||
|
||||
// Both input and output names
|
||||
auto node = make_node<my_func, in<"img","sigma">, out<"blurred","mask">>(10);
|
||||
|
||||
// Mixed synchronous and latched inputs
|
||||
auto node = make_node<my_func, in<"error">, latch<"setpoint">, out<"output">>(10);
|
||||
```
|
||||
|
||||
**Wrong name count is a compile error.** The `Node` class `static_assert`s that
|
||||
@@ -342,6 +346,83 @@ Multi-output functions must return `std::tuple<...>`. Single return accepted as-
|
||||
|
||||
---
|
||||
|
||||
## Component 4a — Latched Input Ports
|
||||
|
||||
### Motivation
|
||||
|
||||
Control and robotics applications naturally have two kinds of inputs at different update rates:
|
||||
|
||||
- **Synchronous inputs** (`in<>`) — the node must have fresh data on every fire. Typical for sensor readings that drive the computation (e.g. encoder RPM).
|
||||
- **Latched inputs** (`latch<>`) — the node uses the most recently received value, and does not block if no new value has arrived. Typical for setpoints or parameters that change infrequently relative to the control loop (e.g. bearing from a CV pipeline, PID gains).
|
||||
|
||||
Without latched ports, a node must block on all inputs simultaneously. This forces the control loop to run at the rate of the slowest input — unacceptable when a 1kHz encoder loop must wait for a 30Hz vision update.
|
||||
|
||||
### Semantics
|
||||
|
||||
A `latch<>` port:
|
||||
|
||||
1. **Does not block** if its channel is empty — it reuses the last successfully popped value.
|
||||
2. **Does block on first fire** — there is no meaningful "default" value, so the node waits until at least one value has arrived on each latched port before firing for the first time.
|
||||
3. **Consumes the value** when one is available (standard `pop()`), then holds it until the next value arrives.
|
||||
|
||||
The node fires whenever all `in<>` ports have data, using the last known value for each `latch<>` port.
|
||||
|
||||
### Implementation in `run_loop`
|
||||
|
||||
`run_loop` maintains a `std::tuple` of cached values, one slot per latched port. On each iteration:
|
||||
|
||||
```cpp
|
||||
// Synchronous ports — blocking pop (existing behaviour)
|
||||
auto sync_args = std::make_tuple(input<0>().pop(), input<1>().pop(), ...);
|
||||
|
||||
// Latched ports — non-blocking try_pop; keep cached value on miss
|
||||
try_pop(latch_cache_<I>, latch_channel_<I>); // updates cache if data available
|
||||
|
||||
// Call wrapped function with merged argument tuple
|
||||
auto result = std::apply(Func, merge(sync_args, latch_cache_));
|
||||
```
|
||||
|
||||
Latched channels are otherwise identical to synchronous channels: bounded FIFO, `shared_ptr` storage policy, same shutdown behaviour.
|
||||
|
||||
### Example — PID with live setpoint
|
||||
|
||||
```cpp
|
||||
// bearing arrives at ~30 Hz from CV; rpm arrives at ~1 kHz from encoder
|
||||
double pid_compute(double rpm, double bearing) { ... }
|
||||
|
||||
auto pid = make_node<pid_compute,
|
||||
in<"rpm">, // synchronous — blocks until fresh encoder tick
|
||||
latch<"bearing">, // latched — uses last known bearing from CV
|
||||
out<"pwm">
|
||||
>(8);
|
||||
|
||||
Network net;
|
||||
net.add("tacho", tacho_node)
|
||||
.add("tracker", tracker_node)
|
||||
.add("pid", pid)
|
||||
.connect("tacho", tacho_node.output<"rpm">(), "pid", pid.input<"rpm">())
|
||||
.connect("tracker", tracker_node.output<"bearing">(),"pid", pid.input<"bearing">())
|
||||
.build();
|
||||
```
|
||||
|
||||
The PID node fires at encoder rate. If no new bearing has arrived since the last tick, it reuses the previous one — correct behaviour for a control loop.
|
||||
|
||||
### Port Ordering Contract
|
||||
|
||||
`in<>` and `latch<>` ports together must cover all function parameters in declaration order. The `static_assert` on name count is extended to cover both tags jointly:
|
||||
|
||||
```cpp
|
||||
static_assert(
|
||||
sizeof...(InNames) + sizeof...(LatchNames) == input_count ||
|
||||
(sizeof...(InNames) == 0 && sizeof...(LatchNames) == 0),
|
||||
"make_node: in<> and latch<> names together must match function arity, or provide none"
|
||||
);
|
||||
```
|
||||
|
||||
The function parameter at position `i` is synchronous if `i` is in the `in<>` pack, latched if in the `latch<>` pack. Mixed ordering is allowed — the tag packs define which positions are latched, not a contiguous suffix.
|
||||
|
||||
---
|
||||
|
||||
## Component 5 — `network.hpp`: Graph Builder + Orchestrator
|
||||
|
||||
`Network` is **non-owning** — nodes are declared by the user and must outlive the network.
|
||||
@@ -997,3 +1078,4 @@ All major design questions are now closed:
|
||||
| Sub-networks | `Network` implements `INode`; `expose_input`/`expose_output` define boundary ports |
|
||||
| `make_py_network` | Pure C++ template; nanobind module recompilation is the registration step |
|
||||
| GIL strategy | Acquire per Python callback; release while blocking on channel ops |
|
||||
| Mixed-rate inputs | `latch<>` tag for ports that reuse last-seen value; blocks only on first fire; node fires at rate of `in<>` ports |
|
||||
|
||||
Reference in New Issue
Block a user