Skip to content

Workspace and Build

paiOS uses a Cargo Workspace with a “Hexagonal Feature-Gated Plugin” pattern to enforce architectural boundaries. This page documents the workspace layout, feature flag conventions, default profiles, feature aggregation, and the sys-crate pattern.

The pai-os repository contains the full project. The Rust workspace root (where the root Cargo.toml and crates/ directory live) is the engine/ folder. The composition root crate is pai-engine (the final executable).

engine/ # Workspace root (inside pai-os repo)
├── Cargo.toml # Workspace root
├── Cargo.lock
├── crates/ # Hexagonal Modules (Domains + Ports + Adapters)
│ ├── common/ # Shared types, logging, ConfigProvider
│ ├── core/ # SessionManager, StateMachine, high-level Traits
│ ├── vision/ # Vision Domain
│ │ ├── Cargo.toml # Features: vision_v4l2, vision_rga, vision_mock
│ │ └── src/
│ │ ├── domain/ # VisionManager, StreamProfile, FramePool
│ │ ├── ports/ # FrameSource, ImageProcessor
│ │ └── adapters/ # Feature-gated implementations (e.g., v4l2.rs)
│ ├── audio/ # Audio Domain (CPAL, WebRTC)
│ ├── inference/ # Inference Domain (RKNN, LlamaCpp, Sherpa, MCP Client)
│ ├── api/ # API Gateway (REST, gRPC, MCP Server)
│ └── peripherals/ # HMI, Buttons, GPIO
├── libs/ # ONLY for pure C-FFI wrappers (sys-crates)
│ ├── rknn-sys/ # Unsafe Rust bindings for Rockchip C-Libraries
│ └── ...
└── pai-engine/ # Composition Root (The Executable)
├── Cargo.toml # Pulls crates as optional dependencies
└── src/
└── main.rs # Parses hardware.toml, instantiates adapters, injects into core

For details on the internal structure and layout of these domain crates (e.g., src/domain/, src/ports/, src/adapters/), see the Engine Domains overview.

Why a Coarse-Grained Workspace (Not Single-Crate or Micro-Crates)?

Section titled “Why a Coarse-Grained Workspace (Not Single-Crate or Micro-Crates)?”

The workspace layout in this section follows the decision in ADR-008: Workspace Layout for Hexagonal Engine:

  • Not a single crate: a single engine crate would make it too easy to create a “ball of mud” where domain, ports, and adapters are mixed and boundaries rely only on social discipline.
  • Not micro-crates: splitting every port/adapter into separate crates would add ceremony and cognitive load without clear benefit for paiOS, which is focused on a single engine rather than a crate ecosystem.
  • Coarse-grained domain crates: a small number of crates (common, core, vision, audio, inference, api, peripherals) map directly to engine domains (domain crates) and keep Hexagonal boundaries clear while staying easy to navigate.
RuleDescription
No Root AdaptersAdapters are feature-gated modules inside their domain crate (e.g., crates/vision/src/adapters/v4l2.rs). They do NOT get their own top-level crates.
The Executablepai-engine is the only place for dependency injection (Composition Root).
Sys-Crateslibs/ is strictly for *-sys crates (raw, unsafe FFI bindings). Rationale: ADR-008 (why sys-crates live in libs/).

We enforce a strict [domain]_[technology] naming convention for capability features on domain crates, and keep profile features on pai-engine only. [domain] is the abbreviated domain prefix (e.g. infer for Inference, periph for Peripherals):

KindPatternExamplesPurpose
Capability (domain crate)[domain]_[tech]vision_v4l2, vision_image, infer_rknn, infer_llamacpp_cpu, api_grpc_uds, periph_gpioIsolates technology-specific adapters inside a single domain crate
Capability (mock) (domain crate)[domain]_mockvision_mock, audio_mock, infer_mock, api_mock, periph_mockEnables mock adapters for testing and CI
Profile / preset (pai-engine)free-form, but treated as a build presetdesktop, rockchip (and future platform profiles, e.g. raspi)Groups a set of capability features for a given platform or build mode

Rationale:

  • Linker safety: Prevents linker errors on unsupported hosts by only compiling adapters for explicitly enabled capabilities.
  • Portability: Binary remains portable; only enabled capabilities compile, and profiles make platform selection explicit.
  • CI/CD: *_mock capabilities allow tests to run without hardware; a test profile aggregates these consistently.
  • Developer experience: Clear feature names and a small number of well-defined profiles make it obvious which adapters are active.

Why Capability-Based Features (not target_arch)

Section titled “Why Capability-Based Features (not target_arch)”

A common mistake is checking CPU architecture to select hardware adapters:

// ❌ WRONG: aarch64 covers Rockchip RK3588, Raspberry Pi 5, AND Apple M3
#[cfg(target_arch = "aarch64")]
fn run_npu() { /* Rockchip code? Also true on Apple Silicon! */ }

paiOS takes a capability-based approach instead: features are named after the hardware capability or library, not the SoC or architecture. This solves two problems:

1. Heterogeneous SoCs: A Rockchip RK3588 and a Raspberry Pi 5 are both aarch64. Only one has an RKNN NPU. The feature infer_rknn makes the distinction unambiguous.

