Merge pull request #443 from grajohnt/TrellisM4_jt
NeoTrellisM4 Grains of Sand Demo
This commit is contained in:
commit
42de0f426f
1 changed files with 289 additions and 0 deletions
289
NeoTrellis_M4_Sand/code.py
Normal file
289
NeoTrellis_M4_Sand/code.py
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
# Digital sand demo uses the accelerometer to move sand particles in a
|
||||||
|
# realistic way. Tilt the board to see the sand grains tumble around and light
|
||||||
|
# up LEDs. Based on the code created by Phil Burgess and Dave Astels, see:
|
||||||
|
# https://learn.adafruit.com/digital-sand-dotstar-circuitpython-edition/code
|
||||||
|
# https://learn.adafruit.com/animated-led-sand
|
||||||
|
# Ported (badly) to NeoTrellis M4 by John Thurmond
|
||||||
|
#
|
||||||
|
# The MIT License (MIT)
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Tony DiCola
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
import time
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import adafruit_trellism4
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import adafruit_adxl34x
|
||||||
|
|
||||||
|
N_GRAINS = 8 # Number of grains of sand
|
||||||
|
WIDTH = 8 # Display width in pixels
|
||||||
|
HEIGHT = 4 # Display height in pixels
|
||||||
|
NUMBER_PIXELS = WIDTH * HEIGHT
|
||||||
|
MAX_FPS = 10 # Maximum redraw rate, frames/second
|
||||||
|
|
||||||
|
MAX_X = WIDTH * 256 - 1
|
||||||
|
MAX_Y = HEIGHT * 256 - 1
|
||||||
|
|
||||||
|
|
||||||
|
class Grain:
|
||||||
|
"""A simple struct to hold position and velocity information
|
||||||
|
for a single grain."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize grain position and velocity."""
|
||||||
|
self.x = 0
|
||||||
|
self.y = 0
|
||||||
|
self.vx = 0
|
||||||
|
self.vy = 0
|
||||||
|
|
||||||
|
grains = [Grain() for _ in range(N_GRAINS)]
|
||||||
|
|
||||||
|
color = random.randint(1, 254) # Set a random color to start
|
||||||
|
current_press = set() # Get ready for button presses
|
||||||
|
|
||||||
|
# Set up Trellis and accelerometer
|
||||||
|
trellis = adafruit_trellism4.TrellisM4Express(rotation=0)
|
||||||
|
i2c = busio.I2C(board.ACCELEROMETER_SCL, board.ACCELEROMETER_SDA)
|
||||||
|
sensor = adafruit_adxl34x.ADXL345(i2c)
|
||||||
|
|
||||||
|
oldidx = 0
|
||||||
|
newidx = 0
|
||||||
|
delta = 0
|
||||||
|
newx = 0
|
||||||
|
newy = 0
|
||||||
|
|
||||||
|
occupied_bits = [False for _ in range(WIDTH * HEIGHT)]
|
||||||
|
|
||||||
|
def index_of_xy(x, y):
|
||||||
|
"""Convert an x/column and y/row into an index into
|
||||||
|
a linear pixel array.
|
||||||
|
|
||||||
|
:param int x: column value
|
||||||
|
:param int y: row value
|
||||||
|
"""
|
||||||
|
return (y >> 8) * WIDTH + (x >> 8)
|
||||||
|
|
||||||
|
|
||||||
|
def already_present(limit, x, y):
|
||||||
|
"""Check if a pixel is already used.
|
||||||
|
|
||||||
|
:param int limit: the index into the grain array of
|
||||||
|
the grain being assigned a pixel Only grains already
|
||||||
|
allocated need to be checks against.
|
||||||
|
:param int x: proposed clumn value for the new grain
|
||||||
|
:param int y: proposed row valuse for the new grain
|
||||||
|
"""
|
||||||
|
for j in range(limit):
|
||||||
|
if x == grains[j].x or y == grains[j].y:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wheel(pos):
|
||||||
|
# Input a value 0 to 255 to get a color value.
|
||||||
|
# The colours are a transition r - g - b - back to r.
|
||||||
|
if pos < 0 or pos > 255:
|
||||||
|
return 0, 0, 0
|
||||||
|
if pos < 85:
|
||||||
|
return int(255 - pos*3), int(pos*3), 0
|
||||||
|
if pos < 170:
|
||||||
|
pos -= 85
|
||||||
|
return 0, int(255 - pos*3), int(pos*3)
|
||||||
|
pos -= 170
|
||||||
|
return int(pos * 3), 0, int(255 - (pos*3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for g in grains:
|
||||||
|
placed = False
|
||||||
|
while not placed:
|
||||||
|
g.x = random.randint(0, WIDTH * 256 - 1)
|
||||||
|
g.y = random.randint(0, HEIGHT * 256 - 1)
|
||||||
|
placed = not occupied_bits[index_of_xy(g.x, g.y)]
|
||||||
|
occupied_bits[index_of_xy(g.x, g.y)] = True
|
||||||
|
g.vx = 0
|
||||||
|
g.vy = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Display frame rendered on prior pass. It's done immediately after the
|
||||||
|
# FPS sync (rather than after rendering) for consistent animation timing.
|
||||||
|
|
||||||
|
for i in range(NUMBER_PIXELS):
|
||||||
|
|
||||||
|
# Some color options:
|
||||||
|
|
||||||
|
# Random color every refresh
|
||||||
|
#trellis.pixels[(i%8, i//8)] = wheel(random.randint(1, 254)) if occupied_bits[i] else (0, 0, 0)
|
||||||
|
|
||||||
|
# Color by pixel (meh - needs work)
|
||||||
|
#trellis.pixels[(i%8, i//8)] = wheel(i*2) if occupied_bits[i] else (0, 0, 0)
|
||||||
|
|
||||||
|
# Change color to random on button press, or cycle when you hold one down
|
||||||
|
trellis.pixels[(i%8, i//8)] = wheel(color) if occupied_bits[i] else (0, 0, 0)
|
||||||
|
|
||||||
|
# Set as single color
|
||||||
|
#trellis.pixels[(i//8,i%8)] = (255, 0, 0) if occupied_bits[i] else (0, 0, 0)
|
||||||
|
|
||||||
|
# TODO: Change color depending on which button you press?
|
||||||
|
|
||||||
|
# Change color to a new random color on button press
|
||||||
|
pressed = set(trellis.pressed_keys)
|
||||||
|
for press in pressed - current_press:
|
||||||
|
if press:
|
||||||
|
print("Pressed:", press)
|
||||||
|
color = random.randint(1, 254)
|
||||||
|
print("Color:", color)
|
||||||
|
|
||||||
|
# Read accelerometer...
|
||||||
|
f_x, f_y, f_z = sensor.acceleration
|
||||||
|
|
||||||
|
# I had to manually scale these to get them in the -128 to 128 range-ish - should be done better
|
||||||
|
f_x = int(f_x * 9.80665 * 16704/1000)
|
||||||
|
f_y = int(f_y * 9.80665 * 16704/1000)
|
||||||
|
f_z = int(f_z * 9.80665 * 16704/1000)
|
||||||
|
|
||||||
|
ax = f_x >> 3 # Transform accelerometer axes
|
||||||
|
ay = f_y >> 3 # to grain coordinate space
|
||||||
|
az = abs(f_z) >> 6 # Random motion factor
|
||||||
|
|
||||||
|
print("%6d %6d %6d"%(ax,ay,az))
|
||||||
|
az = 1 if (az >= 3) else (4 - az) # Clip & invert
|
||||||
|
ax -= az # Subtract motion factor from X, Y
|
||||||
|
ay -= az
|
||||||
|
az2 = (az << 1) + 1 # Range of random motion to add back in
|
||||||
|
|
||||||
|
# Adjust axes for the NeoTrellis M4 (probably better ways to do this)
|
||||||
|
ax2 = ax
|
||||||
|
ax = -ay
|
||||||
|
ay = ax2
|
||||||
|
|
||||||
|
# ...and apply 2D accel vector to grain velocities...
|
||||||
|
v2 = 0 # Velocity squared
|
||||||
|
v = 0.0 # Absolute velociy
|
||||||
|
for g in grains:
|
||||||
|
|
||||||
|
g.vx += ax + random.randint(0, az2) # A little randomness makes
|
||||||
|
g.vy += ay + random.randint(0, az2) # tall stacks topple better!
|
||||||
|
|
||||||
|
# Terminal velocity (in any direction) is 256 units -- equal to
|
||||||
|
# 1 pixel -- which keeps moving grains from passing through each other
|
||||||
|
# and other such mayhem. Though it takes some extra math, velocity is
|
||||||
|
# clipped as a 2D vector (not separately-limited X & Y) so that
|
||||||
|
# diagonal movement isn't faster
|
||||||
|
|
||||||
|
v2 = g.vx * g.vx + g.vy * g.vy
|
||||||
|
if v2 > 65536: # If v^2 > 65536, then v > 256
|
||||||
|
v = math.floor(math.sqrt(v2)) # Velocity vector magnitude
|
||||||
|
g.vx = (g.vx // v) << 8 # Maintain heading
|
||||||
|
g.vy = (g.vy // v) << 8 # Limit magnitude
|
||||||
|
|
||||||
|
# ...then update position of each grain, one at a time, checking for
|
||||||
|
# collisions and having them react. This really seems like it shouldn't
|
||||||
|
# work, as only one grain is considered at a time while the rest are
|
||||||
|
# regarded as stationary. Yet this naive algorithm, taking many not-
|
||||||
|
# technically-quite-correct steps, and repeated quickly enough,
|
||||||
|
# visually integrates into something that somewhat resembles physics.
|
||||||
|
# (I'd initially tried implementing this as a bunch of concurrent and
|
||||||
|
# "realistic" elastic collisions among circular grains, but the
|
||||||
|
# calculations and volument of code quickly got out of hand for both
|
||||||
|
# the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.)
|
||||||
|
|
||||||
|
for g in grains:
|
||||||
|
newx = g.x + g.vx # New position in grain space
|
||||||
|
newy = g.y + g.vy
|
||||||
|
if newx > MAX_X: # If grain would go out of bounds
|
||||||
|
newx = MAX_X # keep it inside, and
|
||||||
|
g.vx //= -2 # give a slight bounce off the wall
|
||||||
|
elif newx < 0:
|
||||||
|
newx = 0
|
||||||
|
g.vx //= -2
|
||||||
|
if newy > MAX_Y:
|
||||||
|
newy = MAX_Y
|
||||||
|
g.vy //= -2
|
||||||
|
elif newy < 0:
|
||||||
|
newy = 0
|
||||||
|
g.vy //= -2
|
||||||
|
|
||||||
|
oldidx = index_of_xy(g.x, g.y) # prior pixel
|
||||||
|
newidx = index_of_xy(newx, newy) # new pixel
|
||||||
|
# If grain is moving to a new pixel...
|
||||||
|
if oldidx != newidx and occupied_bits[newidx]:
|
||||||
|
# but if that pixel is already occupied...
|
||||||
|
# What direction when blocked?
|
||||||
|
delta = abs(newidx - oldidx)
|
||||||
|
if delta == 1: # 1 pixel left or right
|
||||||
|
newx = g.x # cancel x motion
|
||||||
|
# and bounce X velocity (Y is ok)
|
||||||
|
g.vx //= -2
|
||||||
|
newidx = oldidx # no pixel change
|
||||||
|
elif delta == WIDTH: # 1 pixel up or down
|
||||||
|
newy = g.y # cancel Y motion
|
||||||
|
# and bounce Y velocity (X is ok)
|
||||||
|
g.vy //= -2
|
||||||
|
newidx = oldidx # no pixel change
|
||||||
|
else: # Diagonal intersection is more tricky...
|
||||||
|
# Try skidding along just one axis of motion if
|
||||||
|
# possible (start w/ faster axis). Because we've
|
||||||
|
# already established that diagonal (both-axis)
|
||||||
|
# motion is occurring, moving on either axis alone
|
||||||
|
# WILL change the pixel index, no need to check
|
||||||
|
# that again.
|
||||||
|
if abs(g.vx) > abs(g.vy): # x axis is faster
|
||||||
|
newidx = index_of_xy(newx, g.y)
|
||||||
|
# that pixel is free, take it! But...
|
||||||
|
if not occupied_bits[newidx]:
|
||||||
|
newy = g.y # cancel Y motion
|
||||||
|
g.vy //= -2 # and bounce Y velocity
|
||||||
|
else: # X pixel is taken, so try Y...
|
||||||
|
newidx = index_of_xy(g.x, newy)
|
||||||
|
# Pixel is free, take it, but first...
|
||||||
|
if not occupied_bits[newidx]:
|
||||||
|
newx = g.x # Cancel X motion
|
||||||
|
g.vx //= -2 # Bounce X velocity
|
||||||
|
else: # both spots are occupied
|
||||||
|
newx = g.x # Cancel X & Y motion
|
||||||
|
newy = g.y
|
||||||
|
g.vx //= -2 # Bounce X & Y velocity
|
||||||
|
g.vy //= -2
|
||||||
|
newidx = oldidx # Not moving
|
||||||
|
else: # y axis is faster. start there
|
||||||
|
newidx = index_of_xy(g.x, newy)
|
||||||
|
# Pixel's free! Take it! But...
|
||||||
|
if not occupied_bits[newidx]:
|
||||||
|
newx = g.x # Cancel X motion
|
||||||
|
g.vx //= -2 # Bounce X velocity
|
||||||
|
else: # Y pixel is taken, so try X...
|
||||||
|
newidx = index_of_xy(newx, g.y)
|
||||||
|
# Pixel is free, take it, but first...
|
||||||
|
if not occupied_bits[newidx]:
|
||||||
|
newy = g.y # cancel Y motion
|
||||||
|
g.vy //= -2 # and bounce Y velocity
|
||||||
|
else: # both spots are occupied
|
||||||
|
newx = g.x # Cancel X & Y motion
|
||||||
|
newy = g.y
|
||||||
|
g.vx //= -2 # Bounce X & Y velocity
|
||||||
|
g.vy //= -2
|
||||||
|
newidx = oldidx # Not moving
|
||||||
|
occupied_bits[oldidx] = False
|
||||||
|
occupied_bits[newidx] = True
|
||||||
|
g.x = newx
|
||||||
|
g.y = newy
|
||||||
Loading…
Reference in a new issue