Friday, 5 June 2015

Dotstar LEDs with Raspberry Pi - the Python bit

In this post, I showed how I made my Raspberry Pi jukebox. Here's how it is controlled.

At it's core, it's just a python script which waits for interrupts from the Pi's GPIO pins. That's the easy bit (although it would be a lot easier if there wasn't two different ways to number GPIO pins - I mean really...)

I do know that just making a bunch of functions isn't particularly pythonic and I should probably do something with classes and objects and stuff, but I had a really immovable deadline on this project so I just stuck with what I knew (which mostly comes from php, years ago).

Adafruit provide a library to let you access the Dotstar leds, but it's pretty basic. That's cool, it was fun learning how to figure stuff out. I ended up using a couple of modules to generate colour gradients and to shift between various colourspaces.

A nifty thing happened when I wanted to flash random, but bright colours as part of rave mode. Taking random RGB values was getting very pastel colours most of the time, which wasn't quite what I was looking for. A friend on Facebook suggested using HSV, so I could set saturation/value to a high value (70-100%) and then take a random hue. Worked wonderfully, but I couldn't ever get a decent video of it in action to show the difference.

I used threading (for the first time in python, yay!) to run a background thread that polls Kodi to see if it's playing, because Kodi can be controlled by means other than the buttons, so we need to know if it's playing to decide whether to put the lights on or not.

Some leds are "fixed" - they're the ones over the switches - and will remain a single colour while the rest of the strip phases colours. That lets you know which switch is on.

Ravemode is still my favourite bit. That was good fun to come up with.

Code is long so it's after the jump. I know it could be tidier, and a bit smarter, but I was on a deadline. I shall return to fiddle with it later on. There's more I'd like this box to do, but for now it just needs to play music.




#!/usr/bin/env python

import requests
import json
import urllib
import os
from threading import Thread

import time
from random import randint,choice,uniform
import RPi.GPIO as GPIO
import time
from dotstar import Adafruit_DotStar
from colour import Color
from subprocess import call
from lxml import etree as ET


#############################################################
#### assorted global variables and general purpose shiz

# button states - pin-no:0/1 for up/down
bState = { 19:0,16:0,26:0,20:0,21:0 }

# this is  checked in main loop to determine wtf is going on
# this appears to be being deprecated in favour of kodistate which is updated by polling kodi
# only ravemode uses thingHappening any more
thingHappening = "nothing"

# let's get that light party started
numpixels = 30

# fixed leds set to their number if so. set to > numpixels if not
switchstates = [99,99,99,99]
switchcolours = {0:0xFFFF00,1:0xFFFF00,2:0xFFFF00,3:0xFFFF00}

# fade to black
def turnOff(intv=0.04):
 c1 = "#" + hex(strip.getPixelColor(0)).ljust(8,"0").replace("0x","")
 phase(c1,"#000000",intv)
 

#############################################################
#### RAVEMODE ####

# length of lit group in colourWave
grouplen = 10

def hsv_to_rgb(h, s, v):
        if s == 0.0: v*=255; return [v, v, v]
        i = int(h*6.) # XXX assume int() truncates!
        f = (h*6.)-i; p,q,t = int(255*(v*(1.-s))), int(255*(v*(1.-s*f))), int(255*(v*(1.-s*(1.-f)))); v*=255; i%=6
        if i == 0: return [v, t, p]
        if i == 1: return [q, v, p]
        if i == 2: return [p, v, t]
        if i == 3: return [p, q, v]
        if i == 4: return [t, p, v]
        if i == 5: return [v, p, q]

def randColour():
 # weighted HSV
 h = uniform(0,100)/100
 s = uniform(70,100)/100
 v = uniform(70,100)/100
 rb = hsv_to_rgb(h,s,v)
 r = int(round(rb[0]))
 g = int(round(rb[1]))
 b = int(round(rb[2]))
 col = (r,g,b)

 return col

# control fuctions for sideFlash() feature
def setLeft(c):
 for n in range(22,30):
  strip.setPixelColor(n,c[0],c[1],c[2])
 strip.show()

def setRight(c):
 for n in range(0,7):
  strip.setPixelColor(n,c[0],c[1],c[2])
 strip.show()

def setFront(c):
 for n in range(8,21):
  strip.setPixelColor(n,c[0],c[1],c[2])
 strip.show()

def sideFlash():
 cl = randColour()
 cr = randColour()
 cf = randColour()
 off = (0,0,0)

 for a in range(0,80):
  r = randint(0,6)
  if r == 0:
   setLeft(cl)
   time.sleep(round(uniform(0,10)/100,2))
  if r == 1:
   setRight(cr)
   time.sleep(round(uniform(0,10)/100,2))
  if r == 2:
   setFront(cf)
   time.sleep(round(uniform(0,10)/100,2))
  if r == 3:
   setLeft(off)
   time.sleep(round(uniform(0,10)/100,2))
  if r == 4:
   setRight(off)
   time.sleep(round(uniform(0,10)/100,2))
  if r == 5:
   setFront(off)
   time.sleep(round(uniform(0,10)/100,2))

 setLeft(off)
 setRight(off)
 setFront(off) 
 
# it's like a fabulous Knight Rider
def colourWave():
 for r in range(randint(1,6)):
  head = 0
  tail = 0 - grouplen
  for n in range(numpixels + grouplen):
   c = randColour()
   strip.setPixelColor(n,c[0],c[1],c[2])
   strip.show()
   time.sleep(0.005)
   strip.setPixelColor(tail,0)
   time.sleep(0.005)
   head += 1
   tail += 1

  head = numpixels
  tail = numpixels + grouplen 

  for n in range(numpixels + grouplen + 2):
   c = randColour()
   strip.setPixelColor(head,c[0],c[1],c[2])
   strip.show()
   time.sleep(0.005)
   strip.setPixelColor(tail,0)
   time.sleep(0.005)
   head -= 1
   tail -= 1


def flashStrip():
 global bigrainbow
 for r in range(randint(5,16)):
  rcol = randColour()
  for n in range(numpixels):
   strip.setPixelColor(n,rcol[0],rcol[1],rcol[2])
  strip.show()
  time.sleep(0.09)
  for n in range(numpixels):
   strip.setPixelColor(n,0)
  strip.show()
  time.sleep(0.03)
 time.sleep(0.05)

def raveMode(duration):
 global thingHappening
 
 # max out teh bright
 strip.setBrightness(255)

 ts = time.time() + duration
 while time.time() < ts:
  r = randint(0,2)
  if r == 0:
   flashStrip()
  if r == 1:
   colourWave()
  if r == 2:
   sideFlash()
 turnOff()
 thingHappening = "nothing"


#############################################################
#### phase strip ####

def phase(c1,c2,interval,px=None):

 c1 = Color(c1)
 c2 = Color(c2)
 grad = list(c1.range_to(c2,128))
 
 for c in grad:
  rbg = c.rgb
  red = int(round(255 * rbg[0]))
  blue = int(round(255 * rbg[1]))
  green = int(round(255 * rbg[2]))
  if px != None:
   strip.setPixelColor(px,red,blue,green)
  else:
   for n in range(numpixels):
    if n in switchstates:
     for i,v in enumerate(switchstates):
      if v == n:
       # TODO fix phase-in for fixed pixels
       #phasePixel(n,switchcolours[i],0.2)
       strip.setPixelColor(n,switchcolours[i])
    else:
     strip.setPixelColor(n,red,blue,green)
  strip.show()
  time.sleep(interval)

def phaseStrip():

 strip.setBrightness(40) 

 # TODO check if any px are lit, if so start from their colour, fade to red
 # otherwise start from black
 # phaseStrip("#000000","#FF0000",0.002)

 # R to the G to the B fade on.
 phase("#FF0000","#00FF00",0.02)
 phase("#00FF00","#0000FF",0.02)
 phase("#0000FF","#FF0000",0.02)
 time.sleep(0.0001)

def phasePixel(px,c2,intv,c1=None):
 if c1 == None:
  c1 = Color("#" + hex(strip.getPixelColor(px)).ljust(8,"0").replace("0x",""))
 #else:  
 phase(c1,c2,intv,px)


#############################################################
#### holding pattern ####

def holdingPattern():
 rainbow = [0x0079E5,0x0050E5,0x0027E5,0x0200E5,0x2B00E5,0x5500E6,0x7E00E6,0xA800E6,0xD200E6,0xE600D1,0xE700A8,0xE7007E,0xE70054,0xE7002B,0xE70001,0xE82800,0xE85200,0xE87C00,0xE8A600,0xE8D000,0xD7E900,0xADE900,0x83E900,0x59E900,0x2EE900,0x04EA00,0x00EA25,0x00EA50,0x00EA7A,0x00EBA5]

 brightness = 1

 for n in range(numpixels):
  strip.setPixelColor(n,rainbow[n])
  strip.setBrightness(int(round(brightness)))
  brightness += 1
  strip.show()
  time.sleep(0.1)

 for n in range(numpixels):
  strip.setPixelColor(n,0)
  strip.setBrightness(int(round(brightness)))
  brightness -= 1
  strip.show()
  time.sleep(0.1)


#############################################################
#### Kodi/playlist bizniss ####

# playing list, tracks which switches are on
playing = []

# special case tracking for hyperplaylist
hyperplay = False

def doKodiThings(button):
 # add to playing list
 playing.append(button)
 # stop anything currently playing
 if kodistate == "playing":
  kodiAllStop()
  # do it do it do it
 kodiPlay()

def doResetThings(button):

 global hyperplaying

 # what is playing now?
 nowplaying = playing[-1]

 # remove turned off switch from playing list
 playing.remove(button)

 # stahp, if the button turned off is the one playing
 if button == nowplaying or hyperplaying == True:
  kodiAllStop()

 # if some switches are still on, and we haven't turned off a non-playing switch,
 # and hyperplay is active, play the most recently turned on switch/hyperplaylist
 if len(playing) > 0:
  if nowplaying != playing[-1] or hyperplaying == True:
   kodiPlay()
 else:
  hyperplaying = False


def kodiAllStop():
 # this stop command works, it shouldn't
 stopcmd = "/usr/bin/kodi-send --host=localhost --port=9777 --action=\"Playmedia(Stop)\""
 call(stopcmd, shell=True)
 clrcmd = "/usr/bin/kodi-send --host=localhost --port=9777 --action=\"Playlist.Clear\""
 call(clrcmd, shell=True)

def kodiPlay():
 # plays the last (most recent) item on the playing list
 # name playlists Magicplay_$name
 # if more than one switch on, make unified playlist
 if len(playing) > 1:
  hyperPlaylist(playing)
  plc = "magic"
 else:
 # just one switch on, so play that
  plc = str(playing[-1])

 playcmd = "/usr/bin/kodi-send --host=localhost --port=9777 --action=\"Playmedia(special://profile/playlists/music/Magicplay_" + plc + ".xsp)\""
 call(playcmd, shell=True)

def pinToName(pin):
 if pin == 19:
  return 'farleft'
 if pin == 26:
  return 'left'
 if pin == 16:
  return 'right'
 if pin == 20:
  return 'farright'

def hyperPlaylist(playlists):

 global hyperplaying

 rootpath = "/home/kodi/.kodi/userdata/playlists/music/Magicplay_"
 savepath = rootpath + "magic.xsp"

 smp = ET.Element('smartplaylist')
 smp.set('type','songs')
 nm = ET.SubElement(smp, 'name')
 nm.text = "Magicplay_Magic"
 mt = ET.SubElement(smp, 'match')
 mt.text = "any"

 for f in playlists:
  rl = ET.SubElement(smp, 'rule')
  rl.set('field', 'playlist')
  rl.set('operator', 'is')
  rl.text = "Magicplay_" + f

 oot = ET.tostring(smp, pretty_print=True, standalone=True, xml_declaration=True, encoding='UTF-8')
 with open(savepath, "w") as of:
  of.write(oot)
 hyperplaying = True


#############################################################
#### KODI MONITOR ####

kodistate = "not"

def kodiMonitor():
 global kodistate
 headers = {'content-type': 'application/json'}
 xbmc_host = 'localhost'
 xbmc_port = 8080
 xbmc_json_rpc_url = "http://" + xbmc_host + ":" + str(xbmc_port) + "/jsonrpc"
 payload = {"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 1}
 url_param = urllib.urlencode({'request': json.dumps(payload)})

 while True:
  response = requests.get(xbmc_json_rpc_url + '?' + url_param, headers=headers)
  data = json.loads(response.text)
  if data['result']:
   # kodi is playing
   kodistate = "playing"
  else:
   kodistate = "not"

  time.sleep(1)


#############################################################
#### GPIO SHIT ####

def buttonHandler(pin):
 # rising/falling state seems to have some lag, add a delay
 time.sleep(0.5)
 # if pin falling, switch is off
 if GPIO.input(pin) == 1:
  if bState[pin] == 1:
   bState[pin] = 0
   switchOff(pin)

 # pin rising, switch is on
 else:
  if bState[pin] == 0:
   bState[pin] = 1
   switchOn(pin)

def switchOn(pin):
 global thingHappening
 global switchstates
 global prevThing

 # update switch status
 # this allows an led to stay lit during phasing to indicate a switch is on.
 if pin == 19: # farleft
  switchstates[0] = 10
 if pin == 26: # left
  switchstates[1] = 13
 if pin == 16: # right
  switchstates[2] = 17
 if pin == 20: # farright
  switchstates[3] = 20

 # update thingHappening, only applies to ravemode, other updates come via kodiMonitor()
 if pin == 21:
  thingHappening = "ravemode"
 else:
  # playlist shit
  doKodiThings(pinToName(pin))


def switchOff(pin):
 global switchstates

 # update switch status
 if pin == 19:  # farleft
  switchstates[0] = 99
 if pin == 26:  # left
  switchstates[1] = 99
 if pin == 16: # right
  switchstates[2] = 99
 if pin == 20: # farright
  switchstates[3] = 99

 # now, is audio playing? If so thingHappening should be 'playing', otherwise:
 if pin != 21:
  doResetThings(pinToName(pin))


def main():
 # consider working in GPIO.BOARD because BCM pinouts don't make any fucking sense
 GPIO.setmode(GPIO.BCM)

 # bridge to pin 6 (or gnd) to drop voltage
 GPIO.setup(19, GPIO.IN, pull_up_down=GPIO.PUD_UP) # pin 35
 GPIO.setup(16, GPIO.IN, pull_up_down=GPIO.PUD_UP) # pin 36
 GPIO.setup(26, GPIO.IN, pull_up_down=GPIO.PUD_UP) # pin 37
 GPIO.setup(20, GPIO.IN, pull_up_down=GPIO.PUD_UP) # pin 38
 GPIO.setup(21, GPIO.IN, pull_up_down=GPIO.PUD_UP) # pin 40

 # pinouts here, we're in BCM mode
 # http://www.raspberrypi-spy.co.uk/wp-content/uploads/2012/09/Raspberry-Pi-GPIO-Layout-Revision-1.png

 # Button callbacks
 GPIO.add_event_detect(19, GPIO.BOTH, callback=buttonHandler, bouncetime=500) # button 0
 GPIO.add_event_detect(16, GPIO.BOTH, callback=buttonHandler, bouncetime=500) # button 1
 GPIO.add_event_detect(26, GPIO.BOTH, callback=buttonHandler, bouncetime=500) # button 2
 GPIO.add_event_detect(20, GPIO.BOTH, callback=buttonHandler, bouncetime=500) # button 3
 GPIO.add_event_detect(21, GPIO.BOTH, callback=buttonHandler, bouncetime=500) # button 4

 # main loop
 while True:
  try:
   time.sleep(0.2)

   if kodistate == "not":

    # check if strip is on. If so, phase off.
    c1 = "#" + hex(strip.getPixelColor(1)).ljust(8,"0").replace("0x","")
    if c1 != "#000000":
     turnOff()
     time.sleep(5)

    # occasional knight ride colourwave rather than constant phasing
    if randint(1,10000) > 9998:
     holdingPattern()

   if kodistate == "playing":
    phaseStrip()

   if thingHappening == "ravemode":
    raveMode(15)

  except:
   turnOff()
   break

 GPIO.cleanup()


#############################################################
#### Ready set go go go ####
if __name__=="__main__":
 strip = Adafruit_DotStar(numpixels)
 strip.begin()
 kmon = Thread(target=kodiMonitor)
 kmon.daemon = True
 kmon.start()
 main()

5 comments:

  1. Hi, I saw your post on reddit and it inspired me to try to do something similar to your project. I think it would be cool to try to sync up the lights in an LED strip with the music that is being played. But I'm looking through the Dotsar LED docs and examples, and I don't see anything that says I can set the brightness of individual pixels. Did you come across anything like this? I'm essentially trying to assign a pixel to a frequency and increase/decrease the brightness of that pixel in sync with the magnitude of the frequency.

    Thanks, and again, love your project!
    Matt M.

    ReplyDelete
    Replies
    1. Hey, so how'd it go? I'm interested in making one too. Perhaps with a clock, and alarm functionality. I've never done anything with Raspberry Pi or Python so far though...

      Delete
    2. It went pretty well! I ended up rewriting this to use MPD instead of Kodi, but apart from that, yeah. Good.

      There are some pictures here: http://imgur.com/a/BUCaF

      Delete
  2. Ah! Nevermind--just hit me that I can just use RGB values then multiply them by the proportion that I want the brightness to reflect-- (255,0,0 is the same as 100,0,0 just different brightness).
    Anyway, still love your idea, thanks for helping me jog my memory! Ha.

    ReplyDelete
    Replies
    1. Nice solution! As far as I know you can only set brightness on the strip (via strip.setbrightness()), not per pixel, but your way works. I might update my fadeToBlack() function to work that way..

      btw, reading the source for Adafruit_Dotstar module is a good idea, it's well commented and there's a few bits in there which aren't mentioned anywhere else.

      Hope your project works out!

      Delete