CLER
Compile Time DSP Flowgraph for SDRs and Embedded Systems
Why CLER?
Embedded devices traditionally relied on dedicated chips for DSP — fusion, filtering, modulation. But with today's powerful SoCs and rise of agentic AI, it's often faster, cheaper, and more flexible to move DSP into software.
Existing frameworks like GNU Radio are battle-tested but require significant expertise and development time. Their runtime flexibility comes with overhead that makes embedded deployment challenging, and their large filesystem footprint doesn't fit today's AI context windows for modern development workflows.
CLER bridges this gap with a hybrid approach. On desktop systems, it leverages advanced techniques like doubly-mapped buffers for zero-copy performance that can match established frameworks. On embedded systems, it gracefully falls back to lightweight alternatives, maintaining deterministic behavior even without an MMU. Using compile-time C++ templates, the same codebase achieves optimal performance across platforms while keeping the core <1000 lines — perfect for AI-assisted development.
Key Design Principles
🔗 Variadic Outputs
Blocks are completely unconstrained by amount of channels or types, via template parameters. Supports cyclic graphs for control systems.
🎯 Optimized Schedulers
ThreadPerBlock (default, simple, debuggable) and FixedThreadPool (cache-optimized, better for constrained systems) with platform-aware memory layout.
⚡ High-Performance I/O
Doubly-mapped buffers for zero-copy performance on desktop, with graceful fallback to lightweight alternatives on embedded systems.
🤖 AI-Friendly Core
<1000 lines of core code fits perfectly in AI context windows, making it easy to get help with development.
Hello World - Signal Generation & Plotting
Here's a complete example that generates two sine waves, adds them together, and plots the result:
#include "cler.hpp"
#include "cler.hpp"
#include "task_policies/cler_desktop_tpolicy.hpp"
#include "desktop_blocks/sources/source_cw.hpp"
#include "desktop_blocks/utils/throttle.hpp"
#include "desktop_blocks/math/add.hpp"
#include "desktop_blocks/plots/plot_timeseries.hpp"
#include "desktop_blocks/gui/gui_manager.hpp"
int main() {
cler::GuiManager gui(800, 400, "Hello World Plot Example");
const size_t SPS = 1000;
SourceCWBlock source1("CWSource", 1.0f, 1.0f, SPS); //amplitude, frequency
SourceCWBlock source2("CWSource2", 1.0f, 20.0f, SPS);
ThrottleBlock throttle("Throttle", SPS);
AddBlock adder("Adder", 2); // 2 inputs
PlotTimeSeriesBlock plot(
"Hello World Plot",
{"Added Sources"},
SPS,
3.0f // duration in seconds
);
plot.set_initial_window(0.0f, 0.0f, 800.0f, 400.0f); //x,y, width, height
auto flowgraph = cler::make_desktop_flowgraph(
cler::BlockRunner(&source1, &adder.in[0]),
cler::BlockRunner(&source2, &adder.in[1]),
cler::BlockRunner(&adder, &throttle.in),
cler::BlockRunner(&throttle, &plot.in[0]),
cler::BlockRunner(&plot)
);
flowgraph.run();
while (gui.should_close() == false) {
gui.begin_frame();
plot.render();
gui.end_frame();
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
return 0;
}
This creates a flowgraph with lock-free channels. The BlockRunner syntax connects blocks: first argument is the block, remaining arguments are its outputs (passed as parameters to the block's procedure() method).
Try the Examples
git clone https://github.com/cariboulabs/cler.git
cd cler
mkdir build && cd build
cmake ..
make -j"$(nproc --ignore=1)" # Use all cores-1
cd desktop_examples
./hello_world # Basic signal processing
./mass_spring_damper # Interactive control system
CLER (core) is a header-only library — just include cler.hpp
plus a platform task policy header and you are ready to go! Or more idiomatic, you can link against cler::cler
or cler::desktop_blocks
.