# 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.

import time, sys, os, re
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      = 96      # 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)

# Speed of movement, in seconds (recommend 0.1-0.3)
SPEED=0.075

# Size of your matrix
MATRIX_WIDTH=12
MATRIX_HEIGHT=8

# 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 95                        |
#                                      /----------/
#                                      |
#                                      \----------\
# The first LED is number 0                       |
# Mine starts at the bottom left here: -----------/

myMatrix=[95,94,93,92,91,90,89,88,87,86,85,84,
          72,73,74,75,76,77,78,79,80,81,82,83,
          71,70,69,68,67,66,65,64,63,62,61,60,
          48,49,50,51,52,53,54,55,56,57,58,59,
          47,46,45,44,43,42,41,40,39,38,37,36,
          24,25,26,27,28,29,30,31,32,33,34,35,
          23,22,21,20,19,18,17,16,15,14,13,12,
           0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11]

# Feel free to write a fancy set of loops to populate myMatrix
# if you have a really big display! I used two cheap strings of
# 50 LEDs, so I just have a 12x8 grid = 96 LEDs
# I got mine from: http://www.amazon.co.uk/gp/product/B00MXW054Y
# I also used an 74AHCT125 level shifter & 10 amp 5V PSU
# 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 colour(r,g,b):
  # Fix for Neopixel RGB->GRB, also British spelling
  return Color(g,r,b)

def colourTuple(rgbTuple):
  return Color(rgbTuple[1],rgbTuple[0],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,colour(255,255,255))
  time.sleep(0.01)

# Open the image file given as the command line parameter
try:
  loadIm=Image.open(sys.argv[1])
except:
  if len(sys.argv)==0:
    raise Exception("Please provide an image filename as a parameter.")
  else:
    raise Exception("Image file %s could not be loaded" % sys.argv[1])

# 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
#   Animate MATRIX_WIDTH frames, like a flipbook
#   with a speed of S.SSS seconds between each frame
#   Example: 0014-0049 flip 0.100
#   From position 14, animate with 100ms between frames
#   until you reach or go past position 49
#   Note that this will jump positions MATRIX_WIDTH at a time
#   Takes a bit of getting used to - experiment
# 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'
  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)
initLeds(strip)

# And here we go.
try:
  while(True):

    # 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

    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

      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+)?))?)$', 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
              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
          # Move to the next line of the text file
          # only if we have completed all pixels in range
          if x>=fi:
            tx=tx+1
        else:
          print "Found INVALID command line %d:\n%s" % (tx,txtlines[tx])
          tx=tx+1

      x=x+thisincrement
      time.sleep(thissleep)

except (KeyboardInterrupt, SystemExit):
  print "Stopped"
  allonecolour(strip,colour(0,0,0))