craig richardson
Published

DIY 15*10 Dual Mode LED Matrix

A homemade LED matrix controlled by a Raspberry Pi Zero for animated pictures and disco lights.

IntermediateFull instructions provided787
DIY 15*10 Dual Mode LED Matrix

Things used in this project

Hardware components

led strip
×3
power supply
×1
level shifter
×1
mini breadboard
×1
plastic box
×1
rocker switch
×1
Pi Zero
×1
power supply
×1

Story

Read more

Schematics

image1.png

Code

disco.py

Python
import time, math 
import RPi.GPIO as GPIO
from neopixel import * # See https://learn.adafruit.com/neopixels-on-raspberry-pi/software 
from random import *

# LED strip configuration:
LED_COUNT      = 150     # Number of LED pixels.
LED_PIN        = 18      # GPIO pin connected to the pixels (must support PWM!).
LED_FREQ_HZ    = 800000  # LED signal frequency in hertz (usually 800khz)
LED_DMA        = 5       # DMA channel to use for generating signal (try 5)
LED_BRIGHTNESS = 128     # Set to 0 for darkest and 255 for brightest
LED_INVERT     = False   # True to invert the signal (when using NPN transistor level shift)
LED_CHANNEL		= 0		 # Set to '1' for GPIOs 13, 19, 41,45 OR 53
LED_STRIP		= ws.WS2811_STRIP_RGB	#Strip type and colour ordering

# Size of matrix
MATRIX_WIDTH=15
MATRIX_HEIGHT=10

myMatrix=[	149,148,147,146,145,144,143,142,141,140,139,138,137,136,135,
			120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,
			119,118,117,116,115,114,113,112,111,110,109,108,107,106,105,
			 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,100,101,102,103,104,
			 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75,
			 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74,
			 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45,
			 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
			 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15,
			  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14]

# pin used for the mode button
BTNPIN = 8

# we want disco mode when the button is on, which means we return 1 
# when the pin is 0, as the pin goes low when the  switch
# connects to ground
def isDiscoMode():
	input_state = GPIO.input(BTNPIN) 
	if input_state == False: 
		return 1
	return 0
		
def allonecolour(strip,colour):
	# Paint the entire matrix one colour
	for i in range(strip.numPixels()):
		strip.setPixelColor(i,colour)
	strip.show()
	
def initLeds(strip):
	# Intialize the library (must be called once before other functions).
	strip.begin()
	# Wake up the LEDs by briefly setting them all to white
	allonecolour(strip,Color(255,255,255))
	time.sleep(0.01)
	
def rainbow(pos):
	"""Generate rainbow colors across 0-255 positions."""
	if pos < 85:
		return Color(pos * 3, 255 - pos * 3, 0)
	elif pos < 170:
		pos -= 85
		return Color(255 - pos * 3, 0, pos * 3)
	else:
		pos -= 170
		return Color(0, pos * 3, 255 - pos * 3)

def plot(strip, x, y, colour):
	if x < 0 or x >= MATRIX_WIDTH:
		return
	if y < 0 or y >= MATRIX_HEIGHT:
		return		
#	print("plot x %d y %d", x,y)
	strip.setPixelColor(myMatrix[int(y) * MATRIX_WIDTH + int(x)],colour)


def newLine(strip, x0,y0,x1,y1,colour):
	lx = x1 - x0
	ly = y1 - y0
	len = math.sqrt(lx*lx + ly*ly)
	if len == 0:
		return
	intLen = int(len)
	lx = lx / len
	ly = ly / len
	sx = x0 + 	lx * 0.5
	sy = y0 + ly * 0.5
	while intLen >= 0:
		plot(strip, sx,sy,colour)
		sx += lx
		sy += ly
		intLen = intLen - 1
		
def drawSquare(strip, x,y,rot,size,colour):
	angSin = math.sin(rot)*size
	angCos = math.cos(rot)*size
	x1 = angCos - angSin
	y1 = angSin + angCos
	
	x2 =  y1
	y2 =  -x1
	
	x3 = -x1
	y3 = -y1
	
	x4 = -y1
	y4 = x1
	
	newLine(strip, x+x1,y+y1,x+x2,y+y2, colour)
	newLine(strip, x+x2,y+y2,x+x3,y+y3, colour)
	newLine(strip, x+x3,y+y3,x+x4,y+y4, colour)
	newLine(strip, x+x4,y+y4,x+x1,y+y1, colour)
	
