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))
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.
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.
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 %.
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 %.
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').
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.
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.