Skip to content

Repository files navigation

ph-haptics

Crates.io Documentation MSRV License

ph-haptics is a host-compiled haptics DSL and deterministic no_std runtime that turns declarative ERM/LRA patterns into timed motor-control commands for embedded applications.

Release

Version 0.1.0 is available on crates.io, with API documentation on docs.rs. The host-side mock machine in mock/ provides the crate's contract proof.

It replaces hand-written timing state machines: patterns are validated and compiled into static Rust data on the host, then evaluated on the target without parsing, allocation, or fixed-rate polling. It is a scheduling and motor-modeling layer, not a hardware layer: firmware maps the emitted commands onto its own PWM, GPIO, bus, or motor-driver hardware. See the formal ph-haptics contract.

  • Curve shaping comes from ph-curves.
  • Works with both ERM and LRA motor types.
  • Runtime is tickless for ramp segments (via ph-curves::Tickless).
  • Text/script compilation is handled by ph-haptics-gen (not the runtime crate).

Installation

Add the runtime to embedded consumers with default features only:

cargo add ph-haptics@0.1.0

The gen feature is reserved for host-side generation commands and tooling; do not enable it in a firmware dependency.

Layout

Path Role
src/ Runtime library (Runner, motors, compiled catalog types)
src/bin/ph-haptics-gen.rs Host-only .phh → Rust generator (--features gen)
assets/phh-library/ Curated .phh catalog + builtin motor_profiles.toml
mock/ Host-side deterministic contract proof (virtual clock + golden traces)
docs/ Contract and design notes (contract, compiler, public API, architecture)
scripts/ Canonical CI (ci.sh) and mock verify (mock-verify.sh)

ph-haptics-gen

All textual/script compilation lives in the generator binary. Install the matching published version for host-side use:

cargo install ph-haptics --version 0.1.0 --features gen --bin ph-haptics-gen

From a source checkout, run the generator with Cargo. This example uses a real library asset:

cargo run --features gen --bin ph-haptics-gen -- \
  --input assets/phh-library/erm_ui_micro.phh \
  --output src/haptics_compiled.rs \
  --curves-module crate::curves

Optional --curves-file <path> enables strict curve-symbol validation against a ph-curves-gen output file. Omit it unless you have such a generated file.

This takes one text file with multiple definitions and emits Rust constants:

  • one MotorProfile static per resolved profile (built-in + optional overlay)
  • one instruction array + Program + CompiledHapticDef per haptic
  • COMPILED_HAPTICS array containing all compiled haptics
  • COMPILED_CATALOG ready-to-use CompiledCatalog over COMPILED_HAPTICS

By default, ph-haptics-gen includes the built-in profile pack from assets/phh-library/motor_profiles.toml.

Use --profiles-toml <path> to add or override profiles.

Motor profiles TOML overlay format

Profiles are defined in TOML:

[profiles.handheld_erm]
motor = "erm"
kick_ms = 8
kick_level = 100.0
min_level = 25.1
max_level = 100.0
gamma = "ease_in_quad"
ramp_step_ms = 2
min_dt_ms = 1
duty_step = 3.1

All level/duty fields are percentages (0..100). At codegen they become runtime MotorProfile fraction fields (kick_frac, min_run_frac, max_frac, duty_step).

motor ("erm" or "lra") selects which built-in defaults fill in omitted fields, and is checked against every haptic that references the profile — a motor=lra haptic cannot use an ERM-tuned profile. Prefer setting it explicitly. If omitted, names starting with lra_ inherit DEFAULT_LRA_PROFILE and everything else inherits DEFAULT_ERM_PROFILE, which is easy to get wrong: a profile named cross_lra or my_lra_tuning silently inherits the ERM defaults.

Haptics text format

Top-level directives:

  • haptic <name> motor=<erm|lra> [profile=<name>] [loop=<once|forever|N>]
  • include "path/to/file.phh" — inline another .phh file (recursive, jailed under the top-level input directory). Each file is expanded at most once, so a file reached through two different paths (a diamond) is inlined a single time. A file that includes itself, directly or transitively, is a cycle and is an error.

