Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HAPTIC.SKIN — API Reference

Developer documentation for HAPTIC.SKIN, an open-source haptic navigation wearable: a neck-worn band of 8 vibration motors that points you toward your destination.

This site documents the three programmable surfaces of the system — not the project itself (for the project, hardware and BOM, see the GitHub repository).

SurfaceWhat it isReference
Python APIThe haptic_skin package — drive the necklace, run patterns, parse routes. The high-level entry point for applications.Python API
Firmware APIThe Rust + Embassy firmware running on the Pico (RP2040): motor driver, protocol parser, power guard.Firmware API
Serial protocolThe binary wire protocol between host and device over USB CDC-ACM.Serial protocol

One-liner

from haptic_skin import haptic

haptic("turn_left", 0.8)   # buzz the left motor at 80 %

By default the package talks to a built-in simulator, so you can develop with no hardware attached. Set HAPTIC_SKIN_PORT=/dev/ttyACM0 to drive a real Pico.

Architecture

flowchart LR
    APP["Your app\n(Python)"] --> PKG["haptic_skin\n(necklace, patterns, nav)"]
    PKG -->|"binary frames\nUSB CDC-ACM"| FW["Pico firmware\n(Rust + Embassy)"]
    FW -->|"8× PWM"| M["8× ERM motors"]

License

Code is MIT, documentation CC BY-SA 4.0, hardware CERN-OHL-P v2 — see Licensing.

Installation

Python package

pip install -e client/      # from the repository root

Then, from any program:

from haptic_skin import haptic, Necklace

haptic("NE", 0.8)                 # quick one-liner (default necklace)

nk = Necklace(port="/dev/ttyACM0")  # explicit device
nk.set_vector([0, 0, 255, 0, 0, 0, 0, 0])
nk.stop()

With no port (or HAPTIC_SKIN_PORT unset) the package uses an in-process simulator — no hardware required.

Firmware (Pico 1 W, RP2040)

rustup target add thumbv6m-none-eabi
cargo install elf2uf2-rs

cd firmware-rust-v2
cargo build --release

Flash: hold BOOTSEL, plug the Pico in (it mounts as RPI-RP2), then:

cargo run --release     # elf2uf2-rs copies the .uf2

Building this documentation

cd docs-site
cargo install mdbook mdbook-mermaid
pip install pdoc
./build.sh              # mdBook hub + Python API (pdoc) + Rust API (rustdoc)

Output lands in docs-site/book/ (with book/python/ and book/rust/).

Python API — haptic_skin

The full, auto-generated reference (from docstrings) lives here:

➡️ Open the complete haptic_skin API reference

It is generated with pdoc from the package source, so it always matches the installed code.

Module map

ModulePurpose
haptic_skinPackage entry point — haptic(), connect(), re-exports
necklaceNecklace controller + Direction enum
patternsOne-shot vibration patterns (preview, approach, now, arrived…)
navNavigator — continuous homing compass from GPS fixes
routingRoute parsing + Google Directions + trace densification
geoHaversine distance, bearing, relative-bearing buckets
bridgeWebSocket bridge for the Street View demo
transportSerialTransport / MockTransport + simulator
protocolBinary frame builders + Decoder

Quick reference

from haptic_skin import haptic, connect, Necklace, Direction

# one-liner on the default necklace (simulator unless HAPTIC_SKIN_PORT is set)
haptic("turn_left", 0.8)

# explicit controller
nk = connect(port="/dev/ttyACM0")
nk.set_motor(Direction.NE, 200)   # one motor
nk.set_vector([0]*8)              # the 8 motors at once
nk.haptic("E", 0.5)              # a direction at 0..1 intensity
nk.ping()                        # -> bool (Pong received)
nk.stop()

See the serial protocol for what these calls put on the wire.

Firmware API (Rust)

The firmware runs on a Raspberry Pi Pico 1 W (RP2040) in Rust + Embassy (async, no_std). The full rustdoc reference is here:

➡️ Open the complete firmware API reference

Generated with cargo doc from firmware-rust-v2/, so it tracks the code.

Crate map

ModulePurpose
mainInit, 8 PWM channels, boot self-test, USB command loop
configConstants — motor count, PWM_TOP, power-guard caps, USB identity
drivers::motorsMotors — drive the 8 motors by intensity (0..=255)
protoBinary protocol — FrameParser, Command, encode_event
tasks::power_guardCurrent cap (≤ 6 simultaneous motors at 60 % duty)
tasks::motor_schedulerMotorFrame composition

Pin mapping

8 motors → 8 independent hardware PWM outputs. On the RP2040 an even GPIO is channel A of slice gpio / 2, so the firmware uses GP0, GP2, GP4, GP6, GP8, GP10, GP12, GP14 (channel A of slices 0–7), at ~20 kHz.

Pico W note: the on-board LED is wired to the CYW43 wireless chip, not a GPIO — a status LED needs an external GPIO.

See the serial protocol for the commands the firmware accepts.

Serial protocol

Host ↔ Pico link over USB CDC-ACM, 115200 baud, using compact binary frames.

  • Firmware implementation: firmware-rust-v2/src/proto.rs (FrameParser)
  • Host implementation: client/src/haptic_skin/protocol.py (Decoder + builders)

Frame format

Every frame shares the same layout:

[start] [type] [len] [payload … (len bytes)] [xor]
FieldSizeDescription
start10xAA host → Pico, 0xBB Pico → host
type1command code (H→P) or event code (P→H)
len1payload length in bytes (0..=8)
payloadlendata
xor1XOR of type ^ len ^ payload[…] (excludes start and xor)

len is capped at 8 (MAX_PAYLOAD = MOTOR_COUNT); larger frames are rejected (BadLength). The receiver resynchronises on the next start byte if it lands mid-stream.

