Library Packages

A library package is an npm package that adds native firmware capability to a Cuttlefish project. You install it with npm install, import it like any other TypeScript module, and the transpiler resolves the import into C++ — a shim (a thin C++ adapter file) compiled into your firmware, plus the build fragments the framework needs. On Zephyr those fragments are a devicetree overlay (Zephyr’s hardware-description file) and Kconfig lines (Zephyr’s build-configuration symbols).

The first library package is @typecad/zephyr-esp32s3-rgb, which drives the onboard addressable RGB LED of the ESP32-S3 DevKitC — a WS2812, an addressable LED often sold as a “NeoPixel”. This page uses it twice: first as a consumer, then as a complete walkthrough for building your own.

Using a Library Package

You need a Cuttlefish project on the Zephyr framework with an ESP32-S3 DevKitC target (for example, one created with cuttlefish create --target esp32s3_devkitc).

  1. Install the package.

    npm install @typecad/zephyr-esp32s3-rgb
  2. Import it and call it like any other module.

    import { rgbLed } from '@typecad/zephyr-esp32s3-rgb';
    
    export function setup(): void {
      rgbLed.color(0, 255, 0).show();                 // green
      rgbLed.color('#ff0000').show();                 // red, CSS-style hex
      rgbLed.brightness(64).color('#ffffff').show();  // dim white
      rgbLed.off();                                   // black + show
    }

    Colors buffer locally. Nothing reaches the LED until show() (or off()).

  3. Compile and upload.

    npm run compile
    npm run upload

    Watch for ✓ Done — that is the signal the import resolved through the library. Four things landed in your project:

    • #include "__tc_rgbled.h" in the generated source, with the shim files next to it.
    • CONFIG_LED_STRIP=y, CONFIG_I2S=y, CONFIG_DMA=y appended to prj.conf.
    • A ws2812 node appended to boards/esp32s3_devkitc.overlay.
    • The shim compiled and linked into zephyr.elf like any other source file.

A library declares the framework it targets. Importing a Zephyr library into a native (desktop) project stops the transpile with a message naming both frameworks — you find out in seconds, not at a C++ compiler error.

Finding Libraries

npm is the catalog — there is no separate registry to maintain. Every library package carries two keywords: the marker cuttlefish-library and one category keyword. The CLI scans the npm registry with those keywords.

Categories

CategoryKeywordCovers
LEDscuttlefish-ledLEDs, addressable strips (WS2812-class), LED drivers
Displayscuttlefish-displayTFT, OLED, e-ink panels and their drivers
Sensorscuttlefish-sensorEnvironmental, motion, IMU, light, distance
Actuatorscuttlefish-actuatorMotors, servos, steppers, relays
Communicationscuttlefish-commsRadios and networking beyond built-in WiFi/BLE (LoRa, GSM, CAN add-ons)
Storagecuttlefish-storageSD cards, flash chips, FRAM
Audiocuttlefish-audioMicrophones, amplifiers, codecs
Powercuttlefish-powerPMICs, battery gauges, chargers
Inputcuttlefish-inputKeypads, encoders, add-on touch
I/O Expanderscuttlefish-ioPort expanders, matrix drivers, 7-segment
Timingcuttlefish-timingRTCs, precision clocks, GPS time
Utilitycuttlefish-utilityPure-code helpers with no hardware of their own

Searching and Installing

  1. Browse every library, or narrow by category or free text.

    cuttlefish library search                  # everything published
    cuttlefish library search --category led   # one category
    cuttlefish library search ws2812           # free text within libraries

    Watch for the package list with names, versions, category labels, and an installed marker. --json gives the machine-readable form.

  2. Install into your project from its root.

    cuttlefish library install @typecad/zephyr-esp32s3-rgb

    Watch for Done — the package is in your dependencies and ready to import. This is an npm install under the hood, so version pinning and lockfiles behave exactly as you expect.

What’s in a Library Package

Five pieces, all ordinary files in one npm package:

