Type-safe C++ API for the Blues Notecard. Header-only, zero dependencies beyond the standard library. C++17, C++20, and C++23 — each unlocks additional features.
Community project. Not affiliated with or supported by Blues Inc. Notecard is a trademark of Blues Inc. Familiarity with the Notecard and its API is assumed.
#include <note.hpp>
Notecard nc;
nc.begin(Serial1, 9600); // serial — or nc.begin(Wire) for I2C
nc.hub.set()
.product("com.example.app")
.mode("periodic")
.execute();Install from GitHub: Sketch → Include Library → Add .ZIP Library and point to this repository's ZIP download.
lib_deps = https://github.com/m-mcgowan/note-cpp.gitadd_subdirectory(note-cpp)
target_link_libraries(my_app PRIVATE note-cpp)The typed API is the same on every platform. As an alternative to the fluent style shown above, you can also use assignment to set request properties.
auto req = nc.hub.set();
req.product = "com.example.app";
req.mode = "periodic";
req.outbound = 60_mins;
req.execute();Your own custom structs allow you to send and receive arbitrary bodies (say, with note.add). The same struct is used for sending and receiving notes, as well as for note.template registration.
struct Readings {
float temperature;
int16_t humidity;
NOTE_FIELDS(temperature, humidity) // optional on C++20
};
Readings readings{.temperature = 22.5f, .humidity = 60};
nc.note.add()
.file("sensors.qo")
.body(readings)
.execute();The same struct shape works for environment variables — see examples/stdcpp/env-vars.cpp for a worked example that reads env.get results into a typed DeviceConfig.
Responses carry typed fields and a truthy operator:
auto rsp = nc.card.version().execute();
if (rsp) {
Serial.println(rsp.version);
Serial.println(rsp.device);
} else {
Serial.println(rsp.error());
}Full walkthrough: docs/getting-started.md, with the same code as examples/stdcpp/getting-started.cpp. If you're migrating from note-arduino / note-c, the migration guide has side-by-side examples.
- Typed API — typed requests and responses for all 74 Notecard APIs; fluent or direct-assignment styles; on C++20 GCC the compiler validates string constants for fields like
modeortriggersat compile time, rejecting typos before the binary runs. - Focused operations — distinct types per intent for multi-purpose requests (
note.getvsnote.pop,card.location.mode.fixed()vs.get()); setting a non-applicable field is a compile error. - Zero-heap operation — pair the library with a
MonotonicArena(orHeapResetPool) and the entire send/receive cycle stays out ofmalloc. The same typed API works on an Arduino Uno with no heap at all, the arena lifecycle reclaims memory byreset()rather than per-allocation. - Body values and Note templates — one struct for send, receive, and template registration; plain aggregates work directly on C++20+, with
NOTE_FIELDS(...)available on C++17 or for non-aggregates. - Error handling — truthy responses on success, structured
ErrorInfoon failure; per-request safety classification (ReadOnly,Idempotent,NonIdempotent,Destructive) informs retry. - Target filtering (C++20) — constrain the available APIs by hardware variant (WiFi/Cell/Skylo/LoRa) and/or minimum firmware version; unsupported requests become compiler warnings, or errors in strict mode.
- Choice of streaming and tree modes — two JSON-parsing strategies; tree mode keeps a walkable
JsonReaderon the response, while streaming mode reads the wire directly into your typed struct with no tree in memory. - Memory control — you choose when and how memory is allocated, including fully heap-free allocation, such as where response strings live (primitive types like numbers and booleans appear directly in the responses): options include a static
MonotonicArena(zero heap, bounded RAM, predictable on every target), aHeapResetPool(malloc-backed with arena lifecycle), defaultmalloc/free,std::pmr, or a custom function-pointerAllocator. The same surface is used by all of them, so you can swap the allocator, the typed API does not change. - JSONB wire format — optional
NOTE_JSONBswaps JSON text for compact binary opcodes; reduces flash on constrained targets. (Enabled automatically on constrained targets.) - Wire protocols — Implements the expected wire protocol for Notecard. header-only serial (
SerialFramer) and I2C (I2cFramer) with CRC auto-detection, segmented TX/RX, retry, auto-reset and RTS/CTS transactions. Binary transfer requests (card.binary.put/get) uses COBS framing internally. - Duration units —
Minutes,Seconds,Hours,Dayswith safe implicit conversion to smaller units; wrong direction is a compile error.
C++ version compatibility matrix — what's available on C++17, what unlocks on C++20/23.
The library exposes three independent choices that compose freely. The typed API surface looks the same in every combination — pick each axis where it makes sense for your target, mix and match without changing call-site code.
graph LR
A["Wire format<br/>JSON · JSONB"] --- B["Response presentation<br/>streaming · tree"] --- C["Binary payload<br/>text-only · with-binary"]
- Wire format — JSON text or JSONB binary opcodes (
-DNOTE_JSONB=1). Smaller payloads on bandwidth-sensitive links; the typed API at the call site is byte-identical between the two. - Response presentation — streaming (SAX events directly into your typed
Responseand.into(struct)sinks; no tree held in memory) or tree (aJsonBackendassembles a walkableJsonReaderyou can query by key after the call). Picked by whichNotecardconstructor you call. - Binary payload —
card.binary.put()/.get()and large note bodies use a COBS-framed binary channel alongside the JSON/JSONB request channel. Works in every cell of the first-two-axes matrix.
The four combinations of wire format × presentation — JSON×streaming, JSON×tree, JSONB×streaming, JSONB×tree — all have host-side and on-device coverage. See docs/composition.md for the full matrix and the configuration-by-configuration breakdown, and examples/stdcpp/wire-format-and-presentation.cpp for a runnable demo where the same demo() body walks all four cells.
The library is built to scale from resource-constrained MCUs to desktop-class hardware. The same API surface can be used from ATmega328P (32 KB flash / 2 KB RAM) up to ESP32, Cortex-M, and desktop hosts. For additional optimization, you dial resource use by choosing how much of the stack to pull in.
| Target | Defaults | Recommended flags | Typical flash / RAM |
|---|---|---|---|
| AVR Uno (ATmega328P) | streaming, arena-backed (zero heap) | NOTE_MINIMAL (auto-enables NOTE_JSONB), JsonView / note::scan for responses |
10.9 – 24.3 KB / 680 – 836 B |
| Cortex-M0 / STM32 | streaming, arena-backed (zero heap) | NOTE_MINIMAL |
typed API fits comfortably |
| ESP32 / Cortex-M4+ | streaming with arena allocator | defaults | full typed API + body structs |
| Linux / macOS host | tree path with a JSON backend | cJSON or nlohmann backend |
full surface, heap allowed |
"Zero heap" on the constrained tiers means arena-backed — string
interning lands in a user-supplied MonotonicArena (or
HeapResetPool) and is reclaimed wholesale by arena.reset(). Plug
in the default note::Allocator{} (malloc/free) and you'll get a
heap; the typed API doesn't care which, but only arenas keep the
build genuinely heap-free.
Each row peels off one more layer; the typed API (rows 1–2) is what most users want, rows 3–5 trade some convenience for reduced flash use. All five styles share the same transport and can be mixed in one image — the compiler drops what you don't call.
| # | Style | Flash | Δ flash vs typed | RAM |
|---|---|---|---|---|
| — | note-c baseline (Notecard::requestAndResponse) |
25,076 B | −18 B | 729 B + 371 B heap |
| 1 | Typed API groups (api.hub.set().product(...).execute()) |
25,680 B | +586 B | 773 B + 0 B heap |
| 2 | Typed direct (nc.execute(HubSet{...})) |
25,094 B | baseline | 753 B + 0 B heap |
| 3 | Raw JSON + SAX sink (JsonBuf + transact_dispatch + JsonSink) |
21,754 B | −3,340 B | 781 B + 0 B heap |
| 4 | Raw + JsonView scan (RAM keys) |
11,790 B | −13,304 B | 695 B + 0 B heap |
| 5 | Raw + JsonView scan (F() flash keys) |
11,692 B | −13,402 B | 679 B + 0 bytes heap |
Rows 1–2 build with -DNOTE_NO_RESPONSE_RAII=1 (arena allocators don't need per-Response cleanup — see the AVR guide § Worked example). Without that flag rows 1–2 grow by ~2.2 KB.
Per-row code patterns: Arduino guide § Binary size comparison. Compile-time switches: docs/feature-flags.md. Benchmark harness: tools/binary-size-comparison/.
note-cpp ships full prose documentation alongside the headers. The four pointers below cover the most common entry points; docs/README.md is the full index.
- Start here: Getting started — top-down walkthrough from a clean project to your first request
- Migrating from note-arduino — side-by-side examples for common patterns
- Feature flags — compile-time options for binary size optimization (AVR, Cortex-M0)
- Full documentation index — all guides, from getting started to internals
- API reference (Doxygen) — generate locally with
./ci.sh --docsor use an IDE such as VS Code for inline docs and auto-completion of the API.
The same TEST_CASEs — over 2,000 of them — run on the host doctest binaries (under five compilers) and on real Notecard hardware over serial/I2C, plus a Wokwi-simulated ATmega328P runtime to catch Uno-specific init/stack issues. Host coverage stays high (95%+ lines) and is enforced in CI — see the coverage badge above. Docs are verified pre-push: every internal link resolves, every code snippet comes from a compiled source file, and the migration tables stay column-aligned by tooling.
Full breakdown: docs/quality-assurance.md.
Bug reports and PRs welcome via Issues and Pull Requests. See CONTRIBUTING.md for development setup, building, testing, and codegen.