opener — Project Layout
- Date:
2026-04-22
- Status:
draft
- Version:
0.1
opener — Project Layout
This document describes the source tree organisation for the opener project. The repository is a Zephyr module (consumable via west) that also ships the Linux DKMS host kernel module and the shared RPC protocol headers that are compiled into both.
Monorepo rationale
The Zephyr firmware, the Linux DKMS kernel module, and the shared RPC protocol headers are kept in a single repository for the following reasons.
Protocol header consistency. The RPC wire protocol is defined once in include/opener/rpc/. Both build systems consume the same header files directly from the repository tree. There is no publication step, no version negotiation, and no risk of the Zephyr firmware and the Linux kernel module disagreeing on frame layout, magic bytes, module IDs, or CRC parameters.
Independent build systems with no coupling. Zephyr uses CMake + west; the Linux kernel module uses Kbuild + DKMS. The two build systems are entirely independent: a Kbuild failure in linux/ does not block a Zephyr west build, and vice versa. Developers working only on the firmware side are never forced to have a kernel build environment set up, and kernel module developers are never forced to install the full nRF Connect SDK.
Companion development. When a new RPC procedure is added to the Zephyr server, the corresponding Linux kernel proxy is written in the same commit and reviewed together. This keeps the protocol and both implementations in step without relying on cross-repository coordination.
Reference host implementation. The Linux kernel module is the canonical example of how to integrate opener into a host OS. It demonstrates the full host-side contract: SPI transport, IRQ-driven receive, net_device registration, Generic Netlink UAPI, and per-module RPC proxy pattern. Any future port to another OS (e.g. a bare-metal host or a Windows driver) can use it as a specification by example.
1. Repository Root
opener/
├── zephyr/
│ └── module.yml # Zephyr module manifest
├── west.yml # optional: self-contained west manifest for development
├── CMakeLists.txt # Zephyr module root CMakeLists
├── Kconfig # module-level Kconfig (sources subsys/ and lib/)
│
├── include/ # public headers — Zephyr stack API + shared RPC protocol
├── subsys/ # DECT NR stack subsystems (Zephyr)
├── lib/ # RPC transport library (Zephyr server side)
├── drivers/ # SPI RPC slave transport driver (Zephyr)
├── boards/ # board definitions and DTS overlays
├── dts/ # devicetree bindings
├── samples/ # ready-to-build sample applications
└── linux/ # Linux host-side code (DKMS kernel module + tools)
2. Zephyr Module Manifest
zephyr/module.yml
name: opener
build:
cmake: .
kconfig: Kconfig
boards-root: .
dts-root: .
west-commands: scripts/west_commands.py # optional west extensions (flash, debug)
The cmake and kconfig keys point to the repository root so that the Zephyr build system picks up CMakeLists.txt and Kconfig automatically when the module is listed in the west workspace.
CMakeLists.txt (root, Zephyr-managed build only):
cmake_minimum_required(VERSION 3.20)
# Make the shared RPC protocol headers available to all Zephyr targets.
zephyr_include_directories(include)
add_subdirectory(subsys/opener)
add_subdirectory(lib/opener_rpc)
add_subdirectory(drivers/spi_rpc_slave)
Kconfig (root):
rsource "subsys/opener/Kconfig"
rsource "lib/opener_rpc/Kconfig"
rsource "drivers/spi_rpc_slave/Kconfig"
3. include/ — Public Headers
include/
└── opener/
├── rpc/ # ── SHARED with Linux kernel module ──
│ ├── frame.h # wire frame layout: MAGIC, header struct, constants
│ ├── module_ids.h # MOD_ID and PROC_ID enumerations
│ ├── error_codes.h # RPC-level error codes
│ └── crc16_ccitt.h # CRC-16/CCITT inline (portable, no OS dependency)
│
├── phy.h # HAL abstraction (struct opener_hal_ops)
├── mac.h # MAC public API (struct opener_mac_ops, assoc, sched)
├── dlc.h # DLC public API (struct opener_dlc_ops, DLC-SA)
└── cvg.h # CVG public API (struct opener_cvg_ops)
4. subsys/opener/ — DECT NR Stack (Zephyr)
subsys/opener/
├── CMakeLists.txt
├── Kconfig
│
├── phy/
│ ├── CMakeLists.txt
│ ├── Kconfig
│ ├── opener_phy_hal.c # HAL dispatch (calls struct opener_hal_ops)
│ └── nrf91x1/
│ ├── CMakeLists.txt # only added when CONFIG_OPENER_DIRECT_PHY_NRF91X1=y
│ └── opener_phy_nrf91x1.c # direct nRF91x1 MDM calls
│
├── mac/
│ ├── CMakeLists.txt
│ ├── Kconfig
│ ├── opener_mac_core.c # superframe, slot allocation, PT/FT mode
│ ├── opener_mac_scheduler.c # TX/RX scheduler (struct opener_scheduler_ops impl)
│ ├── opener_mac_assoc.c # association manager
│ └── opener_mac_group.c # group addressing (MAC-GRP)
│
├── dlc/
│ ├── CMakeLists.txt
│ ├── Kconfig
│ ├── opener_dlc_core.c # DLC dispatch, routing service
│ └── opener_dlc_sa.c # DLC-SA: segmentation, ARQ (SAW / GBN)
│
└── cvg/
├── CMakeLists.txt
├── Kconfig
└── opener_cvg_core.c # convergence layer (IPv6 profile)
subsys/opener/Kconfig (excerpt):
menuconfig OPENER
bool "DECT-2020 NR protocol stack"
help
Enable the DECT-2020 NR MAC/DLC/CVG stack.
if OPENER
config OPENER_FT_MODE
bool "Fixed Termination (gateway) mode"
config OPENER_PT_MODE
bool "Portable Termination (device) mode"
config OPENER_MAX_PT
int "Maximum number of associated PTs (FT mode)"
default 64
range 1 256
config OPENER_DIRECT_PHY_NRF91X1
bool "Direct PHY access via nRF91x1 MDM API"
depends on SOC_NRF9120 || SOC_NRF9160
rsource "phy/Kconfig"
rsource "mac/Kconfig"
rsource "dlc/Kconfig"
rsource "cvg/Kconfig"
endif # OPENER
5. lib/opener_rpc/ — RPC Transport Library (Zephyr Server Side)
lib/opener_rpc/
├── CMakeLists.txt
├── Kconfig
│
├── server/
│ ├── rpc_server.c # SPI frame pump: rx_thread, tx_queue drain
│ ├── rpc_dispatch.c # demultiplex mod_id → handler table
│ └── handlers/
│ ├── rpc_mac_handler.c # MAC module RPC handlers (proc_ids → mac API calls)
│ ├── rpc_dlc_handler.c
│ └── rpc_cvg_handler.c
│
└── common/
└── crc16_ccitt.c # CRC-16/CCITT implementation (shared source,
# also compiled into the Linux kernel module)
lib/opener_rpc/Kconfig:
config OPENER_RPC_SERVER
bool "DECT NR RPC server (SPI slave transport)"
depends on OPENER && SPI
help
Enable the SPI-based RPC server. The device acts as SPI slave and
exposes all stack modules to the Linux host over the wire protocol
defined in include/opener/rpc/.
6. drivers/spi_rpc_slave/ — SPI Slave Transport Driver (Zephyr)
drivers/spi_rpc_slave/
├── CMakeLists.txt
├── Kconfig
└── spi_rpc_slave.c # Zephyr SPI slave driver: DMA transfer, IRQ GPIO assertion
The driver asserts the IRQ GPIO when an event frame is ready to push to the host, and exposes a simple byte-stream interface to lib/opener_rpc/server/rpc_server.c.
7. boards/ and dts/ — Board Support
boards/
└── arm/
├── opener_devkit/ # reference development kit board definition
│ ├── opener_devkit.yaml
│ ├── opener_devkit.dts
│ └── Kconfig.board
└── overlays/
└── spi_rpc.overlay # SPI + IRQ GPIO Devicetree overlay for RPC transport
dts/
└── bindings/
└── opener,spi-rpc-slave.yaml # Devicetree binding for the RPC slave node
dts/bindings/opener,spi-rpc-slave.yaml (excerpt):
description: DECT NR SPI RPC slave node
compatible: "opener,spi-rpc-slave"
properties:
irq-gpios:
type: phandle-array
required: true
description: GPIO used by the device to signal a pending event to the host.
spi-max-frequency:
type: int
default: 8000000
8. samples/ — Sample Applications
Three samples are provided, each exercising a distinct operational role and serving as the reference starting point for that class of product.
Sample |
Mode |
Role |
|---|---|---|
|
PT only |
Sensor node: associates with an FT, sends periodic uplink data |
|
FT only |
Aggregator / gateway: manages associations, receives uplink data, drives downlink commands; intended for host-controlled deployment with the Linux DKMS module |
|
FT + PT simultaneously |
Mesh relay: associates upward with a parent FT as a PT, while acting as an FT to child PTs; demonstrates the self-organising mesh topology |
samples/
├── opener_sensor_pt/
│ ├── CMakeLists.txt
│ ├── prj.conf # CONFIG_OPENER=y CONFIG_OPENER_PT_MODE=y
│ ├── boards/
│ │ └── nrf9161dk.conf
│ └── src/
│ └── main.c
│
├── opener_aggregator_ft/
│ ├── CMakeLists.txt
│ ├── prj.conf # CONFIG_OPENER=y CONFIG_OPENER_FT_MODE=y CONFIG_OPENER_RPC_SERVER=y
│ ├── boards/
│ │ └── nrf9161dk.conf
│ └── src/
│ └── main.c
│
└── opener_relay/
├── CMakeLists.txt
├── prj.conf # CONFIG_OPENER=y CONFIG_OPENER_FT_MODE=y CONFIG_OPENER_PT_MODE=y
├── boards/
│ └── nrf9161dk.conf
└── src/
└── main.c
9. linux/ — Linux Host Side
linux/
├── kernel/ # DKMS kernel module
│ ├── dkms.conf
│ ├── Makefile # kernel Kbuild file
│ └── opener_host/ # module source directory
│ ├── compat_types.h # uint8_t / uint16_t shims for kernel environment
│ ├── opener_host.h # internal structs (opener_priv, locking, queues)
│ ├── opener_main.c # spi_driver probe/remove, IRQ top-half
│ ├── opener_rpc.c # SPI frame pump, RPC call serialisation
│ ├── opener_netdev.c # net_device (ARPHRD_NONE, IPv6 profile)
│ ├── opener_netlink.c # Generic Netlink UAPI (family "OPENER")
│ └── opener_scheduler.c # scheduler module proxy (kernel side)
│
└── tools/ # userspace management utilities
├── CMakeLists.txt
├── opener_ctl.c # libnl-based CLI: stats, channel config
└── opener_ctl.h
9.1 DKMS Configuration
linux/kernel/dkms.conf:
PACKAGE_NAME="opener-host"
PACKAGE_VERSION="0.1.0"
BUILT_MODULE_NAME[0]="opener_host"
BUILT_MODULE_LOCATION[0]="opener_host/"
DEST_MODULE_LOCATION[0]="/kernel/drivers/net/"
MAKE[0]="make -C /lib/modules/${kernelver}/build M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build"
CLEAN="make -C /lib/modules/${kernelver}/build M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build clean"
AUTOINSTALL="yes"
9.2 Kernel Module Makefile
linux/kernel/Makefile:
# Shared RPC protocol headers live at repo root include/.
# The DKMS build copies only linux/kernel/ into the build tree, so the
# relative path must be resolved from the original source location.
OPENER_INCLUDE := $(src)/../../include
ccflags-y := -I$(OPENER_INCLUDE)
obj-m := opener_host.o
opener_host-y := \
opener_host/opener_main.o \
opener_host/opener_rpc.o \
opener_host/opener_netdev.o \
opener_host/opener_netlink.o \
opener_host/opener_scheduler.o
DKMS path note: when DKMS copies the sources into its build tree it preserves the directory structure under
linux/kernel/. The$(src)variable in the Makefile resolves to the module source directory inside the DKMS tree, so$(src)/../../includecorrectly walks up to the repository rootinclude/directory regardless of where DKMS places the build tree.
9.3 Adding a New Module Proxy (Host Side)
To expose a new stack module via RPC on the kernel side, add one file following the existing pattern:
Step |
Action |
|---|---|
1 |
Define |
2 |
Add |
3 |
Implement |
4 |
Register the event handler in |
5 |
Add |
6 |
Add the corresponding RPC handler in |
10. Source Cross-Reference
The following table shows which directories contribute to each build target.
Directory |
Zephyr firmware |
Linux DKMS module |
Host userspace tools |
|---|---|---|---|
|
✓ (headers) |
✓ (headers, via |
✓ (headers) |
|
✓ (headers) |
— |
— |
|
✓ |
— |
— |
|
✓ |
✓ ( |
— |
|
✓ |
— |
— |
|
✓ |
— |
— |
|
— |
✓ |
— |
|
— |
— |
✓ |
11. Open Questions
OQ-LAYOUT-01 — crc16_ccitt.c dual compilation: the CRC implementation lives in lib/opener_rpc/common/ and is compiled by both the Zephyr build and the Linux kernel Makefile (via an explicit obj-m entry or ccflags-y path). An alternative is to keep it header-only as a static inline in include/opener/rpc/crc16_ccitt.h, avoiding the dual-source problem entirely. The trade-off is a slightly larger object per translation unit vs. a cleaner build dependency graph.
OQ-LAYOUT-02 — DKMS include path stability: the $(src)/../../include path in the kernel Makefile assumes DKMS preserves the relative directory structure (linux/kernel/ inside the repo root). If the DKMS installation copies only linux/kernel/ without the repository root, this path breaks. A robust alternative is to install the shared headers as a system header package (e.g., opener-dev) and reference them via a fixed path such as /usr/include/opener.
OQ-LAYOUT-03 — West manifest ownership: this repository can be used as a west module (added to an external manifest) or as a self-contained manifest repo (via west.yml). The self-contained mode is useful for standalone development but may conflict with the application’s own manifest. The recommended approach (module-only) should be documented and the west.yml clearly marked as a development convenience.