Last active
December 18, 2021 19:29
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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