PieceFileWhat it is
TypeScript APIsrc/index.tsThe typed surface you import. Never executed — it is the contract your editor sees.
Manifestcuttlefish.library.jsonDeclares the import, the framework, and the native artifacts.
Shimsshims/*.h, shims/*.cppThe C++ implementation, written into the generated sources.
Devicetree fragmentshims/*.overlayAppended to the generated overlay when the library is used.
Kconfigmanifest kconfigCONFIG_* lines appended to prj.conf when the library is used.

The division of labor is the point. The library owns the code and the hardware facts. The framework owns the merge — it already knows how to write prj.conf and overlays for its build system. You never touch either.

Scaffolding a Library

You don’t have to build the five pieces by hand. cuttlefish library init generates a complete, valid package and you replace the stubs.

  1. Scaffold from any directory.

    cuttlefish library init @acme/led-ring --framework zephyr --category led --targets esp32s3_devkitc

    Without flags it prompts: package name, framework, category, board targets. The scaffold lands in a directory named for the library id (led-ring).

  2. Check what you got.

    led-ring/
      package.json             # keywords, files, build script — publish-ready
      cuttlefish.library.json  # the manifest, with Zephyr kconfig + overlay slots
      src/index.ts             # the typed API skeleton
      shims/__tc_led_ring.h    # AUTOSAR C++14 stub that compiles as-is
      shims/__tc_led_ring.cpp
      shims/led-ring.overlay   # devicetree fragment skeleton (Zephyr)
      tests/library.test.ts    # starter AUTOSAR-strict test over the shim bytes
      README.md                # authoring checklist
  3. Validate as you go.

    cuttlefish library validate .

    Watch for Valid library package. — the validator checks the manifest schema, that every listed shim and overlay file exists, that the gate token appears in the include, that package.json carries the marker and category keywords, and that the shim bytes pass the AUTOSAR C++14 strict check. A fresh scaffold passes; each error message names the fix.

The walkthrough below fills in what each of these pieces means, using the RGB LED library as the worked example.

Walkthrough: the ESP32-S3 Onboard RGB LED

The native Zephyr path to this LED runs through a devicetree binding, pinmux wiring (the chip’s routing of controller signals to pins), DMA (Direct Memory Access, the hardware data mover), and three Kconfig symbols before the first blink. The library moves all of that into itself, paid once, so every project after it is three lines.

This walkthrough builds @typecad/zephyr-esp32s3-rgb from scratch.

1. Lay out the package

zephyr-esp32s3-rgb/
  package.json             # normal npm package, zero runtime dependencies
  tsconfig.json
  src/index.ts             # the TypeScript API
  cuttlefish.library.json  # the manifest
  shims/
    __tc_rgbled.h          # C++ class header
    __tc_rgbled.cpp        # C++ implementation
    tc-rgb.overlay         # devicetree fragment

In package.json, ship the manifest and shims with the code:

{
  "name": "@typecad/zephyr-esp32s3-rgb",
  "version": "1.0.0",
  "type": "module",
  "files": ["dist", "src", "shims", "cuttlefish.library.json"]
}

2. Write the TypeScript API

export class RgbLed {
  color(r: number, g: number, b: number): this;
  color(hex: string): this;
  color(rOrHex: number | string, g?: number, b?: number): this {
    return this;
  }
  brightness(scale: number): this { return this; }
  show(): this { return this; }
  off(): this { return this; }
}

export const rgbLed: RgbLed = new RgbLed();

Two rules shape this file:

  • Method names are load-bearing. The transpiler renders calls on imported objects verbatim — rgbLed.color(0, 255, 0).show() appears in the C++ exactly as written. The C++ class in the shim must define the same names, and the shim defines a global rgbLed instance to match the export.
  • Export a singleton, not a constructor. A bare new RgbLed() in user code renders as a C++ heap allocation, which the AUTOSAR C++14 coding standard (a strict embedded C++ ruleset the shims follow) forbids. A preconstructed instance sidesteps the heap entirely.

3. Declare the manifest

{
  "id": "zephyr-esp32s3-rgb",
  "module": "@typecad/zephyr-esp32s3-rgb",
  "framework": "zephyr",
  "targets": ["esp32s3_devkitc"],
  "include": ""__tc_rgbled.h"",
  "gateToken": "__tc_rgbled",
  "shims": [
    { "path": "shims/__tc_rgbled.h", "outName": "__tc_rgbled.h" },
    { "path": "shims/__tc_rgbled.cpp", "outName": "__tc_rgbled.cpp" }
  ],
  "kconfig": ["CONFIG_LED_STRIP=y", "CONFIG_I2S=y", "CONFIG_DMA=y"],
  "overlay": "shims/tc-rgb.overlay"
}
FieldWhat it does
moduleThe import specifier this library answers to.
frameworkThe one framework it supports. A mismatch stops the transpile with a clear message.
targetsOptional. Build-target prefixes; a build for a different board stops the transpile the same way.
includeThe #include line emitted for the import.
gateTokenA token that must appear in the emitted sources before any artifact is written — an installed-but-unused library contributes nothing.
shimsFiles copied into the generated source directory.
kconfigLines appended to prj.conf. Your own zephyr.kconfig overrides still win.
overlayA devicetree fragment appended to the generated overlay.

A library package is framework-specific by design. A native counterpart of this library would be its own package with "framework": "native" and a different shim — the mechanism does not ask one package to serve every framework.

4. Write the shim

The shim is a plain C++ class. It buffers one pixel and pushes it with Zephyr’s LED-strip driver:

// __tc_rgbled.h
#ifndef TC_RGBLED_H_
#define TC_RGBLED_H_

#include <cstdint>
#include <zephyr/device.h>
#include <zephyr/drivers/led_strip.h>

class RgbLed final
{
public:
  RgbLed& color(std::uint8_t r, std::uint8_t g, std::uint8_t b);
  RgbLed& color(const char* hex);
  RgbLed& brightness(std::uint8_t scale);
  RgbLed& show();
  RgbLed& off();
private:
  struct led_rgb pixel_{};
  std::uint8_t scale_ = 255U;
};

extern RgbLed rgbLed;

#endif  // TC_RGBLED_H_
// __tc_rgbled.cpp (the one line that touches hardware)
RgbLed& RgbLed::show() {
  struct led_rgb out{};
  out.r = channel_scaled(pixel_.r, scale_);
  out.g = channel_scaled(pixel_.g, scale_);
  out.b = channel_scaled(pixel_.b, scale_);
  static_cast<void>(led_strip_update_rgb(
    DEVICE_DT_GET(DT_ALIAS(led_strip)), &out, 1U));
  return *this;
}

Shims must pass --autosar=strict — fixed-width integers, static_cast instead of C-style casts, no heap, final on leaf classes. Write them that way from the first line; the compliance check treats library shims exactly like framework shims.

5. Ship the devicetree fragment

The overlay fragment describes the LED to Zephyr: a WS2812 on GPIO48, clocked by the I2S0 peripheral (the chip’s digital-audio bus, which happens to be a precise waveform generator), wired in the part’s GRB channel order (green-red-blue — the order the LED samples color bytes off the wire):

/ {
	aliases {
		led-strip = &led_strip;
	};
};

&i2s0_default {
	group1 {
		pinmux = <I2S0_O_SD_GPIO48>;
	};
};

i2s_led: &i2s0 {
	status = "okay";

	dmas = <&dma 3>;
	dma-names = "tx";

	led_strip: ws2812@0 {
		compatible = "worldsemi,ws2812-i2s";
		reg = <0>;
		chain-length = <1>;
		color-mapping = <2 1 3>; /* GRB */
		reset-delay = <500>;
	};
};

