Ticklish Device Example - Adafruit Circuit Playground Bluefruit

The following sample CircuitPython demonstrates reactive behavior in a ‘ticklish’ device. Left alone, it occasionally moves a servo in an ‘idle’ motion. Once stroked or touched, it moves more quickly and continuosly. This is intended as a starting point for constructing more elaborate behaviors including a mixture of autonomous and reactive expressions.

This program assumes that both a soft pressure sensor and a hobby servo are externally attached. Please refer to the following pages for circuit details:

Direct download: cpb_ticklish.py.

  1# cpb_ticklish.py
  2
  3# Demonstrate reactive behavior: an idle movement sequence, interrupted with
  4# reactive movements in response to a sensor input.
  5
  6# This sketch assumes two external circuits: a resistive soft-sensor connected
  7# to A2, and a hobby servo on 'SDA A5'.  Please refer to the cpb_soft_sensing.py
  8# and cpb_sine_servo.py sketches for more details on the wiring.
  9
 10# ----------------------------------------------------------------
 11# Import any needed standard Python modules.
 12import time, math
 13
 14# Import the board-specific input/output library.
 15from adafruit_circuitplayground import cp
 16
 17# Import the low-level hardware libraries.
 18import board
 19import digitalio
 20import analogio
 21import pwmio
 22
 23# Import the Adafruit helper library.
 24from adafruit_motor import servo
 25
 26# ----------------------------------------------------------------
 27# Initialize hardware.
 28
 29# Configure the digital input pin used for its pullup resistor bias voltage.
 30bias = digitalio.DigitalInOut(board.D10)        # pad A3
 31bias.switch_to_input(pull=digitalio.Pull.UP)
 32
 33# Configure the analog input pin used to measure the sensor voltage.
 34sensor = analogio.AnalogIn(board.A2)
 35scaling = sensor.reference_voltage / (2**16)
 36
 37# Create a PWMOut object on pad SDA A5 to generate control signals.
 38pwm = pwmio.PWMOut(board.A5, duty_cycle=0, frequency=50)
 39
 40# Create a Servo object which controls a hobby servo using the PWMOut.
 41actuator = servo.Servo(pwm, min_pulse=1000, max_pulse=2000)
 42actuator.angle = 0
 43
 44# Configure the NeoPixel LED array for bulk update using show().
 45cp.pixels.auto_write = False
 46
 47# Turn down the NeoPixel brightness, otherwise it is somewhat blinding.
 48cp.pixels.brightness = 0.5
 49
 50# ----------------------------------------------------------------
 51# Define functions used by the main loop.
 52
 53# Use the CPB NeoPixel LEDS as a dial gauge.  A normalized value between zero
 54# and one is shown as a spot of light like a dial needle.  The USB connector is
 55# considered to be the top; the LED to its right is zero, with values increasing
 56# clockwise around to the LED to the left of the USB.  The spot is anti-aliased
 57# to increase precision, i.e. it cross-fades between LEDs as the value changes.
 58
 59def update_LED_dial(value):
 60    # Clamp the input to the range limits.
 61    clamped = min(max(value, 0.0), 1.0)
 62
 63    # Convert the normalized value to an indicator angle in degrees with respect to the top.
 64    indicator = (clamped * 300) + 30
 65
 66    # Project the angle onto the NeoPixels. The list assumes the Neopixels are
 67    # uniformly spaced at 30 degree intervals, with the bottom position absent
 68    # (e.g. at the battery connector).
 69    for i, led_angle in enumerate((330, 300, 270, 240, 210, 150, 120, 90, 60, 30)):
 70        diff = abs(indicator - led_angle)
 71        if diff < 30:
 72            c = min(max(255 - int(10*diff), 0), 255)
 73            cp.pixels[i] = (c, c, c) # shade of gray
 74        else:
 75            cp.pixels[i] = (0,0,0)   # no light
 76
 77    # Send new data to the physical LED chain.
 78    cp.pixels.show()
 79
 80# ----------------------------------------------------------------
 81# Initialize global variables for the main loop.
 82
 83# Convenient time constant expressed in nanoseconds.
 84second = 1000000000
 85
 86# Integer time stamp for the next console output.
 87next_print_time = time.monotonic_ns()
 88
 89# Previous touch value, used to detect changes.
 90last_touch = 0
 91
 92# Behavior mode symbol.  This is the state label for a simple behavioral state machine.
 93behavior_mode = 'idle'
 94
 95# Integer time stamp for a state timeout.
 96next_timeout_time = 0
 97
 98# Integer time stamp for next behavior activity to begin.
 99next_activity_time = time.monotonic_ns() + 2 * second
