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:
Soft Sensing Examples - Adafruit Circuit Playground Bluefruit
Hobby Servo Examples - Adafruit Circuit Playground Bluefruit
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 -------------------------------------
|