Inside each haptic block:

  • ramp <duration_ms> <from%> <to%> <curve|@gamma> [step=<u16>] [rounding=<nearest|floor|ceil>] [min_dt=<u32>] [lra_hz=<u16>] [lra_hz_to=<u16>]
  • hold <duration_ms> <level%> [lra_hz=<u16>]
  • pause <duration_ms>
  • use <name> — inline instructions from a previously defined haptic (same motor kind)
  • repeat N / endrepeat — duplicate enclosed instructions N times (no nesting; N ≤ 1024)
  • end

All level values (from, to, level) are percentages 0..100 (decimals allowed, e.g. 45.78). They map to the internal 0..=u16::MAX range.

lra_hz / lra_hz_to are LRA-only and are rejected on a motor=erm haptic (the runtime would silently ignore them). loop=N is rejected when N cycles would exceed the runtime's u32 millisecond clock.

Example:

include "shared.phh"

haptic click motor=erm profile=handheld_erm loop=once
ramp 16 0 100 @gamma
pause 8
end

haptic buzz motor=lra loop=forever
hold 20 68.7 lra_hz=210
pause 10
end

haptic double_click motor=erm
repeat 2
use click
endrepeat
end

haptic alert motor=lra loop=3
ramp 30 0 100 ease_in_quad lra_hz=180 lra_hz_to=240
pause 20
end

Profile integration in generated catalogs and runtime command scheduling:

  • kick is applied at runtime when the motor starts from rest or recovers from level 0 (kick_ms + kick_level); the generator does not inject synthetic Hold instructions
  • @gamma resolves from the selected profile's gamma
  • ramp default step comes from duty_step
  • ramp default min_dt comes from min_dt_ms (or ramp_step_ms if min_dt_ms=0)
  • non-zero levels are clamped to profile [min_level, max_level]
  • ERM haptics without profile=... use DEFAULT_ERM_PROFILE
  • LRA haptics without profile=... use DEFAULT_LRA_PROFILE
  • when a profile provides gamma, output level is remapped through profile.gamma_curve
  • lra_hz_to enables linear frequency sweeps across a ramp segment

Pattern Library

A curated .phh catalog is available in assets/phh-library/ with ERM/LRA UI, notifications, alerts, gameplay, ambient loops, accessibility, wearable, and cross-device pattern sets. A matching preset profile pack is included at assets/phh-library/motor_profiles.toml. See assets/phh-library/README.md for usage.

Compiled command path

Runtime construction is intentionally centered on generated compiled assets:

use ph_haptics::{MotorConfig, ErmConfig};

include!("haptics_compiled.rs");

let mut runner = COMPILED_CATALOG
    .runner_started("click", MotorConfig::Erm(ErmConfig::new(255)), 0)
    .unwrap();

let frame = runner.poll(0);

// Firmware applies this abstract command through its own hardware adapter.
let command = frame.command;

Hardware boundary

ph-haptics determines the abstract motor command that should be active and the next time it may change. It does not configure peripherals, implement a motor driver, or claim physical actuator behavior. Those responsibilities belong to a firmware-owned hardware adapter.

The crate-level proof is the host-side mock machine in mock/ (bash scripts/mock-verify.sh). Hardware integrations live in downstream consumer repositories and are not evidence for the crate contract.

no_std/no_alloc

Embedded runtime contract (default features):

  • #![no_std] — only core + ph-curves (default features off)
  • no_alloc — no runtime allocation; no global allocator required
  • No parsing, peripheral control, or hardware-driver implementation in the library

ph-haptics-gen (--features gen) is host-only. Never enable gen on a firmware dependency. Consumers use default-feature ph-haptics and map DriveCommand values through their own hardware adapters.

See docs/architecture.md for the layer table and package boundary rules. Generator internals: docs/compiler-design.md. Public surface: docs/public-api.md.

Contributing / local CI

Run the complete contributor validation suite from the repository root:

bash scripts/ci.sh

On Windows, use Git Bash (or any bash). Details and gate list: CONTRIBUTING.md. Mock-only proof command: mock/README.md.

Report security issues via GitHub Security Advisories or a private maintainer contact; do not file public issues for undisclosed vulnerabilities.

About

Haptics DSL compiler and no_std scheduling/motor-modeling runtime — timed ERM/LRA drive commands for downstream firmware.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages