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
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# cpb_ticklish.py

# Demonstrate reactive behavior: an idle movement sequence, interrupted with
# reactive movements in response to a sensor input.

# This sketch assumes two external circuits: a resistive soft-sensor connected
# to A2, and a hobby servo on 'SDA A5'.  Please refer to the cpb_soft_sensing.py
# and cpb_sine_servo.py sketches for more details on the wiring.

# ----------------------------------------------------------------
# Import any needed standard Python modules.
import time, math

# Import the board-specific input/output library.
from adafruit_circuitplayground import cp

# Import the low-level hardware libraries.
import board
import digitalio
import analogio
import pwmio

# Import the Adafruit helper library.
from adafruit_motor import servo

# ----------------------------------------------------------------
# Initialize hardware.

# Configure the digital input pin used for its pullup resistor bias voltage.
bias = digitalio.DigitalInOut(board.D10)        # pad A3
bias.switch_to_input(pull=digitalio.Pull.UP)

# Configure the analog input pin used to measure the sensor voltage.
sensor = analogio.AnalogIn(board.A2)
scaling = sensor.reference_voltage / (2**16)

# Create a PWMOut object on pad SDA A5 to generate control signals.
pwm = pwmio.PWMOut(board.A5, duty_cycle=0, frequency=50)

# Create a Servo object which controls a hobby servo using the PWMOut.
actuator = servo.Servo(pwm, min_pulse=1000, max_pulse=2000)
actuator.angle = 0

# Configure the NeoPixel LED array for bulk update using show().
cp.pixels.auto_write = False

# Turn down the NeoPixel brightness, otherwise it is somewhat blinding.
cp.pixels.brightness = 0.5

# ----------------------------------------------------------------
# Define functions used by the main loop.

# Use the CPB NeoPixel LEDS as a dial gauge.  A normalized value between zero
# and one is shown as a spot of light like a dial needle.  The USB connector is
# considered to be the top; the LED to its right is zero, with values increasing
# clockwise around to the LED to the left of the USB.  The spot is anti-aliased
# to increase precision, i.e. it cross-fades between LEDs as the value changes.

def update_LED_dial(value):
    # Clamp the input to the range limits.
    clamped = min(max(value, 0.0), 1.0)

    # Convert the normalized value to an indicator angle in degrees with respect to the top.
    indicator = (clamped * 300) + 30

    # Project the angle onto the NeoPixels. The list assumes the Neopixels are
    # uniformly spaced at 30 degree intervals, with the bottom position absent
    # (e.g. at the battery connector).
    for i, led_angle in enumerate((330, 300, 270, 240, 210, 150, 120, 90, 60, 30)):
        diff = abs(indicator - led_angle)
        if diff < 30:
            c = min(max(255 - int(10*diff), 0), 255)
            cp.pixels[i] = (c, c, c) # shade of gray
        else:
            cp.pixels[i] = (0,0,0)   # no light

    # Send new data to the physical LED chain.
    cp.pixels.show()

# ----------------------------------------------------------------
# Initialize global variables for the main loop.

# Convenient time constant expressed in nanoseconds.
second = 1000000000

# Integer time stamp for the next console output.
next_print_time = time.monotonic_ns()

# Previous touch value, used to detect changes.
last_touch = 0

# Behavior mode symbol.  This is the state label for a simple behavioral state machine.
behavior_mode = 'idle'

# Integer time stamp for a state timeout.
next_timeout_time = 0

# Integer time stamp for next behavior activity to begin.
next_activity_time = time.monotonic_ns() + 2 * second

# Flag to trigger motion.
sweep_start = False

# Commanded value for motion in degrees/second.
sweep_rate = 0

# Current value for motion speed in degrees/second.
sweep_direction = 0

# Integer time stamp for next servo update.
next_servo_update = time.monotonic_ns()

# ----------------------------------------------------------------
# Begin the main processing loop.
while True:

    # Read the current integer clock.
    now = time.monotonic_ns()

    #---- soft sensor input and display -----------------------------
    # Read the integer sensor value and scale it to a value in Volts.
    volts = sensor.value * scaling

    # Normalize the soft sensor reading.  Typically you'll need to adjust these values to your device.
    low_pressure_voltage  = 0.65
    high_pressure_voltage = 0.20
    pressure = (low_pressure_voltage - volts) / (low_pressure_voltage - high_pressure_voltage)

    # Update the LED dial gauge display.
    update_LED_dial(pressure)

    #---- behavior state machine update -----------------------------
    if behavior_mode == 'idle':

        # Check whether the touch sensor has been activated
        if pressure > 0.5:
            # Change behavior state, which will create movement on the next cycle.
            behavior_mode = 'tickled'
            next_activity_time = now
            next_timeout_time = now + 4 * second
            print("Entering tickled state.")

        # Check whether to start another idle movement
        elif now >= next_activity_time:
            next_activity_time += 12 * second
            sweep_rate = 60 # deg/sec
            sweep_start = True

    elif behavior_mode == 'tickled':

        # Check whether the touch sensor is still activated
        if pressure > 0.5:
            next_timeout_time = now + 4 * second

        # Check whether to start another tickled movement
        if now >= next_activity_time:
            next_activity_time += 3 * second
            sweep_rate = 120 # deg/sec
            sweep_start = True

        elif now >= next_timeout_time:
            print("Ending tickled state.")
            next_activity_time = now + 6*second
            behavior_mode = 'idle'

    #---- periodic servo motion commands ----------------------------
    # If the time has arrived to update the servo command signal:
    if now >= next_servo_update:
        next_servo_update += 20000000  # 20 msec in nanoseconds (50 Hz update)

        # If a start event has been received, move in a positive direction.
        if sweep_start is True:
            print(f"Starting sweep motion at {sweep_rate} deg/sec.")
            sweep_start = False
            sweep_direction = sweep_rate

        # Calculate the next servo position.
        next_angle = actuator.angle + sweep_direction / 50.0

        # Change activity at the limits
        if next_angle > 180.0:
            sweep_direction = -sweep_direction
            next_angle = actuator.angle + sweep_direction / 50.0

        elif next_angle < 0.0:
            print("Ending sweep motion.")
            next_angle = 0.0
            sweep_direction = 0.0

        # Send the angle to the actuator.
        actuator.angle = next_angle

    #---- periodic console output -----------------------------------
    # Poll the time stamp to decide whether to emit console output.
    if now >= next_print_time:

        # Advance the time stamp to the next event time.
        next_print_time += second / 2

        # Periodically print the readings on the console.
        print(f"({volts}, {pressure}, {actuator.angle})")

    #---- end of the event loop -------------------------------------