haptic_skin.patterns

Discrete, one-shot vibration patterns — the manual pattern library.

A pattern is a list of frames; a frame is (vector, duration_s) where vector is 8 PWM values (0..255). play() streams the frames to a ~haptic_skin.necklace.Necklace, sleeping between them. The firmware stays "dumb": it only applies the vectors it receives.

Use this for manual triggering and bench calibration (haptic-skin pattern approach E) and from the client API. NOTE: the live Street-View / GPS navigation does NOT use these — it runs a continuous homing compass in haptic_skin.nav (its own streaming cadence). Keep the two in mind: patterns = discrete one-shots, nav = continuous guidance.

 1"""Discrete, one-shot vibration patterns — the *manual* pattern library.
 2
 3A *pattern* is a list of frames; a frame is ``(vector, duration_s)`` where
 4``vector`` is 8 PWM values (0..255). :func:`play` streams the frames to a
 5:class:`~haptic_skin.necklace.Necklace`, sleeping between them. The firmware
 6stays "dumb": it only applies the vectors it receives.
 7
 8Use this for manual triggering and bench calibration (``haptic-skin pattern
 9approach E``) and from the client API. NOTE: the *live* Street-View / GPS
10navigation does NOT use these — it runs a continuous homing compass in
11:mod:`haptic_skin.nav` (its own streaming cadence). Keep the two in mind:
12``patterns`` = discrete one-shots, ``nav`` = continuous guidance.
13"""
14
15from __future__ import annotations
16
17import time
18from typing import List, Tuple
19
20from . import protocol
21from .necklace import Necklace, resolve_direction
22
23Frame = Tuple[List[int], float]
24Pattern = List[Frame]
25
26_ZERO = [0] * protocol.NUM_MOTORS
27
28
29def _one(bucket, intensity: float) -> List[int]:
30    """8-vector with a single motor set to ``intensity`` (0..1)."""
31    vec = [0] * protocol.NUM_MOTORS
32    vec[int(resolve_direction(bucket))] = protocol.intensity_to_pwm(intensity)
33    return vec
34
35
36# -- the six navigation patterns -------------------------------------------
37def silent() -> Pattern:
38    return [(list(_ZERO), 0.1)]
39
40
41def preview(bucket) -> Pattern:
42    """Far (50-20 m): gentle 200 ms pulse at 40 %, every ~2 s."""
43    return [(_one(bucket, 0.40), 0.2), (list(_ZERO), 1.8)]
44
45
46def approach(bucket) -> Pattern:
47    """Near (20-5 m): firmer 200 ms pulse at 70 %, every ~0.8 s."""
48    return [(_one(bucket, 0.70), 0.2), (list(_ZERO), 0.6)]
49
50
51def now(bucket) -> Pattern:
52    """At the turn (<=5 m): one strong 800 ms buzz at 100 %."""
53    return [(_one(bucket, 1.0), 0.8), (list(_ZERO), 0.0)]
54
55
56def arrived() -> Pattern:
57    """Destination: circular sweep of the 8 motors over ~1.5 s at 80 %."""
58    step = 1.5 / protocol.NUM_MOTORS
59    return [(_one(i, 0.8), step) for i in range(protocol.NUM_MOTORS)]
60
61
62def off_route() -> Pattern:
63    """Off route: flash the 'back' motor 3x at 100 % (means 'turn around')."""
64    frames: Pattern = []
65    for _ in range(3):
66        frames.append((_one("back", 1.0), 0.15))
67        frames.append((list(_ZERO), 0.15))
68    return frames
69
70
71# -- player -----------------------------------------------------------------
72def play(necklace: Necklace, pattern: Pattern, *, sleep=time.sleep) -> None:
73    """Run a pattern once, then make sure motors are off."""
74    try:
75        for vector, duration in pattern:
76            necklace.set_vector(vector)
77            if duration > 0:
78                sleep(duration)
79    finally:
80        necklace.set_vector(list(_ZERO))
81
82
83def play_for(necklace: Necklace, pattern: Pattern, duration_s: float,
84             *, sleep=time.sleep, clock=time.monotonic) -> None:
85    """Loop a pattern until ``duration_s`` has elapsed, then stop."""
86    end = clock() + duration_s
87    try:
88        while clock() < end:
89            for vector, frame_dur in pattern:
90                if clock() >= end:
91                    break
92                necklace.set_vector(vector)
93                if frame_dur > 0:
94                    sleep(min(frame_dur, end - clock()))
95    finally:
96        necklace.set_vector(list(_ZERO))
Frame = typing.Tuple[typing.List[int], float]
Pattern = typing.List[typing.Tuple[typing.List[int], float]]
def silent() -> List[Tuple[List[int], float]]:
38def silent() -> Pattern:
39    return [(list(_ZERO), 0.1)]
def preview(bucket) -> List[Tuple[List[int], float]]:
42def preview(bucket) -> Pattern:
43    """Far (50-20 m): gentle 200 ms pulse at 40 %, every ~2 s."""
44    return [(_one(bucket, 0.40), 0.2), (list(_ZERO), 1.8)]