2. Combo hardware: Edge AI frequently uses modular accelerator boards (e.g. a Raspberry Pi 5 + Hailo 8L M.2 module, or a Radxa Rock 5B + Hailo breakout board). With capability-based features, this is a simple cargo invocation: no special handling needed:

# Radxa Rock 5B + Hailo NPU via PCIe:
cargo build --release --no-default-features \
--features "vision_v4l2, vision_rga, infer_hailo, infer_sherpa, periph_gpio"
# Raspberry Pi 5 + Hailo 8L (no RGA, use CPU for image processing):
cargo build --release --no-default-features \
--features "vision_v4l2, vision_image, infer_hailo, infer_sherpa, periph_gpio"

The Core (core) and all domain crates are entirely unaware of which combination is active. They only depend on well-defined port traits. The Composition Root (main.rs) wires the correct adapters based on hardware.toml at startup.

The two-level design:

LevelWhereWhat
Domain cratevision/Cargo.toml, inference/Cargo.toml, …Declares the fine-grained capability features (vision_v4l2, vision_image, infer_rknn, infer_sherpa, …) and *_mock capabilities for tests
Engine aggregation (profiles)pai-engine/Cargo.tomlRe-exports those capabilities and groups them into build profiles (desktop, rockchip, and future platform profiles)

pai-engine/Cargo.toml aggregates features from domain crates:

[package]
name = "pai-engine"
[dependencies]
vision = { path = "../crates/vision", optional = true }
audio = { path = "../crates/audio", optional = true }
inference = { path = "../crates/inference", optional = true }
# ... etc
[features]
default = ["desktop"]
# Profile: Desktop development (the default)
desktop = [
"vision_mock",
"audio_cpal",
"infer_llamacpp_cpu",
"infer_sherpa",
"periph_desktop",
"api_grpc_uds",
"api_http",
"api_mcp_server",
]
# Profile: Rockchip-based deployment (e.g. Radxa Rock 5C / paiBox)
rockchip = [
"vision_v4l2",
"vision_rga",
"audio_cpal",
"audio_webrtc",
"infer_rknn",
"infer_rkllm",
"infer_sherpa",
"infer_mcp_client",
"periph_gpio",
"periph_evdev",
"api_grpc_uds",
"api_grpc_tcp",
"api_http",
"api_mcp_server",
"core_sysinfo",
]
# Individual feature flags (aggregate into domain crates)
vision_v4l2 = ["vision/vision_v4l2"]
vision_mock = ["vision/vision_mock"]
audio_cpal = ["audio/audio_cpal"]
infer_rknn = ["inference/infer_rknn"]
# ... etc
PrincipleDescription
Desktop-first developmentdefault = ["desktop"] ensures cargo build works on any developer machine
No hardware requiredDesktop profile uses mock sensors, desktop hotkeys, CPU inference
Explicit platform buildsRecommended for Rockchip builds: cargo build --no-default-features --features rockchip (Rockchip-only, no desktop defaults). The dev-only combo cargo build --features rockchip keeps desktop enabled and is intended only for advanced local testing.
Additive compositionFeatures are additive; enabling rockchip doesn’t disable desktop features

paiEngine is designed for multi-platform use from the start. The architecture is not limited to a single-board ARM computer: feature-gating allows the same codebase to run on different hardware and operating systems.

AspectPolicy
Linux desktopSupported from day one. The default desktop profile lets you build and run the engine on a Linux desktop with no target hardware. This makes development and testing easy.
Deployment targetsCurrently we focus on Rockchip-based devices (e.g. paiBox) for deployment; the exact set of supported platforms may evolve. Feature flags keep target-specific code isolated.
Other operating systemsWindows, macOS, etc.: If support is straightforward (low-hanging fruit), we will consider it. We cannot guarantee support for additional OS at the current stage. Contributors are welcome to add or maintain support for other platforms; we will support such efforts (review, docs, CI where feasible).

All unsafe and FFI code lives in sys-crates under libs/. The domain crates never depend on them; only adapters (inside domain crates) do. The rationale for this layout (including why sys-crates live in libs/) is in ADR-008: Workspace Layout for Hexagonal Engine.

libs/
├── rknn-sys/ # Rockchip NPU bindings
└── usb-gadget/ # USB HID bindings
crates/
├── inference/
│ └── src/
│ ├── domain/ # No dependency on libs/
│ ├── ports/ # Pure Rust traits
│ └── adapters/
│ └── rknn.rs # Depends on rknn-sys, implements domain port

Benefits:

  • Clear unsafe boundary: All unsafe code isolated in libs/
  • Domain testability: Domain logic testable with mocks; no hardware dependencies
  • Backend swappability: Swap adapters without touching business logic
  • Feature isolation: Adapters are feature-gated, so sys-crates are only linked when needed

Adapters are compiled conditionally inside domain crates. The decision which features are on is made at build time by the root (pai-engine profiles and feature re-exports). Cargo propagates those features to dependency crates. Domain crates do not need to “handle” feature selection; they only gate adapter code with #[cfg(feature = "...")] in adapters/mod.rs or on the adapter modules.

