Skip to content
neuralSPOT-X
User guide
HELIA HUB

Custom modules

A custom module is an ordinary NSX module that NSX’s registry does not pin. It has the same manifest, the same CMake contract and the same resolution behavior as a packaged one. The only difference is that you say where it comes from.

Write one when you have firmware you want to reuse across apps or share with a colleague. For a change you only need in one app, plain CMake in that app’s CMakeLists.txt is less work.

Terminal window
nsx module init my-sensor --type algorithm --summary "Accelerometer feature extraction"

That writes a module directory with a manifest, CMake, a header and a source stub, ready to build. The flags seed the manifest: --type picks the module class, --dependency, --board, --soc and --toolchain are each repeatable and declare what the module needs and what it claims to work with. Compatibility defaults to * for boards and SoCs and to arm-none-eabi-gcc for toolchains, so tighten those to what you have actually built.

The eight module types are algorithm, backend_specific, board, portable_api, runtime, sdk_provider, soc and tooling. The module model explains what each one means and which rules NSX enforces per type.

nsx-module.yaml is the contract. Every field, required and optional, is on the generated nsx-module.yaml reference, so this page only covers the parts that catch people out.

  • build.cmake.package and build.cmake.targets are how the rest of the build reaches your code. The package name is the module name with hyphens turned into underscores: my-sensor becomes my_sensor. The conventional target is the package name under the nsx:: namespace, with an nsx_ prefix dropped if present: my-sensor gives nsx::my_sensor, nsx-timer gives nsx::timer. nsx module init writes both for you.
  • depends.required is resolved; depends.optional is not. Optional dependencies are searchable metadata that tell a reader what your module can work with. Nothing installs them, but when an app adds one, NSX adds it to the build before your module, so your CMakeLists.txt can check if(TARGET nsx::<dep>). This applies to registry modules; NSX does not read a local or vendored module’s optional dependencies when ordering.
  • compatibility is enforced. NSX rejects an app that pairs your module with a board, SoC or toolchain the manifest does not list. Declaring * everywhere makes the check useless and makes the catalog page misleading.
  • The semantic fields are how anyone finds you. summary, capabilities, use_cases, anti_use_cases, agent_keywords, example_refs and composition_hints are optional, and they are what nsx module search matches against and what the catalog page renders. anti_use_cases is worth writing: saying what a module is wrong for saves more time than another sentence about what it is right for.

Check your work before an app ever sees it:

Terminal window
nsx module validate ./my-sensor
nsx module validate ./my-sensor --json

Your module declares a package and one or more targets; NSX generates the wiring that makes them findable and links them into the app. Keep the target’s public include directories and link dependencies honest, because that is the only description the app has of how to consume you.

Dependencies belong in both places. The manifest tells NSX what to resolve and vendor; the CMake target_link_libraries tells the compiler and linker what to use. Declaring a dependency in only one of the two produces either a missing source tree or a missing symbol, depending on which half you forgot.

Three ways, by how much you want NSX to manage.

Terminal window
nsx module add my-sensor --local --path ../my-sensor

nsx.yml records a source.path. Every nsx sync mirrors that directory into modules/my-sensor/, so your edits show up in the next build. nsx.lock records a content hash of the source directory, which means --frozen will notice when it changes. This is the mode for developing a module and an app together.

Terminal window
nsx module add my-sensor --vendored

modules/my-sensor/ becomes yours: committed to your repository, never re-fetched, never overwritten by nsx sync. The lock records only a content hash so drift is still detectable. Use this when the app must build with no network and no second repository.

A repository, or a fork of a packaged module

Section titled “A repository, or a fork of a packaged module”
Terminal window
nsx module register my-sensor \
--metadata ../my-sensor/nsx-module.yaml \
--project my-sensor \
--project-url https://github.com/you/my-sensor.git \
--project-revision v0.1.0

This writes an app-local registry entry under module_registry in nsx.yml and vendors the module in. From then on it resolves like any registry module: pinned to a revision, re-fetched by nsx sync, reported by nsx outdated.

The same command with --override replaces where a module NSX already pins comes from, which is how you test a fork of a packaged module without changing NSX. App-local entries take precedence over the packaged registry, and the override lives in your app’s manifest, so anyone cloning the app gets the fork too. --project-local-path vendors from a filesystem path instead of a git URL, and --dry-run shows the manifest change without writing it.

  • No dependency cycles. Dependency cycle detected at module '<name>' is a manifest problem, not an app problem.
  • At most one sdk_provider module in an app. See SDK foundation and overrides.
  • A board module depends on exactly one SoC module.
  • Compatibility is checked at resolution time, so a mismatch fails nsx lock, not the compiler.

Getting a module into the packaged registry, so that other people’s apps can add it by name, is a change to NSX itself. See Contribute a module.