Far (50-20 m): gentle 200 ms pulse at 40 %, every ~2 s.

def approach(bucket) -> List[Tuple[List[int], float]]:
47def approach(bucket) -> Pattern:
48    """Near (20-5 m): firmer 200 ms pulse at 70 %, every ~0.8 s."""
49    return [(_one(bucket, 0.70), 0.2), (list(_ZERO), 0.6)]

Near (20-5 m): firmer 200 ms pulse at 70 %, every ~0.8 s.

def now(bucket) -> List[Tuple[List[int], float]]:
52def now(bucket) -> Pattern:
53    """At the turn (<=5 m): one strong 800 ms buzz at 100 %."""
54    return [(_one(bucket, 1.0), 0.8), (list(_ZERO), 0.0)]

At the turn (<=5 m): one strong 800 ms buzz at 100 %.

def arrived() -> List[Tuple[List[int], float]]:
57def arrived() -> Pattern:
58    """Destination: circular sweep of the 8 motors over ~1.5 s at 80 %."""
59    step = 1.5 / protocol.NUM_MOTORS
60    return [(_one(i, 0.8), step) for i in range(protocol.NUM_MOTORS)]

Destination: circular sweep of the 8 motors over ~1.5 s at 80 %.

def off_route() -> List[Tuple[List[int], float]]:
63def off_route() -> Pattern:
64    """Off route: flash the 'back' motor 3x at 100 % (means 'turn around')."""
65    frames: Pattern = []
66    for _ in range(3):
67        frames.append((_one("back", 1.0), 0.15))
68        frames.append((list(_ZERO), 0.15))
69    return frames

Off route: flash the 'back' motor 3x at 100 % (means 'turn around').

def play( necklace: haptic_skin.Necklace, pattern: List[Tuple[List[int], float]], *, sleep=<built-in function sleep>) -> None:
73def play(necklace: Necklace, pattern: Pattern, *, sleep=time.sleep) -> None:
74    """Run a pattern once, then make sure motors are off."""
75    try:
76        for vector, duration in pattern:
77            necklace.set_vector(vector)
78            if duration > 0:
79                sleep(duration)
80    finally:
81        necklace.set_vector(list(_ZERO))

Run a pattern once, then make sure motors are off.

def play_for( necklace: haptic_skin.Necklace, pattern: List[Tuple[List[int], float]], duration_s: float, *, sleep=<built-in function sleep>, clock=<built-in function monotonic>) -> None:
84def play_for(necklace: Necklace, pattern: Pattern, duration_s: float,
85             *, sleep=time.sleep, clock=time.monotonic) -> None:
86    """Loop a pattern until ``duration_s`` has elapsed, then stop."""
87    end = clock() + duration_s
88    try:
89        while clock() < end:
90            for vector, frame_dur in pattern:
91                if clock() >= end:
92                    break
93                necklace.set_vector(vector)
94                if frame_dur > 0:
95                    sleep(min(frame_dur, end - clock()))
96    finally:
97        necklace.set_vector(list(_ZERO))

Loop a pattern until duration_s has elapsed, then stop.