def wipe(strip, type, var, colour):
	if type == 0:
		newLine(strip, var,0,var,10,colour)
	if type == 1:
		newLine(strip, var,0,var,4,colour)
		newLine(strip, 15-var,5,15-var,9,colour)
	if type == 2:
		newLine(strip, var,0,var+15,10,colour)
		newLine(strip, -var,0,15-var,10,colour)
	if type == 3:
		newLine(strip, 0,var,2,var,colour)
		newLine(strip, 3,10-var,5,10-var,colour)
		newLine(strip, 6,var,8,var,colour)
		newLine(strip, 9,10-var,11,10-var,colour)
		newLine(strip, 12,var,14,var,colour)
		
def circle(strip, x0,y0, radius, colour):
	x = radius - 1
	y = 0
	dx = 1
	dy = 1
	err = dx - (radius << 1)
	
	while x >= y:
		plot(strip, x0+x, y0+y, colour)
		plot(strip, x0+y, y0+x, colour)
		plot(strip, x0-y, y0+x, colour)
		plot(strip, x0-x, y0+y, colour)
		plot(strip, x0-x, y0-y, colour)
		plot(strip, x0-y, y0-x, colour)
		plot(strip, x0+y, y0-x, colour)
		plot(strip, x0+x, y0-y, colour)
		if err <= 0:
			y += 1
			err += dy
			dy += 2
		else:
			x = x-1
			dx += 2
			err += dx - (radius << 1)

GPIO.setmode(GPIO.BCM) 
# set the pull up resistor
GPIO.setup(BTNPIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) 

# check if we're in disco mode
if not isDiscoMode():
	print("Disco mode not set, exiting Disco script.")
	quit()
			
strip = Adafruit_NeoPixel(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL, LED_STRIP)
initLeds(strip)

# a list of states
SQUARE = 0
WIPE1 = 1
WIPE2 = 2
WIPE3 = 3
WIPE4 = 4
CIRCLES = 5
SQUARES = 6
HYPNOTIZE=7
LASTSTATE=7
# set the first state
state = SQUARE
stateNext = SQUARE
# various timing and sizing things
frame = 0.00
stateTime = 0.0
radius = 4.0
hlfRadius = radius/1.2
fader = 0.5
fadeDir = 0
# list for circles, contains x,y and time
pointArray = [0,0,0 ,0,0,0 ,0,0,0 ,0,0,0]
while(True):
	# pick a random colour for this frame
	rainbowCol = rainbow(randint(0,255))
	# fade background colour, this doesn't want to be too bright so reduce
	rainbowBG = rainbow(int(frame*3)&255)
	bgR = (rainbowBG & 0x00ff0000) >> 21
	bgG = (rainbowBG & 0x0000ff00) >> 13
	bgB = (rainbowBG & 0x000000ff) >> 5
	# clear frame
	for i in range(strip.numPixels()):
		colour = strip.getPixelColor(i)
		red = bgR + ((colour & 0x00ff0000) >> 16)*fader
		grn = bgG + ((colour & 0x0000ff00) >> 8)*fader
		blu = bgB + ((colour & 0x000000ff))*fader
		if red > 255:
			red = 255
		if grn > 255:
			grn = 255
		if blu > 255:
			blu = 255
		strip.setPixelColor(i,Color(int(red),int(grn),int(blu)))
	
	# increase/decrease the fade rate of the background colour
	if fadeDir == 1:
		fader = fader*0.99
		if fader <= 0.5:
			fadeDir = 0
	else:
		fader = fader*1.01
		if fader >= 1.0:
			fader = 1.0
			fadeDir = 1
	
	if state == SQUARE:
		drawSquare(strip, 7, 5, frame,radius+math.sin(frame*1.4)*hlfRadius,rainbowCol)
		if stateTime > 5:
			stateNext = randint(0,LASTSTATE)
	elif state == WIPE1:
		wipe(strip,0,abs(math.sin(stateTime*4))*15, rainbowCol)
		if stateTime > 3.14159:
			stateNext = randint(0,LASTSTATE)
	elif state == WIPE2:
		wipe(strip,1,abs(math.sin(stateTime*4))*15, rainbowCol)
		if stateTime > 3.14159:
			stateNext = randint(0,LASTSTATE)
	elif state == WIPE3:
		wipe(strip,2,abs(math.sin(stateTime*4))*15, rainbowCol)
		if stateTime > 3.14159:
			stateNext = randint(0,LASTSTATE)
	elif state == WIPE4:
		wipe(strip,3,abs(math.sin(stateTime*4))*10, rainbowCol)
		if stateTime > 3.14159:
			stateNext = randint(0,LASTSTATE)
	elif state == CIRCLES:
		idx = 0
		for i in range(0,4):
			pointArray[idx+2] += 1
			if pointArray[idx+2] > 0:
				circle(strip,pointArray[idx+0], pointArray[idx+1],pointArray[idx+2]>>2, rainbowCol)
			if pointArray[idx+2] > 64:
				pointArray[idx+0] = randint(0,15)
				pointArray[idx+1] = randint(0,10)
				pointArray[idx+2] = randint(0,4)-4
			idx += 3
		if stateTime > 5:
			stateNext = randint(0,LASTSTATE)
	elif state == SQUARES:
		idx = 0
		for i in range(0,4):
			pointArray[idx+2] += 1
			if pointArray[idx+2] > 0:
				drawSquare(strip,pointArray[idx+0], pointArray[idx+1],frame*i,pointArray[idx+2]>>3, rainbowCol)
			if pointArray[idx+2] > 64:
				pointArray[idx+0] = randint(0,15)
				pointArray[idx+1] = randint(0,10)
				pointArray[idx+2] = randint(0,4) - 4
			idx += 3
		if stateTime > 5:
			stateNext = randint(0,LASTSTATE)
	elif state == HYPNOTIZE:
		idx=0
		for i in range(0,8):
			pointArray[i] += 1
			if pointArray [i] > 0:
				circle(strip,7,5, pointArray[i]>>1,rainbowCol)
			if pointArray[i]>64:
				pointArray[i]=0
		if stateTime > 5:
			stateNext = randint(0,LASTSTATE)
		
	strip.show()
	frame += 0.1
	stateTime += 0.02
	time.sleep(0.02)
	
	if state != stateNext:
		stateTime = 0.0
		state = stateNext
		if state == CIRCLES or state == SQUARES:
			for i in range(0,4):
				pointArray[i*3+0] = randint(0,15)
				pointArray[i*3+1] = randint(0,10)
				pointArray[i*3+2] = -i*20+randint(0,4) - 4
		if state == HYPNOTIZE:
			for i in range(0,8):
				pointArray[i]=-i*8
		
	
	

image1.txt

Plain text
0000 speed 0.150 0
0060-0090 flip 0.200 10
0105-0195 ping 0.200 6

imagelist.txt

Plain text
#image1

picanim.py

Python
# ledmatrix-scroll by Andrew Oakley www.aoakley.com Public 
# Domain 2015-10-18
#
# Takes an image file (e.g. PNG) as command line argument and scrolls it
# across a grid of WS2811 addressable LEDs, repeated in a loop until CTRL-C
#
# Use a very wide image for good scrolling effect.
#
# If you have a low resolution matrix (like mine, 12x8 LEDs) then you will
# probably need to create your image height equal to your matrix height
# and draw lettering pixel by pixel (e..g in GIMP or mtpaint) if you want
# words or detail to be legible.
# Modified for 150 LEDs by Craig Richardson 24/01/2018

import time, sys, os, re
import RPi.GPIO as GPIO
from neopixel import * # See https://learn.adafruit.com/neopixels-on-raspberry-pi/software
from PIL import Image  # Use apt-get install python-imaging to install this

# LED strip configuration:
LED_COUNT      = 150     # Number of LED pixels.
LED_PIN        = 18      # GPIO pin connected to the pixels (must support PWM!).
LED_FREQ_HZ    = 800000  # LED signal frequency in hertz (usually 800khz)
LED_DMA        = 5       # DMA channel to use for generating signal (try 5)
LED_BRIGHTNESS = 255     # Set to 0 for darkest and 255 for brightest
LED_INVERT     = False   # True to invert the signal (when using NPN transistor level shift)
LED_CHANNEL		= 0		 # Set to '1' for GPIOs 13, 19, 41,45 OR 53
LED_STRIP		= ws.WS2811_STRIP_RGB	#Strip type and colour ordering
# Speed of movement, in seconds (recommend 0.1-0.3)
SPEED=0.075

# Size of your matrix
MATRIX_WIDTH=15
MATRIX_HEIGHT=10

# LED matrix layout
# A list converting LED string number to physical grid layout
# Start with top right and continue right then down
# For example, my string starts bottom right and has horizontal batons
# which loop on alternate rows.
#
# Mine ends at the top right here:     -----------\
# My last LED is number 150                       |
#                                      /----------/
#                                      |
#                                      \----------\
# The first LED is number 0                       |
# Mine starts at the bottom left here: -----------/

myMatrix=[	149,148,147,146,145,144,143,142,141,140,139,138,137,136,135,
			120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,
			119,118,117,116,115,114,113,112,111,110,109,108,107,106,105,
			 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,100,101,102,103,104,
			 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75,
			 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74,
			 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45,
			 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
			 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15,
			  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14]


# pin used for the mode button
BTNPIN = 8

# we want picture mode when the button is off, which means we return 1 
# when the pin is 1, as the pin goes high when the  switch
# is disconnected from ground
def isPictureMode():
	input_state = GPIO.input(BTNPIN) 
	if input_state == True: 
		return 1
	return 0
	
# I got my LEDs from https://www.ebay.co.uk/p/50-X-12mm-LED-Module-RGB-Ws2811-Digital-Pixel-Addressable-LED-Strip-Waterproof-5/2074447882?_trksid=p2047675.l2644
# I also used an 74AHCT125 level shifter & 10 amp 5V PSU from https://www.ebay.co.uk/itm/AC100V-240V-to-DC5V-10A-50W-Power-Supply-Adapter-DC-Female-for-LED-Strip-Light-/182208116955?var=&hash=item2a6c7338db
# Good build tutorial here:
# https://learn.adafruit.com/neopixels-on-raspberry-pi?view=all

# Check that we have sensible width & height
if MATRIX_WIDTH * MATRIX_HEIGHT != len(myMatrix):
  raise Exception("Matrix width x height does not equal length of myMatrix")

def allonecolour(strip,colour):
	# Paint the entire matrix one colour
	for i in range(strip.numPixels()):
		strip.setPixelColor(i,colour)
	strip.show()

def colourTuple(rgbTuple):
  return Color(rgbTuple[0],rgbTuple[1],rgbTuple[2])

def initLeds(strip):
  # Intialize the library (must be called once before other functions).
  strip.begin()
  # Wake up the LEDs by briefly setting them all to white
  allonecolour(strip,Color(255,255,255))
  time.sleep(0.01)

GPIO.setmode(GPIO.BCM) 
# set the pull up resistor
GPIO.setup(BTNPIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) 

# check if we're in picture mode
if not isPictureMode():
	print("Picture mode not set, exiting picture script.")
	quit()

imageList=[]
imageFile = ""
if os.path.isfile("/home/pi/ledmatrix/imagelist.txt"):
	f=open("/home/pi/ledmatrix/imagelist.txt",'r')
	imageList=f.readlines()
	f.close()
	start=0
	while start<len(imageList):
		match=re.search( '(?<=#)\w+', imageList[start], re.M|re.I)
		if match:
			imageFile = match.group(0)
			print("Found image %s" % imageFile)
			start = -1
			break
		start += 1
	if start == 0:
		print("No image found!")
		
# now try to open the image
try:
	loadIm=Image.open('/home/pi/ledmatrix/'+imageFile+'.png')
except:
    raise Exception("Image file %s could not be loaded" % imageFile)