This fragment is adapted from Zephyr’s own sample overlay for this board, which is the right instinct: when upstream shows the canonical wiring for a part, copy it and say so in a comment. The hardware fact worth documenting is the pin — DevKitC v1.0 puts the LED on GPIO48, v1.1 moved it to GPIO38, and a revision swap is a one-line change to the pinmux.

6. Publish and install

The package has zero runtime dependencies — the transpiler reads the manifest and the shims as files. Publish with npm publish, and anyone with a matching project can npm install it and import it. Because the package.json carries the marker (cuttlefish-library) and category (cuttlefish-led) keywords, it appears in cuttlefish library search the moment it is indexed. Until it is published, a workspace or file: dependency works the same way.

How the Transpiler Wires It Up

⚙️ Advanced details — registration, gating, and the sidecar

Nothing scans node_modules. When the transpile graph resolves an import and the package root contains cuttlefish.library.json, the package registers at that moment and its TypeScript is skipped — the package’s own type declarations are the compile-time contract. The import then emits the manifest’s include instead of a transpiled-module header.

After codegen, the transpiler scans the emitted sources for each registered library’s gateToken. For every library whose token is present, it writes the shims next to the emitted sources and records the library in a libraries.json sidecar — the same convention as the board-constants sidecar. The framework’s build step reads that sidecar to merge kconfig lines into prj.conf and append the overlay fragment to the generated overlay. Removing the import removes the shims on the next build; an installed library that is never imported writes nothing.

Checklist for a New Library

  • One framework per package; name it for the framework and the part (zephyr-esp32s3-rgb).
  • TypeScript method names match the C++ shim names exactly; calls render verbatim.
  • Export a singleton instance, not a constructor.
  • Shims pass cuttlefish library validate (AUTOSAR C++14, strict) from the first line.
  • files in package.json includes the shims and the manifest.
  • Keywords include the marker (cuttlefish-library) and one category (cuttlefish-<id>).
  • Document the board facts you carry — pin, hardware revisions, which peripheral the library borrows (this one uses I2S0 and a DMA channel, which nothing else in a Cuttlefish Zephyr project claims).
  • When upstream firmware SDKs show a canonical wiring for your part, adapt their overlay and credit it in a comment.