Skip to content

Resources

Resources are typed runtime objects that tasks and bridges can access without going through the message graph. They are provided by resource bundles and wired by name in copperconfig.ron. Today we primarily use them to expose hardware as a HAL for embedded systems, but the same mechanism works well for "engine services" like memory arenas, thread pools, or reserved subsystems dedicated to specific tasks or bridges.

Concept

  • Bundles own the concrete resources and register them into the runtime at startup.
  • Tasks and bridges declare the bindings they need by name, and the config maps those names to bundle_id.resource_id.
  • Resources can be exclusive (owned by one task) or shared (borrowed or Arc).
  • Bundles can be mission-scoped so the same task code can run against different hardware per mission.

Using resources (most users)

1) Declare the bundles you want in the config. 2) Bind task and bridge resource names to concrete bundle.resource paths. 3) Use the bindings in your task/bridge constructor.

Example copperconfig.ron snippet:

(
    resources: [
        (id: "board", provider: "board_crate::BoardBundle"),
    ],
    tasks: [
        (
            id: "telemetry",
            type: "app::TelemetryTask",
            resources: { bus: "board.telemetry_bus" },
        ),
    ],
    bridges: [
        (
            id: "crsf",
            type: "cu_crsf::CrsfBridge<SerialPort, SerialError>",
            resources: { serial: "board.uart0" },
        ),
    ],
)

The bundle provider defines the resource ids (like uart0 or telemetry_bus), so check the board/component crate docs or the bundle source for the list.

In code, either implement ResourceBindings manually or use the resources! macro for a compact declaration:

use cu29::prelude::*;

resources!({
    bus => Shared<TelemetryBus>,
});

impl CuTask for TelemetryTask {
    type Resources<'r> = Resources;
    // ...
    fn new_with(_cfg: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self> {
        Ok(Self { bus: resources.bus })
    }
}

Notes:

  • Owned<T> means the task takes exclusive ownership (common for peripherals).
  • Shared<T> means the task receives an owned Arc<T> cloned from a shared resource registered by the bundle.
  • Borrowed<T> means the task borrows &T from the ResourceManager for the lifetime of its resource binding.
  • Use missions: [...] on resource entries to scope bundles to specific missions.
  • See Copper Configuration file Reference for the full schema.

Resource usage in the DAG

cu29-graph-view shows resource bundles in tables alongside the task graph. The Used by column identifies task, bridge, and system consumers. A transport resource referenced by log_streaming.destinations[].transport.resource appears as system: logstream (<destination id>), even when no task or bridge binds it. For example, the logstream demo shows network.tx used by system: logstream (ground). This usage is inferred from the existing config; no additional resource annotation is needed.

Creating or adding resources

1) Define the resource types you want to expose (drivers, pools, arenas, handles). 2) Declare bundle IDs with bundle_resources!. 3) Implement ResourceBundle to construct resources and register them in the ResourceManager. 4) Point your config resources entries at the bundle provider path.

Minimal bundle example:

use cu29::resource::{ResourceBundle, ResourceManager};
use cu29::{CuResult, bundle_resources};
use std::sync::Arc;

pub struct BoardBundle;
bundle_resources!(BoardBundle: Uart0, TelemetryBus);

impl ResourceBundle for BoardBundle {
    fn build(
        bundle: cu29::resource::BundleContext<Self>,
        _config: Option<&cu29::config::ComponentConfig>,
        manager: &mut ResourceManager,
    ) -> CuResult<()> {
        let uart_key = bundle.key(BoardBundleId::Uart0);
        let bus_key = bundle.key(BoardBundleId::TelemetryBus);
        manager.add_owned(uart_key, MyUart::new())?;
        manager.add_shared(bus_key, Arc::new(MyBus::new()))?;
        Ok(())
    }
}

Embedded note: if you need to build resources from handles created in main (for example, HAL peripherals), use ResourceManager::add_bundle_prebuilt to insert them during runtime setup.

CUDA pools

