Remote Connection Examples - Adafruit Circuit Playground Bluefruit

The following short Python programs will demonstrate linking together Bluefruit board across the network. These require the use of a Python program running on a laptop or desktop to forward data to and from a partner via the IDeATe MQTT server.

Each can be run by copying the program into code.py on the CIRCUITPY drive offered by the board. The text can be pasted directly from this page, or each file can be downloaded from the CircuitPython sample code folder on this site.

Related Pages

Remote Touch

Direct download: cpb_remote_touch.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
# cpb_remote_touch.py

# Use touch sensitive pads and sound to control a lighting display.  The display
# models a diffusion process, with each touch pad contributing an activation.  A
# loud sound or physical tap will trigger a single flash.

# The same process can be coupled to a remote device via the serial port, e.g. by
# retransmitting the serial stream via a MQTT server.  The same touch and sound data
# generates output messages, and input messages are processed the same as local input.

# This program uses only onboard hardware: capacitive touch sensing, sound level
# sensor, NeoPixel LEDs.

# ----------------------------------------------------------------
# Import the standard Python math library and time functions.
import math, time, sys

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

# Import the runtime for checking serial port status.
import supervisor

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

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

# The display update rate is precisely regulated using the following timer variables.
# The default is 20 Hz frame rate as set by step_interval.
step_timer    = 0.0
last_time     = time.monotonic()
step_interval = 0.05

# The core model state is an array of state variables.  Note that the state is
# modeled for 12 uniformly spaced positions, even though only ten are populated
# with NeoPixels.  This state is abstract but might represent heat moving
# through a ring of material.
state = [0.0] * 12  # create 12 element array of zeros

# The remote touch state can be updated via serial port messages.
remote_touches = [False]*6

# Measure the time since the last remote message, and reset after a period without data.
remote_touch_timer = False

# The serial port output rate is regulated using the following timer variables.
serial_timer    = 0.0
serial_interval = 0.5

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

    # Measure elapsed time and wait until the update timer has elapsed.
    now = time.monotonic()
    interval = now - last_time
    last_time = now
    step_timer -= interval
    if step_timer < 0.0:
        step_timer += step_interval

        # Apply a first-order diffusion model to the state.  This loop creates a new
        # array to replace the previous state.  The modulo operator (%) is used to 'wrap
        # around' the end of the array to close the loop.
        N = len(state)
        new = [0.0] * N
        for i in range(N):
            new[i] = 0.9 * state[i] + 0.05 * state[(i-1) % N] + 0.05 * state[(i+1) % N]

        # Replace the previous state.
        state = new

        # Apply a slow decay to represent leakage outside the ring.
        for i in range(N):
            state[i] = 0.97 * state[i]

        # Check the serial input
        if supervisor.runtime.serial_bytes_available:
            line = sys.stdin.readline()
            tokens = line.split()
            if len(tokens) == 6:
                try:                
                    remote_touches = [int(token) > 0 for token in tokens]
                    remote_touch_timer = 4.0
                except ValueError:
                    pass
        
        # Apply touch inputs as excitation blended into nearby samples.
        if cp.touch_A1 or remote_touches[0]: state[8]  += 0.1 * (1.0 - state[ 8])
        if cp.touch_A2 or remote_touches[1]: state[10] += 0.1 * (1.0 - state[10])
        if cp.touch_A3 or remote_touches[2]: state[11] += 0.1 * (1.0 - state[11])
        if cp.touch_A4 or remote_touches[3]: state[ 2] += 0.1 * (1.0 - state[ 2])
        if cp.touch_A5 or remote_touches[4]: state[ 3] += 0.1 * (1.0 - state[ 3])
        if cp.touch_A6 or remote_touches[5]: state[ 4] += 0.1 * (1.0 - state[ 4])

        # Update the LED colors to reflect the state.  Note that states 0 and 6
        # are invisible as these positions do not have LEDs.
        for i, s in enumerate(state):
            if i != 0 and i != 6:
                # Clamp the state value between 0.0 and 1.0.
                value = min(max(s, 0.0), 1.0)

                # Map the state index to an LED position index.
                pos = i-1 if i < 6 else i-2

                # Map 0.0 to dim blue and 1.0 to bright red.
                cp.pixels[pos] = (255 * value, 0, 50 * (1.0 - value))

        # If a loud sound is detected, override the normal display to flash white.
        # A physical tap will also trigger the flash.
        if cp.loud_sound():
            # cp.pixels.fill((255,255,255))
            pass

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

    serial_timer -= interval
    if serial_timer < 0.0:
        serial_timer += serial_interval
        touches = ["1" if i else "0" for i in [cp.touch_A1, cp.touch_A2, cp.touch_A3, cp.touch_A4, cp.touch_A5, cp.touch_A6]]
        print(" ".join(touches))

    # if the remote touch is active, check for timeout
    if remote_touch_timer is not False:
        remote_touch_timer -= interval
        if remote_touch_timer < 0.0:
            remote_touch_timer = False
            remote_touches = [False]*6

Remote Tones

Direct download: cpb_remote_tones.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
# cpb_remote_tones.py

# Use touch sensitive pads to control a tone generator.

# The same process can be coupled to a remote device via the serial port,
# e.g. by retransmitting the serial stream via a MQTT server.  The same touch
# data generates output messages, and input messages are processed the same as
# local input.

# This program uses only onboard hardware: capacitive touch sensing, speaker.

