Skip to content

Instantly share code, notes, and snippets.

@alexanderlavrushko
Last active December 18, 2021 19:29
Show Gist options
  • Save alexanderlavrushko/0f689371735c58a5344df77a74092d0a to your computer and use it in GitHub Desktop.
Save alexanderlavrushko/0f689371735c58a5344df77a74092d0a to your computer and use it in GitHub Desktop.
Animate NeoPixels and keep total brightness on the same level, make your eyes comfortable by avoiding blinks and flashes. Control with BluefruitConnect app. YouTube: https://youtu.be/09F7x-rpTm0
# NeoPixel animations with constant total brightness.
# Made for Circuit Playground Bluefruit
# Follow this guide to setup the board - https://learn.adafruit.com/adafruit-circuit-playground-bluefruit
#
# Can be controlled from Bluefruit LE Connect application - https://learn.adafruit.com/bluefruit-le-connect/ios-setup
# In the app:
# 1. Connect to CIRCUITPY device
# 2. In menu Controller => Control Pad:
# - Button 1 - set animation type "loop classic"
# - Button 2 - set animation type "loop chase" (it's set by default)
# - Button 3 - set animation type "scanner"
# - Button 4 - set animation type "fire"
# - Button Up - increase animation speed
# - Button Down - slow down animation speed
# - Button Right - increase speed of changing colors (default is 1 - loop takes about 2 minutes, max 10 - loop takes 1 sec)
# - Button Left - slow down speed of changing colors
# 3. In menu Controller => Color Picker:
# - Select a color and send it - will set the current color (speed of changing colors automatically set to 0)
import board
import neopixel
import time
import math
from adafruit_bluefruit_connect.packet import Packet
from adafruit_bluefruit_connect.color_packet import ColorPacket
from adafruit_bluefruit_connect.button_packet import ButtonPacket
# Other packets are not used, we import them to avoid interruption caused by "unrecognized packet" if the phone sends them.
from adafruit_bluefruit_connect.accelerometer_packet import AccelerometerPacket
from adafruit_bluefruit_connect.gyro_packet import GyroPacket
from adafruit_bluefruit_connect.location_packet import LocationPacket
from adafruit_bluefruit_connect.magnetometer_packet import MagnetometerPacket
from adafruit_bluefruit_connect.quaternion_packet import QuaternionPacket
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
ble = BLERadio()
uart_service = UARTService()
advertisement = ProvideServicesAdvertisement(uart_service)
num_pixels = 10
pixels = neopixel.NeoPixel(board.NEOPIXEL, num_pixels, brightness=1, auto_write=False)
num_frames = 6000
min_num_frames = num_frames // 10
max_num_frames = num_frames * 4
frames_per_second = 60
frame_duration = 1 / frames_per_second
current_frame_index = 0
color_change_progress = 0
color_change_step = 1 / (120 * frames_per_second)
color_change_speed = 1
selected_color = (255, 41, 0)
# selected_color = (255, 24, 52)
# selected_color = (255, 41, 0)
current_color = selected_color
activation_values = []
for i in range(num_pixels):
activation_values.append(0)
ANIMATION_LOOP_CLASSIC = 1
ANIMATION_LOOP_CHASE = 2
ANIMATION_FIRE = 3
ANIMATION_SCANNER = 4
# =============
# Helper functions.
# =============
def my_colorwheel(pos):
# Input value 0 to 255, output is tuple = (red, green, blue).
if pos < 0 or pos > 255:
r = g = b = 0
elif pos < 70:
r = int(pos * 3)
g = int(255 - pos * 3)
b = 0
elif pos < 100:
# This part adds Green and Blue, because it's too dark when Red is alone.
if pos < 85:
r = int(pos * 3)
pos -= 70
g = int(45 - pos * 2)
b = 0
else:
pos -= 85
r = int(255 - pos * 3)
g = int(15 - pos)
b = int(pos * 3)
elif pos < 170:
pos -= 85
r = int(255 - pos * 3)
g = 0
b = int(pos * 3)
else:
pos -= 170
r = 0
g = int(pos * 3)
b = int(255 - pos * 3)
return (r, g, b)
def apply_activation_values(values):
for i in range(num_pixels):
current_pixel = (0, 0, 0)
activation = values[i]
r = sum_component(current_pixel[0], current_color[0] * activation)
g = sum_component(current_pixel[1], current_color[1] * activation)
b = sum_component(current_pixel[2], current_color[2] * activation)
new_pixel = (r, g, b)
pixels[i] = new_pixel
def create_animation(type):
if type == ANIMATION_LOOP_CLASSIC:
order = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
computer = AnimationComputer(order, num_pixels, activation=ActivationQuadratic(), activation_length=5.2, brightness=1.2, progress_multiplier=100)
return AnimationMixer([computer])
elif type == ANIMATION_LOOP_CHASE:
order1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
computer1 = AnimationComputer(order1, num_pixels, activation=ActivationQuadratic(), activation_length=6, brightness=0.6, progress_multiplier=62)
order2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
computer2 = AnimationComputer(order2, num_pixels, activation=ActivationLinear(), activation_length=3.2, brightness=0.59, progress_multiplier=100)
return AnimationMixer([computer1, computer2])
elif type == ANIMATION_SCANNER:
mult = 100
loop1 = [0, 1, 2, 3, 4, 3, 2, 1]
tail1 = AnimationComputer(loop1, num_pixels, activation=ActivationQuadratic(), activation_length=6, brightness=0.3, progress_multiplier=mult)
head1 = AnimationComputer(loop1, num_pixels, activation=ActivationConstant(), activation_length=1.2, brightness=0.6, advance=4.8, progress_multiplier=mult)
loop2 = [5, 6, 7, 8, 9, 8, 7, 6]
tail2 = AnimationComputer(loop2, num_pixels, activation=ActivationQuadratic(), activation_length=6, brightness=0.3, progress_multiplier=mult)
head2 = AnimationComputer(loop2, num_pixels, activation=ActivationConstant(), activation_length=1.2, brightness=0.6, advance=4.8, progress_multiplier=mult)
return AnimationMixer([tail1, head1, tail2, head2])
elif type == ANIMATION_FIRE:
# Fire with constant total brightness.
loop1 = [0, 9, 1, 8, 2, 7, 3, 6, 4, 5]
flames1 = AnimationComputer(loop1, num_pixels, activation=ActivationQuadratic(), activation_length=5, brightness=0.55, progress_multiplier=120)
loop2 = [3, 2, 4, 1, 5, 0, 6, 9, 7, 8]
flames2 = AnimationComputer(loop2, num_pixels, activation=ActivationQuadratic(), activation_length=7, brightness=0.6, progress_multiplier=78)
return AnimationMixer([flames1, flames2])
# Fire with jumpy total brightness.
# loop1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# quick_wave = AnimationComputer(loop1, num_pixels, activation=ActivationCosinusKxPlusB(k=6*math.pi, b=math.pi, middle=0.2), activation_length=10, brightness=0.5, progress_multiplier=100)
# loop2 = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
# slow_wave = AnimationComputer(loop2, num_pixels, activation=ActivationQuadratic(), activation_length=5, brightness=0.6, progress_multiplier=78)
# return AnimationMixer([quick_wave, slow_wave])
else:
return None
def create_indexes_all_pixels(num):
array = []
for i in range(num):
array.append(i)
return array
def sum_component(a, b):
result = a + b
if result < 0:
# print("overflow minus", result)
result = 0
elif result > 255:
print("overflow plus", result)
result = 255
return int(result)
def relative_position_in_range(value, my_range):
if value < my_range[0]:
return 0
if value > my_range[1]:
return 1
length = my_range[1] - my_range[0]
return (value - my_range[0]) / length
class ActivationBase(object):
def function(self, x):
return 0
def antiderivative_function(self, x):
return 0
class ActivationConstant(ActivationBase):
def function(self, x):
return 1
def antiderivative_function(self, x):
return x
class ActivationLinear(ActivationBase):
def function(self, x):
return x
def antiderivative_function(self, x):
return x * x / 2
class ActivationQuadratic(ActivationBase):
def function(self, x):
return x * x
def antiderivative_function(self, x):
return x * x * x / 3
class ActivationCosinusKxPlusB(ActivationBase):
def __init__(self, k = 1, b = 0, middle=0.5):
self.k = k
self.b = b
self.middle = middle
def function(self, x):
return self.middle + 0.5 * math.cos(self.k * x + self.b)
def antiderivative_function(self, x):
return self.middle * x + math.sin(self.k * x + self.b) / (2 * self.k)
class AnimationComputer(object):
def __init__(self, activation_indexes, num_output_values, activation = ActivationConstant(), brightness = 1, activation_length = 3.5, advance = 0, progress_multiplier=1):
self.activation_indexes = activation_indexes
self.activation = activation
self.brightness = brightness
self.activation_length = activation_length
self.activation_offset = advance
self.num_output_values = num_output_values
self.progress_multiplier = progress_multiplier
num_activation_indexes = len(self.activation_indexes)
self.relative_activation_length = self.activation_length / num_activation_indexes
self.relative_activation_offset = self.activation_offset / num_activation_indexes
self.limit_step = 1 / activation_length
self.antiderivative_function_at_0 = activation.antiderivative_function(0)
self.antiderivative_function_at_1 = activation.antiderivative_function(1)
def compute_activation_values(self, progress, result_values):
num_activation_indexes = len(self.activation_indexes)
relative_start = self.progress_multiplier * progress + self.relative_activation_offset
relative_activation_range = (relative_start, relative_start + self.relative_activation_length)
first_activation_index = int(relative_start * num_activation_indexes)
relative_pixel_length = 1 / num_activation_indexes
relative_first_pixel_end = (first_activation_index + 1) / num_activation_indexes
first_pixel_high_limit = relative_position_in_range(relative_first_pixel_end, relative_activation_range)
antiderivative_function_values = [self.antiderivative_function_at_0]
current_high_limit = first_pixel_high_limit
while current_high_limit < 1:
antiderivative_function_values.append(self.activation.antiderivative_function(current_high_limit))
current_high_limit += self.limit_step
antiderivative_function_values.append(self.antiderivative_function_at_1)
for i in range(len(antiderivative_function_values) - 1):
filled_square = antiderivative_function_values[i + 1] - antiderivative_function_values[i]
activation = self.brightness * filled_square * self.activation_length
destination_index = self.activation_indexes[(first_activation_index + i) % num_activation_indexes]
new_value = result_values[destination_index] + activation
result_values[destination_index] = new_value
class AnimationMixer(object):
def __init__(self, animators):
self.animators = animators
def compute_activation_values(self, progress, cumulative_values):
for animator in self.animators:
animator.compute_activation_values(progress, cumulative_values)
def move_to_next_frame():
global current_frame_index
current_frame_index += 1
if current_frame_index >= num_frames:
current_frame_index = 0
def process_current_frame():
global activation_values
global current_color
global color_change_progress
if color_change_speed > 0:
color_index = int(255 * color_change_progress)
current_color = my_colorwheel(color_index)
color_change_progress += color_change_speed * color_change_speed * color_change_step
if color_change_progress > 1:
color_change_progress -= 1
if num_frames < min_num_frames:
pixels.fill(current_color)
else:
for i in range(num_pixels):
activation_values[i] = 0
progress = current_frame_index / num_frames
mixer.compute_activation_values(progress, activation_values)
apply_activation_values(activation_values)
pixels.show()
time.sleep(frame_duration)
move_to_next_frame()
# =============
# Main program.
# =============
mixer = create_animation(ANIMATION_LOOP_CHASE)
while True:
# Advertise when not connected.
ble.start_advertising(advertisement)
while not ble.connected:
process_current_frame()
ble.stop_advertising()
while ble.connected:
if uart_service.in_waiting:
packet = Packet.from_stream(uart_service)
if isinstance(packet, ColorPacket):
print("New color: ", packet.color)
current_color = packet.color
color_change_speed = 0
elif isinstance(packet, ButtonPacket):
if packet.pressed:
current_progress = current_frame_index / num_frames
percent = 0.25
delta = int(percent * num_frames)
if packet.button == ButtonPacket.UP:
print("Button UP pressed, increasing speed")
if num_frames - delta >= min_num_frames:
num_frames -= delta
current_frame_index = int(current_frame_index * (1 - percent))
elif packet.button == ButtonPacket.DOWN:
print("Button DOWN pressed, decreasing speed")
if num_frames + delta <= max_num_frames:
num_frames += delta
current_frame_index = int(current_frame_index * (1 + percent))
elif packet.button == ButtonPacket.LEFT:
if color_change_speed > 0:
color_change_speed -= 1
elif packet.button == ButtonPacket.RIGHT:
if color_change_speed < 10:
color_change_speed += 1
elif packet.button == ButtonPacket.BUTTON_1:
mixer = create_animation(ANIMATION_LOOP_CLASSIC)
elif packet.button == ButtonPacket.BUTTON_2:
mixer = create_animation(ANIMATION_LOOP_CHASE)
elif packet.button == ButtonPacket.BUTTON_3:
mixer = create_animation(ANIMATION_SCANNER)
elif packet.button == ButtonPacket.BUTTON_4:
mixer = create_animation(ANIMATION_FIRE)
else:
print("Unexpected packet: ", packet)
process_current_frame()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment