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).
| Surface | What it is | Reference |
|---|---|---|
| Python API | The haptic_skin package — drive the necklace, run patterns, parse routes. The high-level entry point for applications. | Python API |
| Firmware API | The Rust + Embassy firmware running on the Pico (RP2040): motor driver, protocol parser, power guard. | Firmware API |
| Serial protocol | The 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
| Module | Purpose |
|---|---|
haptic_skin | Package entry point — haptic(), connect(), re-exports |
necklace | Necklace controller + Direction enum |
patterns | One-shot vibration patterns (preview, approach, now, arrived…) |
nav | Navigator — continuous homing compass from GPS fixes |
routing | Route parsing + Google Directions + trace densification |
geo | Haversine distance, bearing, relative-bearing buckets |
bridge | WebSocket bridge for the Street View demo |
transport | SerialTransport / MockTransport + simulator |
protocol | Binary 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
| Module | Purpose |
|---|---|
main | Init, 8 PWM channels, boot self-test, USB command loop |
config | Constants — motor count, PWM_TOP, power-guard caps, USB identity |
drivers::motors | Motors — drive the 8 motors by intensity (0..=255) |
proto | Binary protocol — FrameParser, Command, encode_event |
tasks::power_guard | Current cap (≤ 6 simultaneous motors at 60 % duty) |
tasks::motor_scheduler | MotorFrame 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]
| Field | Size | Description |
|---|---|---|
start | 1 | 0xAA host → Pico, 0xBB Pico → host |
type | 1 | command code (H→P) or event code (P→H) |
len | 1 | payload length in bytes (0..=8) |
payload | len | data |
xor | 1 | XOR 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)
| Command | Code | len | Payload | Effect |
|---|---|---|---|---|
Ping | 0x01 | 0 | — | Device replies Pong |
SetMotor | 0x10 | 2 | [idx, intensity] | Motor idx (0–7) to intensity (0–255) |
SetAll | 0x11 | 8 | [v0, v1, … v7] | All 8 motors at once |
StopAll | 0x12 | 0 | — | Turn every motor off |
Events — Pico → host (start = 0xBB)
| Event | Code | len | Payload | Meaning |
|---|---|---|---|---|
Pong | 0x01 | 0 | — | Reply to Ping (handshake / heartbeat) |
Error | 0xFF | 1 | [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
- Pick an issue on the project board or create one. Assign it to yourself.
- Create a branch from
main:git checkout -b feat/<scope>-<short-title>. Scopes:firmware,daemon,client,hardware,patterns,docs,website. - Commit using conventional commits:
feat(client): homing-compass navigatorfix(firmware): debounce serial command parserdocs(science): Pacinian threshold citation
- Open a PR against
main. Link the issue. Fill the PR template. - CI must pass and one reviewer must approve before merge.
- 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)
- Install the toolchain (pick one):
# Arduino CLI arduino-cli core install arduino:avr # or use the Arduino IDE with the AVR boards package - 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 . - A serial simulator lives in
tests/serial_sim.pyfor 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 inpyproject.toml), type hints encouraged, docstrings on public API. - Firmware (Arduino C++): keep
loop()non-blocking (nodelay()in the control path) — usemillis()-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-blockingtoP3-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.
| What | Covers | License | File |
|---|---|---|---|
| Code | firmware (firmware-rust-v2/, firmware/), the haptic_skin Python package (client/), the Street View demo (demo/), scripts, CI | MIT | LICENSE |
| Documentation | everything under docs/, docs-site/, research/, README, the published site at docs.haptic.skin, text & images | CC BY-SA 4.0 | LICENSE-DOCS |
| Hardware | schematics, wiring diagrams, PCB/3D-printable parts and the BOM under hardware/ | CERN-OHL-P v2 | LICENSE-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.