For detailed examples and explanation of how main.rs and feature propagation interact, see the Composition Root documentation.

This ensures heavy C-bindings are opt-in and only compiled when explicitly enabled. Adapters are co-located with their domain, making the relationship explicit and maintainable.

Feature Flag Matrix (Capabilities vs Profiles)

Section titled “Feature Flag Matrix (Capabilities vs Profiles)”

Instead of repeating profile information on every module page, this section is the single source of truth for which capabilities are enabled in which profiles. Domain crate docs link back here when they describe their ports and adapters.

Current top-level profiles in pai-engine/Cargo.toml:

Profile featureIntended useBrief description
desktopDeveloper machines (Linux desktop)Default profile; uses mock or desktop-friendly adapters and CPU-based inference so cargo build works anywhere
rockchipRockchip-based deployment (e.g. Radxa Rock 5C / paiBox)Deployment profile; enables Rockchip-specific adapters (RKNN/RKLLM, RGA, GPIO, EVDEV, etc.)
test (conceptual)CI / testsAggregates *_mock capabilities across domain crates so the engine can run without hardware (defined as needed in pai-engine)

For exact profile definitions and build commands, see the Feature Aggregation in pai-engine snippet earlier in this page.

The following table highlights the most important capability features and which profiles enable them today. It is not an exhaustive list of every feature, but it captures the key adapters and should be kept in sync with pai-engine/Cargo.toml.

Capability featureDomain crateAdapter / Purposedesktoprockchiptest
vision_mockvisionMock camera / image processing for host development
vision_v4l2visionV4L2 camera (MIPI-CSI / USB)
vision_rgavisionRockchip RGA 2D accelerator
vision_imagevisionimage-crate CPU processingoptional / fallback
audio_cpalaudioCPAL-based capture/playback✖ (replaced by audio_mock)
audio_webrtcaudioWebRTC AEC / noise suppression
audio_mockaudioSimulated audio stack for CI
infer_llamacpp_cpuinferenceCPU LLM / embeddings (fallback)optional
infer_rknninferenceRockchip NPU vision models
infer_rkllminferenceRockchip NPU LLMs
infer_sherpainferenceAudio ML (Wake Word, STT, TTS, VAD)
infer_mcp_clientinferenceMCP client (tool execution)
infer_mockinferenceSimulated ML backend
api_grpc_udsapigRPC over UDS (local IPC)✖ (replaced by api_mock)
api_grpc_tcpapigRPC over TCP/TLS
api_httpapiREST (Ollama / OpenAI compatibility)
api_mcp_serverapiMCP server for host integration
api_mockapiSimulated API for CI
periph_desktopperipheralsDesktop hotkeys / notifications
periph_gpioperipheralsGPIO buttons / LEDs
periph_evdevperipheralsEVDEV touch/keyboard events
periph_usb_hidperipheralsUSB HID keyboard emulation
periph_desktop_hidperipheralsDesktop typing simulation
periph_mockperipheralsMock peripherals for CI

Profiles should be interpreted as “enable these capabilities”; new boards or accelerator combinations are added by:

  1. Defining any new capabilities (only where necessary) in the relevant domain crate.
  2. Adding a new profile in pai-engine that lists the desired capabilities.
  3. Updating this matrix so readers can see, at a glance, which adapters each profile activates.

For domain-specific details on what each adapter does, see the corresponding domain pages (Vision, Audio, Inference, API, Peripherals); for the rationale behind this structure, see ADR-008: Workspace Layout for Hexagonal Engine.

Cargo features define the driver/capability; runtime config defines the concrete device and settings.

This distinction is critical for hardware abstraction. A USB webcam and a Rockchip MIPI CSI camera both use the V4L2 API under Linux; they share the same Cargo feature (vision_v4l2) and the same adapter code. The difference is only in the device path and format, which are runtime configuration:

# hardware.toml: Rockchip wearable (MIPI CSI)
[vision.camera]
device_path = "/dev/video11"
format = "NV12"
# hardware.toml: Developer laptop (USB webcam)
[vision.camera]
device_path = "/dev/video0"
format = "MJPEG"

Rule of thumb:

LevelControlsExamples
Compile-time (Cargo feature)Which driver/library is linkedvision_v4l2, infer_rknn, periph_gpio, audio_cpal
Runtime (config file)Which device, paths, and parameters are usedDevice path, format, GPIO pin number, model file path, audio sensitivity

This means: do not create separate Cargo features for “USB webcam” vs “MIPI CSI”; they are the same driver (vision_v4l2), different config. Create a new feature only when the underlying library or FFI binding is different (e.g. vision_v4l2 vs vision_rga: different C libraries).

The executable crate pulls in specific domain crates via feature flags and wires them in pai-engine/src/main.rs. That file is the Composition Root: the only place that knows which adapters exist and how they are selected (config, features). The Core (Orchestrator) receives only injected interfaces and does not know which concrete adapters are loaded.

For the full responsibility of main.rs (config → init adapters → inject into Orchestrator → run), the pattern, and code examples, see the Composition Root documentation.