# ----------------------------------------------------------------
# Import the standard Python math library and time functions.
import math, time, sys

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

# Import the runtime for checking serial port status.
import supervisor

#================================================================
# Define a class to represent the algorithmic composition task.
class Arpeggiator(object):
    
    # Define a dictionary of arpeggio patterns as a class attribute.
    patterns = { 'maj': (0, 4, 7, 12),
                 'min': (0, 3, 7, 12),
                 'maj7': (0, 4, 7, 11),
                 'min7': (0, 3, 7, 11),
                 'dim7': (0, 3, 6, 10),
                 }

    # Initialize an instance of the class.
    def __init__(self):
        # Current compositional state.
        self.root_note = 60     # middle-C as a MIDI note
        self.tonality = 'maj'   # key for the patterns dictionary
        self.tempo = 60         # beats per minute

        # Internal state variables.
        self._index = 0                        # arpeggio index of next note to play
        self._direction = 1                    # index step direction
        self._next_time = time.monotonic_ns()  # clock time in nsec for next note update

        return

    # Update function to be called frequently to recompute outputs.
    def poll(self):
        now = time.monotonic_ns()
        if now >= self._next_time:
            self._next_time += 60000000000 // int(self.tempo)   # add nanoseconds per beat

            # look up the current arpeggio pattern
            pattern = self.patterns[self.tonality]

            # Select the next note play and advance the position.
            if self._index <= 0:
                # choose the root note of the arpeggio and advance up one step
                note = self.root_note                
                self._index = 1
                self._direction = 1

            elif self._index >= len(pattern)-1:
                # choose the top note of the arpeggio and advance down one step
                note = self.root_note + pattern[-1]
                self._index = len(pattern)-2
                self._direction = -1
                
            else:
                # play either a rising or falling tone within the arpeggio
                note = self.root_note + pattern[self._index]
                self._index += self._direction

            # Compute the tone to play and update the speaker output.
            freq = self.midi_to_freq(note)
            cp.stop_tone()
            cp.start_tone(freq)

    # ----------------------------------------------------------------
    # Convert MIDI note value to frequency. This applies an equal temperament scale.
    def midi_to_freq(self, midi_note):
        MIDI_A0 = 21
        freq_A0 = 27.5
        return freq_A0 * math.pow(2.0, (midi_note - MIDI_A0) / 12.0)

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

# Create an tone generator object, an instance of the Arpeggiator class.
arpeggiator = Arpeggiator()

# The local touch state can be updated from the capacitance sensing inputs.
local_touches = [False]*6

# Regulate the rate at which the touch sensors are sampled.
sensing_timer = time.monotonic_ns()

# The remote touch state can be updated via serial port messages.
remote_touches = [False]*6

# Measure the time since the last touch and reset after a period without data.
melody_timer = False

# Regulate the rate of local touch data sent to the serial port output.
serial_timer = time.monotonic_ns()

# ----------------------------------------------------------------
# Begin the main event processing loop.
while True:
    # Read the nanosecond clock.
    now = time.monotonic_ns()
    
    # Check the serial input for a new line of remote data.
    if supervisor.runtime.serial_bytes_available:
        line = sys.stdin.readline()
        tokens = line.split()
        if len(tokens) == 6:
            try:
                # convert a list of six numbers into True/False values
                remote_touches = [int(token) > 0 for token in tokens]
                melody_timer = now + 4000000000  # 4 second timeout
            except ValueError:
                # ignore any input with a formatting error
                pass

    # Measure elapsed time and wait until the input sensing timer has elapsed.
    if now >= sensing_timer:
        sensing_timer += 100000000 # 0.1 second

        # Read all the local touch sensors.
        local_touches = [cp.touch_A1, cp.touch_A2, cp.touch_A3, cp.touch_A4, cp.touch_A5, cp.touch_A6]

        # If touched, reset the fadeout timer.
        if any(local_touches):
            melody_timer = now + 4000000000  # 4 second timeout
            
        # If the serial rate timer has elapsed, also send it to the network.
        if now >= serial_timer:
            serial_timer += 500000000 # 0.5 second
            touches = ["1" if i else "0" for i in local_touches]
            print(" ".join(touches))

    # If anything has been recently touched, update the tone generation.
    if any(local_touches) or any(remote_touches):
        
        if local_touches[0] or remote_touches[0]:
            arpeggiator.root_note = 60
            
        elif local_touches[1] or remote_touches[1]:
            arpeggiator.root_note = 72

        elif local_touches[2] or remote_touches[2]:
            arpeggiator.root_note = 84
            
        if local_touches[3] or remote_touches[3]:
            arpeggiator.tonality = 'maj'

        elif local_touches[4] or remote_touches[4]:
            arpeggiator.tonality = 'min'
            
        elif local_touches[5] or remote_touches[5]:
            arpeggiator.tonality = 'dim7'

        arpeggiator.tempo = 120

        # clear any events
        local_touches = [False]*6
        remote_touches = [False]*6


    # If the melody is active, run the tone generator then check the fadeout process.
    if melody_timer is not False:
        
        # Run the tone generator.
        arpeggiator.poll()

        # If the melody timer has elapsed, advance the fadeout process.
        if now > melody_timer:

            # If all done, stop playing.
            if arpeggiator.tempo <= 30:
                melody_timer = False
                cp.stop_tone()

            else:
                melody_timer += 1000000000 # 1 sec
                arpeggiator.tempo *= 0.9