Commands — host → Pico (start = 0xAA)

CommandCodelenPayloadEffect
Ping0x010Device replies Pong
SetMotor0x102[idx, intensity]Motor idx (0–7) to intensity (0–255)
SetAll0x118[v0, v1, … v7]All 8 motors at once
StopAll0x120Turn every motor off

Events — Pico → host (start = 0xBB)

EventCodelenPayloadMeaning
Pong0x010Reply to Ping (handshake / heartbeat)
Error0xFF1[code]Invalid frame (BadXor, UnknownCmd, BadLength)

Examples

Ping            : AA 01 00 01
StopAll         : AA 12 00 12
SetMotor 3→200  : AA 10 02 03 C8 19      (xor = 10^02^03^C8 = 0x19)
SetAll 100…     : AA 11 08 64 64 64 64 64 64 64 64 6D
Pong (reply)    : BB 01 00 01

Design notes

  • Intensity is one byte (0–255), scaled on the device to the PWM resolution (PWM_TOP). On the host, intensity_to_pwm(0.0..1.0).
  • Power guard runs on the device after decoding: at most 6 motors on at once, each capped to 60 % duty, to stay under the 500 mA USB budget — the host need not worry about it.
  • The binary framing (vs the legacy text protocol) is compact and resynchronises cleanly; the XOR checksum catches corrupted bytes on the serial line.

Contributing to HAPTIC.SKIN

Thanks for your interest. This document covers the workflow for team members and external contributors.

Workflow

  1. Pick an issue on the project board or create one. Assign it to yourself.
  2. Create a branch from main: git checkout -b feat/<scope>-<short-title>. Scopes: firmware, daemon, client, hardware, patterns, docs, website.
  3. Commit using conventional commits:
    • feat(client): homing-compass navigator
    • fix(firmware): debounce serial command parser
    • docs(science): Pacinian threshold citation
  4. Open a PR against main. Link the issue. Fill the PR template.
  5. CI must pass and one reviewer must approve before merge.
  6. Branch is auto-deleted after merge.

Local dev setup

Daemon / client (Python)

cd daemon/  # or client/
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check . && ruff format --check .

Firmware (Arduino C++ on Arduino Mega 2560)

  1. Install the toolchain (pick one):
    # Arduino CLI
    arduino-cli core install arduino:avr
    # or use the Arduino IDE with the AVR boards package
    
  2. Build and flash (board FQBN arduino:avr:mega):
    cd firmware/
    arduino-cli compile --fqbn arduino:avr:mega .
    arduino-cli upload --fqbn arduino:avr:mega -p /dev/ttyACM0 .
    
  3. A serial simulator lives in tests/serial_sim.py for daemon dev without hardware.

Hardware (wiring)

  • 8 QYF-740 motor modules wired to 8 PWM pins on the Mega 2560.
  • Wiring diagram and BOM live under hardware/.

Website

The public site at haptic.skin. No build step for now — just edit files in website/public/ and push to main. GitHub Actions deploys automatically via rsync.

# Preview locally
open website/public/index.html

Code style

  • Python: ruff (configured in pyproject.toml), type hints encouraged, docstrings on public API.
  • Firmware (Arduino C++): keep loop() non-blocking (no delay() in the control path) — use millis()-based timing. Avoid dynamic allocation (String, malloc) in hot loops. Keep PWM writes batched per serial command.
  • Commits: Conventional commits lite (see above).
  • No secrets in commits — use .env (gitignored) for API keys.

Reviewing

Reviews should be lightweight. One approval is enough. Reviewers are auto-assigned via .github/CODEOWNERS. Cross-area reviews are encouraged — it helps everyone understand the whole system before the final defense.

Filing issues

  • Bug: use the bug template, include repro steps and hardware state.
  • Feature: use the feature template, link it to a milestone if applicable.
  • Tag with one zone label (firmware, daemon, …) and one priority label (P0-blocking to P3-nice-to-have).

Licenses

By contributing, you agree your contributions are licensed under:

  • MIT for code
  • CERN-OHL-P v2 for hardware files
  • CC-BY-SA 4.0 for documentation

See LICENSE, LICENSE-HARDWARE, LICENSE-DOCS.

Code of conduct

Be kind. See CODE_OF_CONDUCT.md.

Licensing

HAPTIC.SKIN is multi-licensed: each kind of artifact is covered by the license best suited to it. By contributing, you agree your contribution is released under the corresponding license.

WhatCoversLicenseFile
Codefirmware (firmware-rust-v2/, firmware/), the haptic_skin Python package (client/), the Street View demo (demo/), scripts, CIMITLICENSE
Documentationeverything under docs/, docs-site/, research/, README, the published site at docs.haptic.skin, text & imagesCC BY-SA 4.0LICENSE-DOCS
Hardwareschematics, wiring diagrams, PCB/3D-printable parts and the BOM under hardware/CERN-OHL-P v2LICENSE-HARDWARE

How to comply

  • Reusing the code? Keep the MIT copyright notice. That’s it.
  • Reusing the docs? Credit “HAPTIC.SKIN” with a link, and share adaptations under the same CC BY-SA 4.0.
  • Building the hardware? CERN-OHL-P is permissive: keep the notices and the link to the source design.

Why three licenses

A wearable project produces three different things — software, written knowledge, and a physical design. Each ecosystem has a standard, well-understood license; using the right one for each maximizes reuse and removes ambiguity for anyone who wants to fork, manufacture, or cite the project.

SPDX

Where practical, source files carry an SPDX-License-Identifier header (MIT, CC-BY-SA-4.0, or CERN-OHL-P-2.0) so tooling can detect the license automatically.

Questions

Open a discussion or see CONTRIBUTING.md.