# If the image height doesn't match the matrix, resize it
if loadIm.size[1] != MATRIX_HEIGHT:
  origIm=loadIm.resize((loadIm.size[0]/(loadIm.size[1]//MATRIX_HEIGHT),MATRIX_HEIGHT),Image.BICUBIC)
else:
  origIm=loadIm.copy()
# If the input is a very small portrait image, then no amount of resizing will save us
if origIm.size[0] < MATRIX_WIDTH:
  raise Exception("Picture is too narrow. Must be at least %s pixels wide" % MATRIX_WIDTH)

# Check if there's an accompanying .txt file which tells us
# how the user wants the image animated
# Commands available are:
# NNNN speed S.SSS 
#   Set the scroll speed (in seconds)
#   Example: 0000 speed 0.150 
#   At position zero (first position), set the speed to 150ms
# NNNN hold S.SSS
#   Hold the frame still (in seconds)
#   Example: 0011 hold 2.300
#   At position 11, keep the image still for 2.3 seconds
# NNNN-PPPP flip S.SSS repeat
#   Animate MATRIX_WIDTH frames, like a flipbook
#   with a speed of S.SSS seconds between each frame
#   Example: 0014-0049 flip 0.100 2
#   From position 14, animate with 100ms between frames
#   until you reach or go past position 49
#   for as many times as repeat is set to
#   Note that this will jump positions MATRIX_WIDTH at a time
#   Takes a bit of getting used to - experiment
# NNNN-PPPP ping S.SSS repeat
#   Animate MATRIX_WIDTH frames, like a flipbook back and forth
#   with a speed of S.SSS seconds between each frame
#   Example: 0014-0049 flip 0.100 2
#   From position 14, animate with 100ms between frames
#   until you reach or go past position 49
#   for as many times as repeat is set to
#   Note that this will jump positions MATRIX_WIDTH at a time
# NNNN jump PPPP
#   Jump to position PPPP
#   Example: 0001 jump 0200
#   At position 1, jump to position 200
#   Useful for debugging only - the image will loop anyway
txtlines=[]
#match=re.search( r'^(?P<base>.*)\.[^\.]+$', sys.argv[1], re.M|re.I)
#if match:
#	txtfile=match.group('base')+'.txt'
txtfile='/home/pi/ledmatrix/'+imageFile+'.txt'
if os.path.isfile(txtfile):
	print "Found text file %s" % (txtfile)
	f=open(txtfile,'r')
	txtlines=f.readlines()
	f.close()


# Add a copy of the start of the image, to the end of the image,
# so that it loops smoothly at the end of the image
im=Image.new('RGB',(origIm.size[0]+MATRIX_WIDTH,MATRIX_HEIGHT))
im.paste(origIm,(0,0,origIm.size[0],MATRIX_HEIGHT))
im.paste(origIm.crop((0,0,MATRIX_WIDTH,MATRIX_HEIGHT)),(origIm.size[0],0,origIm.size[0]+MATRIX_WIDTH,MATRIX_HEIGHT))

# Create NeoPixel object with appropriate configuration.
strip = Adafruit_NeoPixel(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL, LED_STRIP)
initLeds(strip)

# And here we go.
try:
	while(True):
		time.sleep(2)
		# Loop through the image widthways
		# Can't use a for loop because Python is dumb
		# and won't jump values for FLIP command
		x=0
		# Initialise a pointer for the current line in the text file
		tx=0

		repeat = 0
		pinged = 0
		while x<im.size[0]-MATRIX_WIDTH:

			# Set the sleep period for this frame
			# This might get changed by a textfile command
			thissleep=SPEED

			# Set the increment for this frame
			# Typically advance 1 pixel at a time but
			# the FLIP command can change this
			thisincrement=1

			# Set for how many repeats a flip animation should do
			thisRepeat = 0
	  
			ping = 0
	  
			rg=im.crop((x,0,x+MATRIX_WIDTH,MATRIX_HEIGHT))
			dots=list(rg.getdata())

			for i in range(len(dots)):
				strip.setPixelColor(myMatrix[i],colourTuple(dots[i]))
			strip.show()

			# Check for instructions from the text file
			if tx<len(txtlines):
				match = re.search( r'^(?P<start>\s*\d+)(-(?P<finish>\d+))?\s+((?P<command>\S+)(\s+(?P<param>\d+(\.\d+)?))(\s+(?P<param2>\d+))?)$', txtlines[tx], re.M|re.I)
				if match:
					print "Found valid command line %d:\n%s" % (tx,txtlines[tx])
					st=int(match.group('start'))
					fi=st
					print "Current pixel %05d start %05d finish %05d" % (x,st,fi)
					if match.group('finish'):
						fi=int(match.group('finish'))
					if x>=st and tx<=fi:
						if match.group('command').lower()=='speed':
							SPEED=float(match.group('param'))
							thissleep=SPEED
							print "Position %d : Set speed to %.3f secs per frame" % (x,thissleep)
						elif match.group('command').lower()=='flip':
							thissleep=float(match.group('param'))
							thisincrement=MATRIX_WIDTH
							thisRepeat=float(match.group('param2'))
							print "Position %d: Flip for %.3f secs" % (x,thissleep)
						elif match.group('command').lower()=='hold':
							thissleep=float(match.group('param'))
							print "Position %d: Hold for %.3f secs" % (x,thissleep)
						elif match.group('command').lower()=='jump':
							print "Position %d: Jump to position %s" % (x,match.group('param'))
							x=int(match.group('param'))
							thisincrement=0
						elif match.group('command').lower()=='ping':
							thissleep=float(match.group('param'))
							thisincrement=MATRIX_WIDTH
							thisRepeat=float(match.group('param2'))
							ping = 1
							print "Position %d: Pinging for %.3f secs Repeat: %d" % (x,thissleep,thisRepeat)
					# Move to the next line of the text file
					# only if we have completed all pixels in range
					if x >= fi:
						print "End of line"
						if repeat >= thisRepeat and not ping:
							tx = tx + 1
							repeat = 0
						else:
							repeat = repeat + 1
							if ping:
								if repeat <= thisRepeat:
									pinged = 1
							else:
								x = st
				else:
					print "Found INVALID command line %d:\n%s" % (tx,txtlines[tx])
					tx=tx+1

			if pinged:
				x = x - thisincrement
			else:
				x = x + thisincrement
			if x <= st and ping == 1:
				x = st
				pinged = 0
			time.sleep(thissleep)

except (Keybo

Credits

craig richardson

craig richardson

3 projects • 2 followers

Comments