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# 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 -------------------------------------