100
101# Flag to trigger motion.
102sweep_start = False
103
104# Commanded value for motion in degrees/second.
105sweep_rate = 0
106
107# Current value for motion speed in degrees/second.
108sweep_direction = 0
109
110# Integer time stamp for next servo update.
111next_servo_update = time.monotonic_ns()
112
113# ----------------------------------------------------------------
114# Begin the main processing loop.
115while True:
116
117    # Read the current integer clock.
118    now = time.monotonic_ns()
119
120    #---- soft sensor input and display -----------------------------
121    # Read the integer sensor value and scale it to a value in Volts.
122    volts = sensor.value * scaling
123
124    # Normalize the soft sensor reading.  Typically you'll need to adjust these values to your device.
125    low_pressure_voltage  = 0.65
126    high_pressure_voltage = 0.20
127    pressure = (low_pressure_voltage - volts) / (low_pressure_voltage - high_pressure_voltage)
128
129    # Update the LED dial gauge display.
130    update_LED_dial(pressure)
131
132    #---- behavior state machine update -----------------------------
133    if behavior_mode == 'idle':
134
135        # Check whether the touch sensor has been activated
136        if pressure > 0.5:
137            # Change behavior state, which will create movement on the next cycle.
138            behavior_mode = 'tickled'
139            next_activity_time = now
140            next_timeout_time = now + 4 * second
141            print("Entering tickled state.")
142
143        # Check whether to start another idle movement
144        elif now >= next_activity_time:
145            next_activity_time += 12 * second
146            sweep_rate = 60 # deg/sec
147            sweep_start = True
148
149    elif behavior_mode == 'tickled':
150
151        # Check whether the touch sensor is still activated
152        if pressure > 0.5:
153            next_timeout_time = now + 4 * second
154
155        # Check whether to start another tickled movement
156        if now >= next_activity_time:
157            next_activity_time += 3 * second
158            sweep_rate = 120 # deg/sec
159            sweep_start = True
160
161        elif now >= next_timeout_time:
162            print("Ending tickled state.")
163            next_activity_time = now + 6*second
164            behavior_mode = 'idle'
165
166    #---- periodic servo motion commands ----------------------------
167    # If the time has arrived to update the servo command signal:
168    if now >= next_servo_update:
169        next_servo_update += 20000000  # 20 msec in nanoseconds (50 Hz update)
170
171        # If a start event has been received, move in a positive direction.
172        if sweep_start is True:
173            print(f"Starting sweep motion at {sweep_rate} deg/sec.")
174            sweep_start = False
175            sweep_direction = sweep_rate
176
177        # Calculate the next servo position.
178        next_angle = actuator.angle + sweep_direction / 50.0
179
180        # Change activity at the limits
181        if next_angle > 180.0:
182            sweep_direction = -sweep_direction
183            next_angle = actuator.angle + sweep_direction / 50.0
184
185        elif next_angle < 0.0:
186            print("Ending sweep motion.")
187            next_angle = 0.0
188            sweep_direction = 0.0
189
190        # Send the angle to the actuator.
191        actuator.angle = next_angle
192
193    #---- periodic console output -----------------------------------
194    # Poll the time stamp to decide whether to emit console output.
195    if now >= next_print_time:
196
197        # Advance the time stamp to the next event time.
198        next_print_time += second / 2
199
200        # Periodically print the readings on the console.
201        print(f"({volts}, {pressure}, {actuator.angle})")
202
203    #---- end of the event loop -------------------------------------