Since v1.1: On supported host platforms, enable cu29's cuda feature to expose cu29::pool::cuda::{CuCudaPool, CudaSliceWrapper}. CuCudaPool::new returns a registered Arc pool whose acquire method produces CuHandle<CudaSliceWrapper<T>>. Tasks can forward clones of that handle through the graph and use as_cuda_slice or as_cuda_slice_mut when calling CUDA-aware libraries.

A resource bundle is the preferred place to create the shared CudaContext and device pool. Give each task its own stream unless stream sharing is an explicit part of the component contract. Cudarc tracks accesses to a CudaSlice across streams, so consumers should obtain pointers through its DevicePtr and DevicePtrMut APIs rather than caching raw pointers across task boundaries.

CUDA allocations are process-local and do not currently have a replayable wire representation. Configure nodes producing CUDA handles with logging: (handle_content: none) so Copper records message metadata without trying to serialize device contents. Use CuHandle::storage_id as a diagnostic when validating that adjacent tasks forwarded the same allocation.

Examples

  • https://github.com/copper-project/copper-rs/tree/master/examples/cu_resources_test - End-to-end example with owned vs shared resources, mission-specific bundles, and bridge bindings.
  • https://github.com/copper-project/copper-rs/tree/master/examples/cu_elrs_bdshot_demo - Embedded demo that wires a board resource bundle into tasks and bridges.
  • https://github.com/copper-project/copper-rs/tree/master/examples/cu_flight_controller - Uses the resources! macro to bind board peripherals to tasks.

Composing resource bundles

Resource bundles can consume other bundles' outputs through their own resources map. For example, a UART and SET pin become an HC-12 radio, and that radio can become a serial LogStream transport:

resources: [
    (id: "board", provider: "BoardResources"),
    (id: "radio", provider: "RadioResources",
     resources: { "serial": "board.uart", "set": "board.set", "delay": "board.delay" },
     config: { "channel": 21 }),
    (id: "telemetry", provider: "RadioLogResources",
     resources: { "serial": "radio.serial" }),
],

RadioResources and RadioLogResources are concrete application aliases for the chosen hardware and framing providers. The runtime macro computes construction order per mission; declaration order does not change bundle indices. Cycles, missing/inactive providers and unknown input or output names fail compilation. Bindings compile into static resource keys. Changes to provider input wiring need a rebuild; a runtime configuration cannot replace the compiled input topology.

A composing provider declares inputs with resources!, sets ResourceBundle::INPUT_NAMES to <Inputs as ResourceBindings>::NAMES, and calls bundle.inputs::<Inputs>(manager)? inside its existing build method. It consumes owned inputs or clones explicitly shared handles, then registers the wrapped output. Borrowed manager references cannot be stored in owned 'static outputs. Concrete resource types and exclusive takes are still checked by the manager at startup. Existing providers require no changes: INPUT_NAMES defaults to empty, and the ResourceBundle::build signature is unchanged.

just graph includes bundle consumers and LogStream destinations in each resource's “Used by” column. Dashed directed arrows run from provider bundles to consuming bundles; hover an arrow for the source slot and destination binding. These arrows show startup dependencies, not scheduled task-message edges.

The cu-hc12 component offers FU3 channel configuration, cu-serial-bridge offers raw byte channels, and cu29-logstream-serial adds bounded packet framing. Choose one consumer per radio. Multiplexing raw traffic and log streaming requires an explicit framing and routing protocol.

For USB-to-TTL HC-12 wiring on Unix, enable the cu-linux-resources/serial-rts Cargo feature and set serial0_rts: true alongside serial0_dev and serial0_nonblocking: true in the Linux bundle config. Bind the radio's set input to board.serial0_rts and use LinuxSerialRtsPin as its pin type. This owned OutputPin disables flow control, starts high and maps low/high to asserted/deasserted active-low RTS#. Use a compatible 3.3 V RTS# output and common ground. The same option works for serial1 through serial5; unsupported modem control fails startup. The HC-12 echo example shows complete USB-only wiring.