hana_rigging 0.1.0

Device identity, presence, availability, and recovery policy for Bevy providers
docs.rs failed to build hana_rigging-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

hana_rigging

Crates.io Downloads docs.rs License

A Bevy kernel for durable device identity, presence, availability, and recovery policy. Hardware providers perform I/O and report their full device set to this crate; the crate does not enumerate or operate hardware itself.

Work in progress. This crate is in active development (v0.1.0) and not subject to semver stability guarantees. APIs will change without notice between commits. Do not depend on this in production code yet.

Rigging — the ropes, pulleys, and counterweights above a stage that hold the lights, screens, and scenery, and let an operator move any of them on cue. The rigging does not produce the light or the image; it is what everything hangs from, and what makes a fixture addressable by name instead of by where it happens to be hanging tonight.

The problem

Devices are addressed by whatever the platform hands you: monitor index 1, camera 0, "the nearest display". Those handles move. Unplug a projector and plug in a different one, and index 1 now points at another physical unit — so the saved window layout, the camera feed, or the DMX patch quietly drives the wrong device. Every subsystem then grows its own hand-rolled reconnect logic, and each one guesses differently about what "the same device" means.

hana_rigging makes the answer exact and shared. Identity is match-or-nothing: a saved key that matches no live unit yields nothing, never a fallback. Nothing enters service without an explicit authorization the kernel issued for that one physical unit.

What it does

  • Durable identityDeviceKey survives restarts and replugs, and records in its own type how much the identity can be trusted
  • Presence and reachability — present, absent, or unreachable-for-a-duration, merged across every provider that reports the same unit
  • Discovery scheduling — on-demand, event-driven, or periodic scans, with bounded concurrency, coalesced reruns, and startup readiness gating
  • Roles and bindings — application-stable RoleKeys bound to device endpoints, so "the presenter display" outlives the display it currently means
  • Recovery policy — per binding, decide whether a saved configuration is forgotten, retained, reapplied on request, or reapplied when the unit returns
  • Authorized applies — drivers configure hardware only through kernel-issued permits, on attempts the kernel starts, polls, deadlines, and retires
  • Identity adjudication — when a replacement unit occupies a departed one's slot, the kernel raises a question for a human instead of guessing
  • Entity mirror — every kernel fact is projected onto Bevy entities as read-only components, so change detection and remote inspection just work

Trust is part of the identity

A DeviceKey carries where its value came from, and that determines what the key is allowed to authorize:

Source Where it comes from What it can do
Reported The unit published it — an EDID serial, a CoreAudio UID Drive output
Authored A human assigned it, for units that report nothing Drive output
Synthesized Derived from descriptors as a location hint Restore saved configuration only

That distinction is the point. A webcam with no serial gets a synthesized key, so its saved window position can come back — but it can never silently become the camera a recording writes to, because a location hint is not proof of which unit is plugged in.

Reconciliation turns a key plus live evidence into an IdentityVerdict: Proven, RestoreOnly, Authored, Displaced (a same-kind unit took the slot — a human decides), WrongUnit, or Unverified.

Usage

Add the plugin, register the identity schemes your providers are allowed to report, and register the reporters and drivers that touch hardware:

use bevy::prelude::*;
use hana_rigging::prelude::*;

let mut app = App::new();
app.add_plugins(MinimalPlugins)
    .add_plugins(RiggingPlugin)
    .register_device_scheme(SchemeName::new("edid")?);

let driver = app.add_endpoint_driver(WindowDriver::default());

let reporter = app.add_device_reporter(
    MonitorReporter::default(),
    ReporterRegistration::required(
        DiscoveryCadence::EventDriven { backstop: Duration::from_secs(30) },
        ReporterCoverage::EstablishesAbsence(AuthoritativeReporterCoverage::one(
            CoveredDeviceIdentitySpace::ReportedScheme {
                kind:   DeviceKind::Display,
                scheme: SchemeName::new("edid")?,
            },
        )),
    ),
);

A reporter hands back its whole current device set each scan, never a delta. That is what lets the kernel conclude a device is genuinely gone rather than merely unmentioned this frame:

impl DeviceReporter for MonitorReporter {
    fn discover(&mut self) -> DiscoveryWork {
        DiscoveryWork::Immediate(MainThreadDiscoveryJob::new(|_world: &mut World| {
            DeviceScan::Complete(enumerate_monitors())
        }))
    }
}

discover receives no World on purpose — it is the boundary that keeps enumeration out of the kernel's own state.

Bind an application-stable role to a device endpoint, and state every policy explicitly. There are no implicit recovery defaults hiding in the kernel:

app.world_mut().resource_mut::<Bindings>().register(Binding {
    role:     RoleKey::new("presenter-display")?,
    endpoint: DeviceEndpoint { device: saved_key, id: EndpointId::Whole },
    driver,
    recovery: RecoveryPolicy::ReapplyOnReturn,
    retry:    RetryOn::NewRevision,
    on_abort: OnAbort::Revert,
    on_loss:  OnSessionLoss::Recreate,
    state:    RoleState::default(),
    requested: RequestedConfiguration::new(WindowPlacement { left: 0, top: 0 }),
    last_known_good: LastKnownGoodConfiguration::default(),
    apply_deadline:  ApplyDeadline::ProcessDefault,
})?;

Then read state off the mirrored entities, or observe the derived events — DeviceArrived, DeviceDeparted, PresenceChanged, IdentityChanged, RoleAwaiting, RoleAvailable, AttemptFinished, and the rest.

The rigging_kernel example is a complete headless run: two reporters pushing overlapping scans that agree on one panel and disagree about everything else, an authored inventory entry, one apply attempt driven to a terminal outcome, and a provoked departure. It ships with the crate — cargo run --example rigging_kernel.

A second example, identity_decision, walks the displaced-unit adjudication path end to end. It lives in the source repository rather than the published package, because it drives the kernel with a scripted device harness that is not itself published.

Schedule

RiggingPlugin chains five ordered sets in Update, exposed as RiggingSystems so integration crates can place their own systems precisely:

Collect → Reconcile → Prepare → SessionLoss → Apply

Prepare is deliberately empty — it is the one interval where identity is settled but no apply has started, which is where consumer systems build the configuration they want applied.

Design rules

These are enforced, not aspirational:

  • The kernel performs no I/O and knows nothing about any specific device kind
  • Exact match or nothing — no nearest-monitor, no first-camera, no tolerance
  • Scans are whole sets; absence is always a named variant, never Option
  • The kernel never silently puts a device in service; the default policy is Forget
  • Resources are authoritative, entity components are read-only mirrors
  • start_apply returns immediately and every poll re-validates that the attempt still targets the same physical unit

Version Compatibility

Version Bevy
hana_rigging 0.1.0 0.19

License

hana_rigging is free, open source and permissively licensed! Except where noted (below and/or in individual files), all code in this repository is dual-licensed under either:

at your option.

Your contributions

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.