Note-cpp — a type-safe C++ library for the Notecard (community project)

Hi all :waving_hand:

I’ve been building note-cpp, a header-only C++ library for the Blues Notecard, and I’d love to share it with the community and get your feedback.

The goal was a Notecard API that feels natural in modern C++ — typed requests and responses, no macros-everywhere, and the same code from an ATmega328P all the way up to an ESP32 or a desktop host.

A quick taste:

#include <note.hpp>

Notecard nc;
nc.begin(Serial1, 9600);          // or nc.begin(Wire) for I2C

nc.hub.set()
    .product("com.example.app")
    .mode("periodic")
    .execute();

auto rsp = nc.card.version().execute();
if (rsp) {
    Serial.println(rsp.version);  // typed fields
} else {
    Serial.println(rsp.error());
}

A few things I think make it nice to use:

  • :puzzle_piece: Fully typed API for all 74 Notecard requests — fluent (nc.hub.set().product(...)) or plain assignment, with typed response fields instead of digging through JSON.
  • :feather: Header-only, zero dependencies, C++17/20/23 (later standards unlock more compile-time checks).
  • :chart_decreasing: Scales from tiny to large — the same API runs on an Arduino Uno (ATmega328P) all the way up through ESP32, Cortex-M, and Linux/macOS. On the Uno the fully-typed build lands around 80% of flash with zero heap — roughly flash-parity with hand-written note-c, and lighter on RAM — and if you need headroom, leaner call styles drop the same app to ~36% of flash. You dial memory and flash use without changing your call sites.
  • :shield: Catches mistakes at compile time — on C++20 it validates string constants like mode/triggers, and can constrain the API to your hardware variant + firmware version, and gives each request intent its own type (setting a field that doesn’t apply is a compile error).
  • :wrench: Your memory, your rules — arena (zero heap), HeapResetPool, plain malloc, std::pmr, or a custom allocator; the typed API is identical across all of them.
  • :electric_plug: Serial + I2C built in with CRC auto-detection, retries, segmentation, and binary transfer (card.binary.put/get) — plus an optional compact JSONB wire format for constrained targets.
  • :white_check_mark: Well tested — the same test cases run on host compilers and on real Notecard hardware over serial/I2C, with high coverage.

Your own structs work as note bodies for send, receive, and note.template registration:

struct Readings {
    float temperature;
    int16_t humidity;
    NOTE_FIELDS(temperature, humidity)   // optional on C++20+
};

nc.note.add().file("sensors.qo").body(Readings{22.5f, 60}).execute();

A note on scope: this is an independent community project — not affiliated with or supported by Blues, and “Notecard” is their trademark. It assumes you’re already comfortable with the Notecard and its API.

Repo, docs, and examples: GitHub - m-mcgowan/note-cpp: Type-safe C++ API for the Blues Notecard. Header-only, zero dependencies. C++17/20/23. · GitHub

I’d genuinely welcome feedback, bug reports, or “I tried it on X board” reports. Thanks for taking a look! :folded_hands:

Quick update since the first post — a couple of things worth sharing.

Installing it is easier now. note-cpp is in the Arduino Library Manager (search “note-cpp”) and the PlatformIO Registry:

; platformio.ini
lib_deps = m-mcgowan/note-cpp

Also, and this is the part I most wanted to call out, note-cpp can run alongside your existing note-c / note-arduino code on the same Notecard connection, so an existing solution can be migrated incrementally — one request at a time, with no all-or-nothing rewrite.

It works via a small bridge transport: note-c keeps owning the serial/I²C bus exactly as it does today, and note-cpp’s typed API rides on top of it by delegating each request to NoteRequestResponseJSON(). Your existing J* / NoteRequest code keeps working untouched while new code uses the typed API on the same wire:

// Existing note-c code — unchanged, still owns the bus:
J *req = NoteNewRequest("hub.set");
JAddStringToObject(req, "product", "com.example.app");
NoteRequest(req);

// New code, typed note-cpp API, same connection
// (nc = note-cpp's typed handle, riding the bridge):
nc.note.add().file("sensors.qo").body(reading).execute();

A typical migration flow goes like this:

  1. Add note-cpp in behind the bridge — keep all your existing setup and link both libraries. Nothing changes on the wire.
  2. Port request sites one at a time to the note-cpp typed API. Half-migrated is fine; both styles share the connection.
  3. Cut over the transport when you’re ready — swap the bridge for a non-bridged note-cpp transport and drop the note-c dependency. This step is optional; you can stay in bridge mode as long as you like, but once you do switch, you get the benefits of note-cpp’s streaming and a reduced memory footprint.

Full write-up, including the bridge implementation and the cut-over steps:

Still a community project, not affiliated with Blues — the goal is to complement note-c / note-arduino, not replace them. As always, feedback and issues are very welcome.