├── pyLiter ├── __init__.py ├── TermLiter.py ├── PiLiter.py ├── LightHardware.py ├── MagickLiter.py ├── LiterThread.py ├── LightState.py └── testLightState.py ├── .gitignore ├── demo.py └── README.md /pyLiter/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["LightHardware", "LightState", "PiLiter", "TermLiter"] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # python files 2 | *.pyc 3 | */__pycache__/* 4 | 5 | # gedit files 6 | *~ 7 | 8 | -------------------------------------------------------------------------------- /pyLiter/TermLiter.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from pyLiter.LightHardware import LightHardware 4 | 5 | class TermLiter(LightHardware): 6 | """ Dummy version of the hardware for a terminal. This can be used to test/develop on computers without the piLiter """ 7 | 8 | def __init__(self): 9 | """ Set up hardware """ 10 | super(TermLiter, self).__init__() 11 | 12 | def updateValues(self, values): 13 | print(self._values._values) 14 | -------------------------------------------------------------------------------- /pyLiter/PiLiter.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import wiringpi2 as wiringpi 4 | 5 | from pyLiter import LightHardware 6 | 7 | class PiLiter(LightHardware): 8 | """ Communicate with the actual hardware """ 9 | lightPinMap = [7, 0, 2, 1, 3, 4, 5, 6] 10 | 11 | def __init__(self): 12 | """ Set up hardware """ 13 | super().__init__() 14 | wiringpi.wiringPiSetup() 15 | # Set up each pin for digital output 16 | for i in self.lightPinMap: 17 | wiringpi.pinMode(i, 1) 18 | 19 | def updateValue(self, lightId, value): 20 | pinId = self.lightPinMap[lightId] 21 | wiringpi.digitalWrite(pinId, value) 22 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import math 3 | from pyLiter.TermLiter import TermLiter 4 | from pyLiter.LiterThread import LiterThread 5 | from pyLiter.LightState import LightState 6 | 7 | class SineWaves: 8 | def __init__(self): 9 | self.a = 100 10 | 11 | def frames(self): 12 | for i in range(0, self.a): 13 | yield LightState.bar(math.sin(i / 5.0) / 2.0 + 0.5) 14 | 15 | if __name__ == '__main__': 16 | liter = TermLiter() 17 | a = LiterThread(liter) 18 | a._shutdownWhenComplete = True 19 | a.start() 20 | try: 21 | a.put(SineWaves().frames) 22 | while a.isAlive(): 23 | a.join(1) 24 | except KeyboardInterrupt: 25 | a.stop() 26 | 27 | print("Exiting main thread") 28 | -------------------------------------------------------------------------------- /pyLiter/LightHardware.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from pyLiter.LightState import LightState 4 | import abc 5 | 6 | class LightHardware(object): 7 | def __init__(self): 8 | self._values = LightState() 9 | 10 | @property 11 | def values(self): 12 | return self._values 13 | 14 | @values.setter 15 | def values(self, values): 16 | """ Set values """ 17 | changes = self._values.diff(values) 18 | self._values = values 19 | self.updateValues(self._values) 20 | for lightId in changes: 21 | self.updateValue(lightId, changes[lightId]) 22 | 23 | @abc.abstractmethod 24 | def updateValue(self, lightId, value): 25 | """ Send updated light value to the hardware """ 26 | pass 27 | 28 | @abc.abstractmethod 29 | def updateValues(self, values): 30 | """ Display updated values to the user (for testing) """ 31 | pass 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyLiter 2 | 3 | A simply python module to to interact with the [Pi-LITEr 8-LED Raspberry Pi shield](http://ciseco.co.uk/docs/Pi-LITEr-v-0-1.pdf) from Ciseko. 4 | 5 | This is a a work in progress. 6 | 7 | ## Installation 8 | 9 | This code currently runs on either python2 or python3, and depends on [wiringpi2](https://github.com/Gadgetoid/WiringPi2-Python). It is intended for use on the Raspberry Pi. 10 | 11 | ### Python 3 12 | 13 | On the Raspberry Pi, run- 14 | 15 | sudo apt-get install python3 python3-pip 16 | pip3 install wiringpi2 17 | 18 | Execute the test script with: 19 | 20 | python3 test.py 21 | 22 | ### Python 2 23 | 24 | sudo apt-get install python python-pip python-future 25 | pip install wiringpi2 26 | 27 | Execute the test script with: 28 | 29 | python test.py 30 | 31 | ## Development 32 | 33 | Execute unit tests with: 34 | 35 | python -m unittest discover 36 | 37 | Install coverage plugin: 38 | 39 | apt-get install python-coverage 40 | yum install python-coverage 41 | 42 | Run with coverage: 43 | 44 | coverage run -m unittest discover 45 | 46 | -------------------------------------------------------------------------------- /pyLiter/MagickLiter.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from pyLiter.LightHardware import LightHardware 4 | from subprocess import call 5 | 6 | class MagickLiter(LightHardware): 7 | """ Write to an image file rather than the real hardware. Used for documentation""" 8 | 9 | def __init__(self): 10 | """ Set up hardware """ 11 | super(MagickLiter, self).__init__() 12 | self._seq = 0 13 | self.size = 18 14 | self.name = 'outp-' 15 | 16 | def updateValues(self, values): 17 | self._seq = self._seq + 1 18 | cmd = [ 19 | "convert", 20 | "-size", 21 | "{:d}x{:d}".format(self.size, self.size), 22 | "canvas:black", 23 | "-write", 24 | self.img(0), 25 | "+delete", 26 | "-size", 27 | "{:d}x{:d}".format(self.size, self.size), 28 | "radial-gradient:yellow", 29 | "-write", 30 | self.img(1), 31 | "+delete", 32 | self.img(values._values[0]), 33 | self.img(values._values[1]), 34 | self.img(values._values[2]), 35 | self.img(values._values[3]), 36 | self.img(values._values[4]), 37 | self.img(values._values[5]), 38 | self.img(values._values[6]), 39 | self.img(values._values[7]), 40 | "+append", 41 | "{:s}{:03d}.png".format(self.name, self._seq)] 42 | call(cmd) 43 | 44 | def img(self, val): 45 | return 'mpr:on' if val == 1 else 'mpr:off' 46 | 47 | -------------------------------------------------------------------------------- /pyLiter/LiterThread.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from pyLiter.LightState import LightState 4 | import threading 5 | import time 6 | import queue 7 | 8 | class LiterThread (threading.Thread): 9 | def __init__(self, liter): 10 | threading.Thread.__init__(self) 11 | self.liter = liter 12 | self.frameRate = 10 13 | self._shutdown = False 14 | self._shutdownWhenComplete = False 15 | self._skip = False 16 | self.animQueue = queue.Queue() 17 | 18 | def put(self, animation): 19 | self.animQueue.put(animation) 20 | 21 | def putFrame(self, item): 22 | self.enque(LiterThread.wrapFrame(item)) 23 | 24 | def run(self): 25 | # Set up liter and delay 26 | self.liter.values = LightState.clear() 27 | delay = 1.0 / self.frameRate; 28 | while not self._shutdown: 29 | try: 30 | a = self.animQueue.get(True, delay) 31 | if self._skip and not self.animQueue.empty(): 32 | continue 33 | self._skip = False 34 | for i in a(): 35 | self.liter.values = i 36 | if self._shutdown or self._skip: 37 | break; 38 | time.sleep(delay) 39 | except queue.Empty: 40 | if self._shutdownWhenComplete: 41 | self._shutdown = True 42 | continue 43 | self._shutdown = True 44 | 45 | def stop(self): 46 | self._shutdown = True 47 | self.join() 48 | 49 | def wait(self): 50 | self._shutdownWhenComplete = True 51 | self.join() 52 | 53 | def cutTo(self, animation): 54 | self.put(animation) 55 | self._skip = True 56 | 57 | @staticmethod 58 | def wrapState(state): 59 | yield state 60 | -------------------------------------------------------------------------------- /pyLiter/LightState.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | class LightState(object): 4 | """ Maintain a list of light states, provide methods for running calculations over them""" 5 | size = 8 6 | _values = [-1, -1, -1, -1, -1, -1, -1, -1] 7 | 8 | def __init__(self, vals=[-1, -1, -1, -1, -1, -1, -1, -1]): 9 | self._values = vals 10 | 11 | def __eq__ (self, other): 12 | return self._values == other._values 13 | 14 | def __lshift__(self, other): 15 | curVals = self._values[:] 16 | for _ in range(other): 17 | nextVals = curVals[1:] 18 | nextVals.append(curVals[0]) 19 | curVals = nextVals 20 | return LightState(curVals) 21 | 22 | def __rshift__(self, other): 23 | curVals = self._values[:] 24 | for _ in range(other): 25 | curVals = [curVals[7]] + curVals[:7] 26 | return LightState(curVals) 27 | 28 | def __and__(self, other): 29 | curVals = self._values[:] 30 | for i in range(8): 31 | curVals[i] = 1 if (curVals[i] == 1 and other._values[i] == 1) else 0 32 | return LightState(curVals) 33 | 34 | def __or__(self, other): 35 | curVals = self._values[:] 36 | for i in range(8): 37 | curVals[i] = 1 if (curVals[i] == 1 or other._values[i] == 1) else 0 38 | return LightState(curVals) 39 | 40 | def __xor__(self, other): 41 | curVals = self._values[:] 42 | for i in range(8): 43 | a = curVals[i] == 1 44 | b = other._values[i] == 1 45 | curVals[i] = 1 if ((a or b) and not (a and b)) else 0 46 | return LightState(curVals) 47 | 48 | def __invert__(self): 49 | curVals = self._values[:] 50 | for i in range(8): 51 | curVals[i] = 0 if curVals[i] == 1 else 1 52 | return LightState(curVals) 53 | 54 | def diff(self, other): 55 | changes = {} 56 | for lightId in range(self.size): 57 | updatedVal = other._values[lightId] 58 | if updatedVal != -1 and self._values[lightId] != updatedVal: 59 | changes[lightId] = updatedVal 60 | return changes 61 | 62 | @staticmethod 63 | def clear(): 64 | return LightState([0, 0, 0, 0, 0, 0, 0, 0]) 65 | 66 | @staticmethod 67 | def bar(value, invert = False): 68 | """ Draw a bar chart with the lights. Optionally 69 | use 'invert' to change direction """ 70 | size = 8 71 | count = int(round(size * value)) 72 | newVal = [0]*size 73 | for i in range(count): 74 | idx = size - i - 1 if invert else i 75 | newVal[idx] = 1 76 | return LightState(newVal) 77 | -------------------------------------------------------------------------------- /pyLiter/testLightState.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import unittest 4 | from pyLiter.LightState import LightState 5 | 6 | class TestLightState(unittest.TestCase): 7 | def testShiftLeft(self): 8 | # Move one place 9 | a = LightState([0, 0, 0, 1, 0, 0, 0, 0]) 10 | b = a << 1 11 | self.assertEqual(b._values, [0, 0, 1, 0, 0, 0, 0, 0]) 12 | # Two places 13 | b = a << 2 14 | self.assertEqual(b._values, [0, 1, 0, 0, 0, 0, 0, 0]) 15 | # Wrapping 16 | b = a << 4 17 | self.assertEqual(b._values, [0, 0, 0, 0, 0, 0, 0, 1]) 18 | # Handling numbers > 8 19 | b = a << 12 20 | self.assertEqual(b._values, [0, 0, 0, 0, 0, 0, 0, 1]) 21 | 22 | def testShiftRight(self): 23 | # Move one place 24 | a = LightState([0, 0, 0, 1, 0, 0, 0, 0]) 25 | b = a >> 1 26 | self.assertEqual(b._values, [0, 0, 0, 0, 1, 0, 0, 0]) 27 | # Two places 28 | b = a >> 2 29 | self.assertEqual(b._values, [0, 0, 0, 0, 0, 1, 0, 0]) 30 | # Wrapping 31 | b = a >> 4 32 | self.assertEqual(b._values, [0, 0, 0, 0, 0, 0, 0, 1]) 33 | # Handling numbers > 8 34 | b = a >> 12 35 | self.assertEqual(b._values, [0, 0, 0, 0, 0, 0, 0, 1]) 36 | 37 | def testEq(self): 38 | a = LightState([0, 1, 0, 1, 0, 1, 0, 1]) 39 | b = LightState([0, 1, 0, 1, 0, 1, 0, 1]) 40 | c = LightState([0, 1, 0, 1, 0, 1, 0, 0]) 41 | self.assertEqual(a, b) 42 | self.assertNotEqual(a, c) 43 | self.assertNotEqual(b, c) 44 | 45 | def testAnd(self): 46 | a = LightState([1, 1, 1, 1, 1, 1, 1, 1]) 47 | b = LightState([0, 0, 0, 0, 0, 0, 0, 0]) 48 | c = LightState([1, 0, 1, 0, 0, 1, 0, 0]) 49 | d = a & b 50 | self.assertEqual(d._values, [0, 0, 0, 0, 0, 0, 0, 0]) 51 | d = b & c 52 | self.assertEqual(d._values, [0, 0, 0, 0, 0, 0, 0, 0]) 53 | d = c & a 54 | self.assertEqual(d._values, [1, 0, 1, 0, 0, 1, 0, 0]) 55 | 56 | def testOr(self): 57 | a = LightState([1, 1, 1, 1, 1, 1, 1, 1]) 58 | b = LightState([0, 0, 0, 0, 0, 0, 0, 0]) 59 | c = LightState([1, 0, 1, 0, 0, 1, 0, 0]) 60 | d = a | b 61 | self.assertEqual(d._values, [1, 1, 1, 1, 1, 1, 1, 1]) 62 | d = b | c 63 | self.assertEqual(d._values, [1, 0, 1, 0, 0, 1, 0, 0]) 64 | d = c | a 65 | self.assertEqual(d._values, [1, 1, 1, 1, 1, 1, 1, 1]) 66 | 67 | def testXor(self): 68 | a = LightState([1, 1, 1, 1, 1, 1, 1, 1]) 69 | b = LightState([0, 0, 0, 0, 0, 0, 0, 0]) 70 | c = LightState([1, 0, 1, 0, 0, 1, 0, 0]) 71 | d = a ^ b 72 | self.assertEqual(d._values, [1, 1, 1, 1, 1, 1, 1, 1]) 73 | d = b ^ c 74 | self.assertEqual(d._values, [1, 0, 1, 0, 0, 1, 0, 0]) 75 | d = c ^ a 76 | self.assertEqual(d._values, [0, 1, 0, 1, 1, 0, 1, 1]) 77 | 78 | def testInvert(self): 79 | a = LightState([1, 1, 1, 1, 1, 1, 1, 1]) 80 | b = LightState([0, 0, 0, 0, 0, 0, 0, 0]) 81 | c = LightState([1, 0, 1, 0, 0, 1, 0, 0]) 82 | d = ~a 83 | self.assertEqual(d._values, [0, 0, 0, 0, 0, 0, 0, 0]) 84 | d = ~b 85 | self.assertEqual(d._values, [1, 1, 1, 1, 1, 1, 1, 1]) 86 | d = ~c 87 | self.assertEqual(d._values, [0, 1, 0, 1, 1, 0, 1, 1]) 88 | 89 | 90 | if __name__ == '__main__': 91 | unittest.main() 92 | --------------------------------------------------------------------------------