├── blink1 ├── __init__.py ├── shine.py ├── flash.py ├── kelvin.py └── blink1.py ├── blink1_demo ├── __init__.py ├── demo_pattern0.py ├── demo_serial_version.py ├── demo_context.py ├── demo_pattern1.py ├── demo1.py ├── demo_pattern2.py ├── demo_servertickle.py ├── demo_leds.py ├── demo_logging.py ├── demo_pattern3.py └── demo_multi_blink1.py ├── blink1_tests ├── __init__.py ├── hidapitest0.py ├── test_kelvin.py ├── test_gamma.py ├── test_b1_context_manager.py └── test_simple_light_control.py ├── publishing.md ├── LICENSE.txt ├── pyproject.toml ├── .gitignore └── README.md /blink1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blink1_demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blink1_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blink1_demo/demo_pattern0.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo_pattern0 -- demo of blink1 library color pattern reading 5 | """ 6 | import sys 7 | from blink1.blink1 import Blink1 8 | 9 | try: 10 | blink1 = Blink1( gamma=(1,1,1) ) # disable gamma 11 | except: 12 | print("no blink1 found") 13 | sys.exit() 14 | 15 | print("Reading full color pattern:") 16 | pattern = blink1.read_pattern() 17 | print(pattern) 18 | -------------------------------------------------------------------------------- /blink1/shine.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import click 3 | from blink1.blink1 import blink1 4 | 5 | 6 | @click.command() 7 | @click.option('--color', default='white', help='What colour to set the Blink(1)') 8 | @click.option('--fade', default=0.2, help='Fade time in seconds') 9 | def shine(color, fade): 10 | with blink1(switch_off=False) as b1: 11 | b1.fade_to_color(fade * 1000.0, color) 12 | 13 | 14 | if __name__ == '__main__': 15 | shine() 16 | -------------------------------------------------------------------------------- /blink1_demo/demo_serial_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo_serial -- demo of blink1 library showing serial number and versions 5 | 6 | """ 7 | from blink1.blink1 import Blink1 8 | 9 | try: 10 | blink1 = Blink1() 11 | except: 12 | print("no blink1 found") 13 | sys.exit() 14 | print("blink(1) found") 15 | 16 | print(" serial number: " + blink1.get_serial_number()) 17 | print(" firmware version: " + blink1.get_version()) 18 | 19 | print("closing connection to blink(1)") 20 | blink1.close() 21 | 22 | print("done") 23 | -------------------------------------------------------------------------------- /blink1_demo/demo_context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | demo_context -- simple context manager demo of blink1 library 4 | 5 | """ 6 | 7 | import time 8 | from blink1.blink1 import blink1 9 | 10 | with blink1() as b1: 11 | b1.fade_to_color(300, 'goldenrod') 12 | time.sleep(2) 13 | 14 | time.sleep(1) 15 | 16 | # notice how blink(1) is turned off and closed 17 | # outside of the 'with' block 18 | # you can override this this 'switch_off' flag 19 | 20 | with blink1(switch_off=False) as b1: 21 | b1.fade_to_color(300, 'pink') 22 | time.sleep(2) 23 | 24 | with blink1() as b1: 25 | b1.fade_to_color(300, 'aqua') 26 | time.sleep(2) 27 | -------------------------------------------------------------------------------- /blink1_demo/demo_pattern1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo_pattern1 -- demo of blink1 library color pattern playing 5 | """ 6 | import time,sys 7 | from blink1.blink1 import Blink1 8 | 9 | try: 10 | blink1 = Blink1() 11 | except: 12 | print("no blink1 found") 13 | sys.exit() 14 | print("blink(1) found") 15 | 16 | print(" serial number: " + blink1.get_serial_number()) 17 | print(" firmware version: " + blink1.get_version()) 18 | 19 | print("playing full entire pattern") 20 | blink1.play(); 21 | 22 | print("waiting for 10 seconds") 23 | time.sleep(10.0) 24 | 25 | print("stopping pattern") 26 | blink1.stop() 27 | 28 | print("done") 29 | -------------------------------------------------------------------------------- /publishing.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Publishing notes 4 | ================= 5 | 6 | To develop and test, see the "Developer Installation" in the README. 7 | 8 | To publish: 9 | 10 | 1. Edit `pyproject.toml` to bump version 11 | 2. Update README.md (use `pydoc blink1.blink1.Blink1 > api.txt` to generate new API ref) 12 | 3. Check can build with `python3 -m build` 13 | 3. Check changes into git 14 | 4. Publish to PyPI with: (must have pypi API token handy) 15 | 16 | ``` 17 | python3 -m pip install --upgrade twine 18 | python3 -m build 19 | # to test 20 | python3 -m twine upload --repository testpypi dist/* 21 | # for reals 22 | python3 -m twine upload dist/* 23 | ``` 24 | 4. Verify new version on PyPi: https://pypi.org/project/blink1/ 25 | -------------------------------------------------------------------------------- /blink1_demo/demo1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo1 -- simple demo of blink1 library 5 | 6 | """ 7 | import time,sys 8 | from blink1.blink1 import Blink1 9 | 10 | try: 11 | blink1 = Blink1() 12 | except: 13 | print("no blink1 found") 14 | sys.exit() 15 | 16 | print("blink(1) found") 17 | 18 | print(" serial number: " + blink1.get_serial_number()) 19 | print(" firmware version: " + blink1.get_version()) 20 | 21 | print("fading to #ffffff") 22 | blink1.fade_to_rgb(1000, 255, 255, 255) 23 | 24 | print("waiting 2.5 seconds, unplug to check error raising..."); 25 | time.sleep(2.5) 26 | 27 | print("fading to #000000") 28 | blink1.fade_to_color(1000, 'black') 29 | 30 | print("closing connection to blink(1)") 31 | blink1.close() 32 | 33 | print("done") 34 | -------------------------------------------------------------------------------- /blink1_tests/hidapitest0.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import hid 4 | 5 | REPORT_ID = 1 6 | VENDOR_ID = 0x27b8 7 | PRODUCT_ID = 0x01ed 8 | 9 | devs = hid.enumerate( VENDOR_ID, PRODUCT_ID) 10 | #print(devs) 11 | serials = [] 12 | for d in devs: 13 | print(d) 14 | serials.append(d.get('serial_number')) 15 | 16 | serialz = map(lambda d:d.get('serial_number'), devs) 17 | 18 | serial_number = None 19 | 20 | #devs = map(lambda d: print(d), devs) 21 | print("serials:", serials) 22 | print("serialz:", serialz) 23 | 24 | hidraw = hid.device( VENDOR_ID, PRODUCT_ID, serial_number ) 25 | hidraw.open( VENDOR_ID, PRODUCT_ID, serial_number ) 26 | #hidraw.send_feature_report([0x02, 0x10, 0x00,0x00,0x00,0x00,0x00,0x00]) 27 | featureReport = [REPORT_ID, 99, 255, 0, 255, 0, 11, 0, 0]; 28 | hidraw.send_feature_report( featureReport ) 29 | -------------------------------------------------------------------------------- /blink1_demo/demo_pattern2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo_pattern2 -- demo of blink1 library color pattern writing 5 | """ 6 | import time,sys 7 | from blink1.blink1 import Blink1 8 | 9 | try: 10 | blink1 = Blink1() 11 | except: 12 | print("no blink1 found") 13 | sys.exit() 14 | print("blink(1) found") 15 | 16 | print(" serial number: " + blink1.get_serial_number()) 17 | print(" firmware version: " + blink1.get_version()) 18 | 19 | # write 100msec fades to green then yellow then black at lines 3,4,5 20 | print("writing green,yellow,black to pattern positions 3,4,5") 21 | blink1.write_pattern_line( 500, 'green', 3) 22 | blink1.write_pattern_line( 500, 'yellow', 4) 23 | blink1.write_pattern_line( 500, 'black', 5) 24 | 25 | print("playing created subpattern 4 times") 26 | blink1.play( 3,5, 4) # play that sub-loop 4 times 27 | 28 | print("done (pattern will continue to play") 29 | blink1.close() 30 | -------------------------------------------------------------------------------- /blink1/flash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | import click 5 | # import webcolors 6 | from blink1.blink1 import Blink1 7 | 8 | 9 | @click.command() 10 | @click.option('--on', default='white', help='Color to flash on') 11 | @click.option('--off', default='black', help='Color to flash off') 12 | @click.option('--duration', default=1, help='Length of each flash cycle in seconds') 13 | @click.option('--repeat', default=2, help='Number of times to flash') 14 | @click.option('--fade', default=0.2, help='Fade time in seconds') 15 | def flash(on, off, duration, repeat, fade): 16 | blink1 = Blink1() 17 | 18 | for i in range(0, repeat): 19 | blink1.fade_to_color(fade * 1000, on) 20 | time.sleep(duration/2.0) 21 | blink1.fade_to_color(fade * 1000, off) 22 | time.sleep(duration/2.0) 23 | 24 | blink1.fade_to_rgb(fade * 1000, 0, 0, 0) 25 | 26 | 27 | if __name__ == '__main__': 28 | flash() 29 | -------------------------------------------------------------------------------- /blink1_demo/demo_servertickle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo_servertickle -- demo of blink1 library servertickle feature 5 | """ 6 | import time,sys 7 | from blink1.blink1 import Blink1 8 | 9 | try: 10 | blink1 = Blink1() 11 | except: 12 | print("no blink1 found") 13 | sys.exit() 14 | print("blink(1) found") 15 | 16 | print(" serial number: " + blink1.get_serial_number()) 17 | print(" firmware version: " + blink1.get_version()) 18 | 19 | print("setting blink(1) green") 20 | blink1.fade_to_color(100, 'green') 21 | 22 | for i in range(0,5): 23 | print("enabling server tickle for 5 seconds") 24 | blink1.server_tickle( enable=True, timeout_millis=5000, stay_lit=True ) 25 | time.sleep(2.0) 26 | 27 | print("stopped updating servertickle, waiting for 10 seconds") 28 | print("blink1 will play its pattern in 5 seconds") 29 | time.sleep(10) 30 | 31 | print("Disabling servertickle") 32 | blink1.server_tickle( enable=False ) 33 | 34 | blink1.close() 35 | -------------------------------------------------------------------------------- /blink1_tests/test_kelvin.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import math 3 | import mock 4 | import time 5 | from blink1.kelvin import kelvin_to_rgb 6 | from blink1.blink1 import blink1 7 | 8 | 9 | class TestGamma(unittest.TestCase): 10 | 11 | def test_red_6000(self): 12 | r, g, b = kelvin_to_rgb(6000) 13 | self.assertEquals(r, 255) 14 | 15 | def test_blue_6700(self): 16 | r, g, b = kelvin_to_rgb(6700) 17 | self.assertEquals(b, 255) 18 | 19 | def test_range(self): 20 | """Verify that it is possible to produce the entire color range""" 21 | 22 | for k in range(10, 12000, 400): 23 | r,g,b = kelvin_to_rgb(k) 24 | self.assertIsInstance(r, int) 25 | self.assertIsInstance(g, int) 26 | self.assertIsInstance(b, int) 27 | 28 | with blink1(white_point=(r,g,b)) as b1: 29 | b1.fade_to_color(0, 'white') 30 | time.sleep(0.05) 31 | 32 | 33 | if __name__ == '__main__': 34 | unittest.main() -------------------------------------------------------------------------------- /blink1_tests/test_gamma.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import math 3 | import mock 4 | from blink1.blink1 import ColorCorrect 5 | 6 | 7 | class TestGamma(unittest.TestCase): 8 | 9 | def testUnity(self): 10 | g = ColorCorrect(gamma=(1,1,1), white_point=(255,255,255)) 11 | self.assertEquals( 12 | g(255,255,255), 13 | (255,255,255) 14 | ) 15 | 16 | def testFullPowerHalfGamma(self): 17 | expected = round(255 * (1 ** 0.5)) 18 | g = ColorCorrect(gamma=(0.5,0.5,0.5), white_point=(255,255,255)) 19 | self.assertEquals( 20 | g(255,255,255), 21 | (expected,expected,expected) 22 | ) 23 | 24 | def testHalfPowerHalfGamma(self): 25 | expected = round(255 * (127 / 255) ** 0.5) 26 | g = ColorCorrect(gamma=(0.5,0.5,0.5), white_point=(255,255,255)) 27 | self.assertEquals( 28 | g(127,127,127), 29 | (expected,expected,expected) 30 | ) 31 | 32 | if __name__ == '__main__': 33 | unittest.main() -------------------------------------------------------------------------------- /blink1_demo/demo_leds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo_leds -- demo of blink1 library showing independent LED access 5 | 6 | """ 7 | import time,sys 8 | from blink1.blink1 import Blink1 9 | 10 | try: 11 | blink1 = Blink1() 12 | except: 13 | print("no blink1 found") 14 | sys.exit() 15 | 16 | print("fading to 255,0,0 on LED1" ) 17 | blink1.fade_to_rgb(500, 255, 0, 0, 1) 18 | print("fading to 0,0,255 on LED2" ) 19 | blink1.fade_to_rgb(500, 0, 0, 255, 2) 20 | 21 | time.sleep(1.0) 22 | 23 | print("fading to blue on LED1" ) 24 | blink1.fade_to_color(500, 'blue', 1) 25 | print("fading to red on LED2" ) 26 | blink1.fade_to_color(500, 'red', 2) 27 | 28 | time.sleep(1.0) 29 | 30 | print("fading to black on both LEDs") 31 | blink1.fade_to_color(1000, 'black', 0) 32 | 33 | time.sleep(1.0) 34 | 35 | print("fading to green on both LEDs") 36 | blink1.fade_to_color(500, '#00FF00') 37 | 38 | time.sleep(1.0) 39 | print("fading to black on both LEDs") 40 | blink1.fade_to_color(500, 'black') 41 | 42 | print("closing connection to blink(1)") 43 | blink1.close() 44 | 45 | print("done") 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2017 Salim Fadhley, Tod E. Kurt 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 61.0.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "blink1" 7 | version = "0.4.0" 8 | description = "Official blink(1) control library" 9 | readme = "README.md" 10 | license = "MIT" 11 | authors = [ 12 | { name = "Salim Fadhley", email = "salimfadhley@gmail.com" }, 13 | { name = "Tod E. Kurt", email = "todbotdotcom@gmail.com" }, 14 | ] 15 | classifiers = [ 16 | "Development Status :: 2 - Pre-Alpha", 17 | "Environment :: Console", 18 | "Intended Audience :: Developers", 19 | "Natural Language :: English", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python :: 3", 22 | "Topic :: Software Development :: Testing", 23 | ] 24 | dependencies = [ 25 | "click", 26 | "hidapi>=0.13.1", 27 | "webcolors", 28 | ] 29 | 30 | [project.scripts] 31 | blink1-flash = "blink1.flash:flash" 32 | blink1-shine = "blink1.shine:shine" 33 | 34 | [project.urls] 35 | Homepage = "https://github.com/todbot/blink1-python" 36 | 37 | [tool.setuptools.packages.find] 38 | where = ["."] 39 | include = ["blink1"] 40 | -------------------------------------------------------------------------------- /blink1_tests/test_b1_context_manager.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | 4 | from blink1.blink1 import blink1 5 | from blink1.kelvin import COLOR_TEMPERATURES 6 | 7 | 8 | class TestBlink1ContextManager(unittest.TestCase): 9 | def test_cm(self): 10 | with blink1() as b1: 11 | b1.fade_to_color(0, "teal") 12 | 13 | def test_cm_closed(self): 14 | with blink1() as b1: 15 | b1.close() 16 | b1.fade_to_color(0, "teal") 17 | 18 | def test_cm_with_gamma(self): 19 | with blink1(gamma=(.5, .5, .5)) as b1: 20 | b1.fade_to_color(0, "teal") 21 | 22 | def test_cm_with_white_point(self): 23 | with blink1(white_point=(255, 255, 255)) as b1: 24 | b1.fade_to_color(0, "white") 25 | time.sleep(0.1) 26 | 27 | def test_cm_with_white_point(self): 28 | with blink1(white_point=COLOR_TEMPERATURES['candle']) as b1: 29 | b1.fade_to_color(0, "white") 30 | time.sleep(0.1) 31 | 32 | def test_cm_with_white_point(self): 33 | with blink1(white_point='blue-sky') as b1: 34 | b1.fade_to_color(0, "white") 35 | time.sleep(0.1) 36 | 37 | 38 | 39 | if __name__ == '__main__': 40 | unittest.main() -------------------------------------------------------------------------------- /blink1_demo/demo_logging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo_logging -- simple demo of blink1 library with logging 5 | 6 | run with: 7 | DEBUGLBLINK1=1 python3 ./blink1_demo/demo_logging.py 8 | 9 | """ 10 | from blink1.blink1 import Blink1 11 | 12 | import time,sys 13 | 14 | import logging 15 | 16 | logging.basicConfig() 17 | #logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) 18 | #logging.basicConfig(filename="test.log", format='%(filename)s: %(message)s', filemode='w') 19 | log = logging.getLogger(__name__) 20 | log.setLevel(logging.DEBUG) 21 | log.info("hello info") 22 | log.debug("hello debug") 23 | log.warning("hello warning") 24 | log.error("hello error") 25 | 26 | try: 27 | blink1 = Blink1() 28 | except: 29 | log.error("no blink1 found") 30 | sys.exit() 31 | 32 | log.info("blink(1) found") 33 | 34 | log.info(" serial number: " + blink1.get_serial_number()) 35 | log.info(" firmware version: " + blink1.get_version()) 36 | 37 | log.info("fading to #ffffff") 38 | blink1.fade_to_rgb(1000, 255, 255, 255) 39 | 40 | log.info("waiting 2.5 seconds, unplug to check error raising..."); 41 | time.sleep(2.5) 42 | 43 | log.info("fading to #000000") 44 | blink1.fade_to_color(1000, 'black') 45 | 46 | log.info("closing connection to blink(1)") 47 | blink1.close() 48 | 49 | log.info("done") 50 | -------------------------------------------------------------------------------- /blink1_demo/demo_pattern3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | demo_pattern3 -- demo of blink1 library color pattern parsing 5 | """ 6 | import time,sys 7 | from blink1.blink1 import Blink1 8 | 9 | pattern_strs = [ 10 | '3, #ff00ff,0.3,1, #00ff00,0.1,2,#ff00ff,0.3,2,#00ff00,0.1,1,#000000,0.5,0', 11 | '5, #FF0000,0.2,0,#000000,0.2,0', 12 | ' 10, #ff00ff,0.3,1, #00ff00,0.1,2, #ff00ff,0.3,2, #00ff00,0.1,1' , 13 | ' 10, #ff00ff,0.3,1, #00ff00,0.1,2 ', 14 | ' 7, #ff00ff,0.3,1, #00ff00,0.1,2, #ff3333 ', 15 | ' 1, ', 16 | ' 2, #ff99cc,,0.3', 17 | ' 0, #ff00ff,0.3', 18 | ] 19 | 20 | patt_str = pattern_strs[1] 21 | 22 | (num_repeats,pattern_list) = Blink1.parse_pattern(patt_str) 23 | print('num_repeats: ', num_repeats) 24 | print('pattern_list: ', pattern_list) 25 | 26 | try: 27 | blink1 = Blink1() 28 | except: 29 | print("no blink1 found") 30 | sys.exit() 31 | print("blink(1) found") 32 | 33 | play_on_blink1 = True 34 | #play_on_blink1 = False 35 | 36 | if( play_on_blink1 ): 37 | print("playing pattern on blink1 device (thus not blocking)"); 38 | blink1.play_pattern( patt_str ) 39 | print("sleeping for 10 secs...") 40 | time.sleep(10) 41 | else: 42 | print("playing using Python (blocking)") 43 | blink1.play_pattern( patt_str, on_device=False ) 44 | 45 | blink1.off() 46 | 47 | blink1.close() 48 | -------------------------------------------------------------------------------- /blink1_demo/demo_multi_blink1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | demo2 -- demo of blink1 library, accessing multiple blink(1) devices 4 | 5 | """ 6 | 7 | import time,sys 8 | from blink1.blink1 import Blink1 9 | 10 | blink1_serials = Blink1.list() 11 | if blink1_serials: 12 | print("blink(1) devices found: "+ ','.join(blink1_serials)) 13 | else: 14 | print("no blink1 found") 15 | sys.exit() 16 | 17 | # To open a particular blink(1), do: 18 | # blink1 = Blink1(serial_number=u'20006487') 19 | print("opening first blink(1)") 20 | blink1 = Blink1( serial_number=blink1_serials[0] ) # first blink1 21 | print(" serial number: " + blink1.get_serial_number()) 22 | print(" firmware version: " + blink1.get_version()) 23 | 24 | print(" playing green, purple, off...") 25 | blink1.fade_to_rgb(500, 0, 255, 0) 26 | time.sleep(0.5) 27 | blink1.fade_to_rgb(500, 255, 0, 255) 28 | time.sleep(0.5) 29 | blink1.fade_to_rgb(500, 0, 0, 0) 30 | time.sleep(0.5) 31 | 32 | print("closing.") 33 | blink1.close() 34 | 35 | print("opening last blink(1)") 36 | blink1 = Blink1( serial_number=blink1_serials[-1] ) # last blink1 37 | print(" serial number: " + blink1.get_serial_number()) 38 | print(" firmware version: " + blink1.get_version()) 39 | print(" playing aqua, off...") 40 | 41 | blink1.fade_to_color(200, 'aqua') 42 | time.sleep(0.5) 43 | blink1.fade_to_color(200, 'black') 44 | time.sleep(0.5) 45 | 46 | print("closing.") 47 | blink1.close() 48 | -------------------------------------------------------------------------------- /blink1_tests/test_simple_light_control.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import mock 4 | from blink1.blink1 import Blink1, BlinkConnectionFailed, InvalidColor 5 | 6 | 7 | class TestSimpleLightControl(unittest.TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | cls.b1 = Blink1() 11 | 12 | @classmethod 13 | def tearDownClass(cls): 14 | cls.b1.off() 15 | cls.b1.close() 16 | del cls.b1 17 | 18 | def testOn(self): 19 | self.b1.fade_to_color(1000, 'white') 20 | 21 | def testInvalidColor(self): 22 | with self.assertRaises(InvalidColor): 23 | self.b1.fade_to_color(1000, 'moomintrol') 24 | 25 | def testAlsoWhite(self): 26 | self.b1.fade_to_color(1000, (255,255,255)) 27 | 28 | def testAWhiteShadeOfPale(self): 29 | self.b1.fade_to_color(1000, '#ffffff') 30 | 31 | def testAGreyerShadeOfPale(self): 32 | self.b1.fade_to_color(1000, '#eeeeee') 33 | 34 | def testAnImplausibleShadeOfWhite(self): 35 | with self.assertRaises(InvalidColor): 36 | self.b1.fade_to_color(1000, '#xxxxxx') 37 | 38 | def testOff(self): 39 | self.b1.off() 40 | 41 | def test_get_firmware_version(self): 42 | ver = self.b1.get_version() 43 | 44 | def test_get_serial_number(self): 45 | sn = self.b1.get_serial_number() 46 | 47 | 48 | class TestFailedConnection(unittest.TestCase): 49 | def testCannotFind(self): 50 | with mock.patch('blink1.blink1.PRODUCT_ID', '0101'): 51 | with self.assertRaises(BlinkConnectionFailed): 52 | b1 = Blink1() 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | .venv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | .spyproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # mkdocs documentation 97 | /site 98 | 99 | # mypy 100 | .mypy_cache/ 101 | 102 | # PyCharm 103 | .idea 104 | -------------------------------------------------------------------------------- /blink1/kelvin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python implementation of Tanner Helland's color color conversion code. 4 | http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ 5 | """ 6 | 7 | import math 8 | 9 | # Aproximate colour temperatures for common lighting conditions. 10 | COLOR_TEMPERATURES = { 11 | 'candle': 1900, 12 | 'sunrise': 2000, 13 | 'incandescent': 2500, 14 | 'tungsten': 3200, 15 | 'halogen': 3350, 16 | 'sunlight': 5000, 17 | 'overcast': 6000, 18 | 'shade': 7000, 19 | 'blue-sky': 10000, 20 | 'warm-fluorescent': 2700, 21 | 'fluorescent': 37500, 22 | 'cool-fluorescent': 5000, 23 | } 24 | 25 | 26 | def correct_output(luminosity): 27 | """ 28 | :param luminosity: Input luminosity 29 | :return: Luminosity limited to the 0 <= l <= 255 range. 30 | """ 31 | if luminosity < 0: 32 | val = 0 33 | elif luminosity > 255: 34 | val = 255 35 | else: 36 | val = luminosity 37 | return round(val) 38 | 39 | 40 | def kelvin_to_rgb(kelvin): 41 | """ 42 | Convert a color temperature given in kelvin to an approximate RGB value. 43 | 44 | :param kelvin: Color temp in K 45 | :return: Tuple of (r, g, b), equivalent color for the temperature 46 | """ 47 | temp = kelvin / 100.0 48 | 49 | # Calculate Red: 50 | if temp <= 66: 51 | red = 255 52 | else: 53 | red = 329.698727446 * ((temp - 60) ** -0.1332047592) 54 | 55 | # Calculate Green: 56 | 57 | if temp <= 66: 58 | green = 99.4708025861 * math.log(temp) - 161.1195681661 59 | else: 60 | green = 288.1221695283 * ((temp - 60) ** -0.0755148492) 61 | 62 | # Calculate Blue: 63 | if temp > 66: 64 | blue = 255 65 | elif temp <= 19: 66 | blue = 0 67 | else: 68 | blue = 138.5177312231 * math.log(temp - 10) - 305.0447927307 69 | 70 | return tuple(correct_output(c) for c in (red, green, blue)) 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python Blink(1) library 4 | ======================== 5 | 6 | Official Python library for blink(1) USB RGB LED notification devices 7 | https://blink1.thingm.com/ 8 | 9 | * [About this library](#about-this-library) 10 | * [Installation](#installation) 11 | * [Example Code and Installed scripts](#example-code-and-installed-scripts) 12 | * [OS-specific notes](#os-specific-notes) 13 | * [Linux:](#linux) 14 | * [Mac OS X:](#mac-os-x) 15 | * [Windows:](#windows) 16 | * [Use](#use) 17 | * [Colors](#colors) 18 | * [Pattern playing](#pattern-playing) 19 | * [Servertickle watchdog](#servertickle-watchdog) 20 | * [Gamma correction](#gamma-correction) 21 | * [White point correction](#white-point-correction) 22 | * [API reference](#api-reference) 23 | * [Developer installation](#developer-installation) 24 | 25 | ## About this library 26 | 27 | Features of this library: 28 | 29 | * Test coverage on all library components 30 | * Python 3.x compatible 31 | * Automatic installation via Python Package Index 32 | * High level control over the blink(1) 33 | * Single implementation with `cython-hidapi` USB HID API (PyUSB cannot access HID devices on all OSes) 34 | 35 | This library lives at https://github.com/todbot/blink1-python 36 | 37 | Originally written by @salimfadhley, at https://github.com/salimfadhley/blink1/tree/master/python/pypi. 38 | Moved to this repository and rewritten for `cython-hidapi` by @todbot. 39 | 40 | ## Installation 41 | 42 | Use the `pip` utility to fetch the latest release of this package and any 43 | additional components required in a single step: 44 | ``` 45 | pip install blink1 46 | ``` 47 | 48 | ## Example Code and Installed scripts 49 | Two command-line scripts `blink1-shine` and `blink1-flash` are installed when this library is installed. 50 | * `blink1-shine` – Tell the blink(1) to be specifc steady color 51 | * `blink1-flash` – Flash the blink(1) two different colors at a specific rate 52 | 53 | For examples, see the [`blink1_demo`](./blink1_demo/) directory for several examples on how to use this library. 54 | 55 | ## OS-specific notes 56 | The `blink1-python` library relies on [cython-hidapi](https://github.com/trezor/cython-hidapi) for USB HID access. This package may require a C compiler and attendant utilities to be installed before installing this library. 57 | 58 | ### Linux: 59 | The following extra packages must be installed: 60 | ``` 61 | sudo apt-get install python-dev libusb-1.0-0-dev libudev-dev 62 | ``` 63 | And udev rules for non-root user access to blink(1) devices: 64 | ``` 65 | echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="27b8", ATTRS{idProduct}=="01ed", MODE:="666", GROUP="plugdev"' | sudo tee /etc/udev/rules.d/51-blink1.rules 66 | sudo udevadm control --reload 67 | sudo udevadm trigger 68 | ``` 69 | 70 | ### Mac OS X: 71 | Install [Xcode](https://developer.apple.com/xcode/) with command-line tools. 72 | 73 | ### Windows: 74 | You may need [Microsoft Visual C++ Compiler for Python 2.7](http://aka.ms/vcpython27) 75 | 76 | ## Use 77 | 78 | The simplest way to use this library is via a context manager. 79 | ``` 80 | import time 81 | from blink1.blink1 import blink1 82 | 83 | with blink1() as b1: 84 | b1.fade_to_color(100, 'navy') 85 | time.sleep(10) 86 | ``` 87 | 88 | When the blink1() block exits the light is automatically switched off. 89 | It is also possible to access the exact same set of functions without the context manager: 90 | ``` 91 | import time 92 | from blink1.blink1 import Blink1 93 | 94 | b1 = Blink1() 95 | b1.fade_to_rgb(1000, 64, 64, 64) 96 | time.sleep(3) 97 | b1.fade_to_rgb(1000, 255, 255, 255) 98 | ``` 99 | 100 | Unlike the context manager, this demo will leave the blink(1) open at the end of execution. 101 | To close it, use the `b1.close()` method. 102 | 103 | To list all connected blink(1) devices: 104 | ``` 105 | from blink1.blink1 import Blink1 106 | blink1_serials = Blink1.list() 107 | print("blink(1) devices found: " + ','.join(blink1_serials)) 108 | ``` 109 | 110 | To open a particular blink(1) device by serial number, pass in its serial number as a Unicode string: 111 | ``` 112 | from blink1.blink1 import blink1 113 | blink1 = Blink1(serial_number=u'20002345') 114 | blink1.fade_to_rgb(1000, 255,0,255) 115 | blink1.close() 116 | ``` 117 | 118 | ### Colors 119 | 120 | There are a number of ways to specify colors in this library: 121 | ``` 122 | blink1.fade_to_color(1000, '#ffffff') # Hexdecimal RGB as a string 123 | blink1.fade_to_color(1000, 'green') # Named color - any color name understood by css3 124 | blink1.fade_to_color(1000, (22,33,44) # RGB as a tuple. Luminance values are 0 <= lum <= 255 125 | ``` 126 | Attempting to select a color outside the plausible range will generate an InvalidColor exception. 127 | 128 | 129 | ### Pattern playing 130 | 131 | The blink(1) device has a 16-line non-volatile color pattern memory. 132 | This color pattern plays automatically if power is applied but not connected to a computer. 133 | You can also trigger this pattern (or sub-patterns) over USB, 134 | leaving your application free to do other things besides blink lights. 135 | 136 | Each line in the color pattern consists of an R,G,B triplet and a fade time to reach that color. 137 | 138 | To play the pattern in blink(1) or sub-patterns: 139 | ``` 140 | blink1.play() # play entire color pattern, infinitely looped 141 | blink1.stop() # stop a color pattern playing (if playing) 142 | 143 | blink1.play(2,3, count=7) # play color pattern lines 2,3 in a loop 7 times 144 | ``` 145 | 146 | To alter the lines of the pattern memory: 147 | ``` 148 | # write 100msec fades to green then yellow then black at lines 3,4,5 149 | blink1.write_pattern_line( 100, 'green', 3) 150 | blink1.write_pattern_line( 100, 'yellow', 4) 151 | blink1.write_pattern_line( 100, 'black', 5) 152 | 153 | blink1.play( 3,5, 4) # play that sub-loop 4 times 154 | ``` 155 | 156 | To save the pattern to non-volatile memory (overwriting the factory pattern): 157 | ``` 158 | blink1.save_pattern() 159 | ``` 160 | 161 | To quickly play a pattern in Blink1Control-style string format: 162 | ``` 163 | # play purple on LED1 in 300ms, green on LED2 in 100ms, then swap, for 10 times 164 | pattern_str = '10, #ff00ff,0.3,1, #00ff00,0.1,2, #ff00ff,0.3,2, #00ff00,0.1,1' 165 | blink1.play_pattern(pattern_str) 166 | # wait 5 seconds while the pattern plays on the blink1 167 | # (or go do something more useful) 168 | time.sleep(5.0) 169 | # flash red-off 5 times fast on all LEDs 170 | blink1.play_pattern('5, #FF0000,0.2,0,#000000,0.2,0') 171 | ``` 172 | 173 | ### Servertickle watchdog 174 | blink(1) also has a "watchdog" of sorts called "servertickle". 175 | When enabled, you must periodically send it to the blink(1) or it will 176 | trigger, playing the stored color pattern. This is useful to announce 177 | a computer that has crashed. The blink(1) will flash on its own until 178 | told otherwise. 179 | 180 | To use, enable severtickle with a timeout value (max timeout 62 seconds): 181 | ``` 182 | blink1.server_tickle(enable=True, timeout_millis=2000) 183 | ``` 184 | 185 | 186 | ### Gamma correction 187 | 188 | The context manager supports a ''gamma'' argument which allows you to supply a per-channel gamma correction value. 189 | ``` 190 | from blink1.blink1 import blink1 191 | 192 | with blink1(gamma=(2, 2, 2)) as b1: 193 | b1.fade_to_color(100, 'pink') 194 | time.sleep(10) 195 | ``` 196 | This example provides a gamma correction of 2 to each of the three colour channels. 197 | 198 | Higher values of gamma make the blink(1) appear more colorful but decrease the brightness of colours. 199 | 200 | ### White point correction 201 | 202 | The human eye's perception of color can be influenced by ambient lighting. In some circumstances it may be desirable 203 | to apply a small color correction in order to make colors appear more accurate. For example, if we were operating 204 | the blink(1) in a room lit predominantly by candle-light: 205 | ``` 206 | with blink1(white_point='candle', switch_off) as b1: 207 | b1.fade_to_color(100, 'white') 208 | ``` 209 | Viewed in daylight this would make the Blink(1) appear yellowish, however in a candle-lit room this would be perceived 210 | as a more natural white. If we did not apply this kind of color correction the Blink(1) would appear blueish. 211 | 212 | The following values are acceptable white-points: 213 | 214 | * Any triple of (r,g,b). Each 0 <= luminance <= 255 215 | * Any color_temperature expressed as an integer or float in Kelvin 216 | * A color temperature name. 217 | 218 | The library supports the following temperature names: 219 | 220 | * candle 221 | * sunrise 222 | * incandescent 223 | * tungsten 224 | * halogen 225 | * sunlight 226 | * overcast 227 | * shade 228 | * blue-sky 229 | 230 | ## API reference 231 | ``` 232 | Help on class Blink1 in blink1.blink1: 233 | 234 | blink1.blink1.Blink1 = class Blink1 235 | | Light controller class, sends messages to the blink(1) via USB HID. 236 | | 237 | | Methods defined here: 238 | | 239 | | __init__(self, serial_number=None, gamma=None, white_point=None) 240 | | :param serial_number: serial number of blink(1) to open, otherwise first found 241 | | :param gamma: Triple of gammas for each channel e.g. (2, 2, 2) 242 | | 243 | | clear_pattern(self) 244 | | Clear entire color pattern in blink(1) 245 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 246 | | 247 | | close(self) 248 | | 249 | | fade_to_color(self, fade_milliseconds, color, ledn=0) 250 | | Fade the light to a known colour 251 | | :param fade_milliseconds: Duration of the fade in milliseconds 252 | | :param color: Named color to fade to (e.g. "#FF00FF", "red") 253 | | :param ledn: which led to control 254 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 255 | | 256 | | fade_to_rgb(self, fade_milliseconds, red, green, blue, ledn=0) 257 | | Command blink(1) to fade to RGB color 258 | | :param fade_milliseconds: millisecs duration of fade 259 | | :param red: 0-255 260 | | :param green: 0-255 261 | | :param blue: 0-255 262 | | :param ledn: which LED to control (0=all, 1=LED A, 2=LED B) 263 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 264 | | 265 | | fade_to_rgb_uncorrected(self, fade_milliseconds, red, green, blue, ledn=0) 266 | | Command blink(1) to fade to RGB color, no color correction applied. 267 | | :raises: Blink1ConnectionFailed if blink(1) is disconnected 268 | | 269 | | get_serial_number(self) 270 | | Get blink(1) serial number 271 | | :return blink(1) serial number as string 272 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 273 | | 274 | | get_version(self) 275 | | Get blink(1) firmware version 276 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 277 | | 278 | | notfound(self) 279 | | 280 | | off(self) 281 | | Switch the blink(1) off instantly 282 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 283 | | 284 | | play(self, start_pos=0, end_pos=0, count=0) 285 | | Play internal color pattern 286 | | :param start_pos: pattern line to start from 287 | | :param end_pos: pattern line to end at 288 | | :param count: number of times to play, 0=play forever 289 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 290 | | 291 | | play_pattern(self, pattern_str, onDevice=True) 292 | | Play a Blink1Control-style pattern string 293 | | :param pattern_str: The Blink1Control-style pattern string to play 294 | | :param onDevice: True (default) to run pattern on blink(1), 295 | | otherwise plays in Python process 296 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 297 | | 298 | | play_pattern_local(self, pattern_str) 299 | | Play a Blink1Control pattern string in Python process 300 | | (plays in blink1-python, so blocks) 301 | | :param pattern_str: The Blink1Control-style pattern string to play 302 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 303 | | 304 | | read(self) 305 | | Read command result from blink(1), low-level internal use 306 | | Receive USB Feature Report 0x01 from blink(1) with 8-byte payload 307 | | Note: buf must be 8 bytes or bad things happen 308 | | 309 | | read_pattern(self) 310 | | Read the entire color pattern 311 | | :return List of pattern line tuples 312 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 313 | | 314 | | read_pattern_line(self, pos) 315 | | Read a color pattern line at position 316 | | :param pos: pattern line to read 317 | | :return pattern line data as tuple (r,g,b, step_millis) or False on err 318 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 319 | | 320 | | save_pattern(self) 321 | | Save internal RAM pattern to flash 322 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 323 | | 324 | | server_tickle(self, enable, timeout_millis=0, stay_lit=False, start_pos=0, end_pos=16) 325 | | Enable/disable servertickle / serverdown watchdog 326 | | :param: enable: Set True to enable serverTickle 327 | | :param: timeout_millis: millisecs until servertickle is triggered 328 | | :param: stay_lit: Set True to keep current color of blink(1), False to turn off 329 | | :param: start_pos: Sub-pattern start position in whole color pattern 330 | | :param: end_pos: Sub-pattern end position in whole color pattern 331 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 332 | | 333 | | set_ledn(self, ledn=0) 334 | | Set the 'current LED' value for writePatternLine 335 | | :param ledn: LED to adjust, 0=all, 1=LEDA, 2=LEDB 336 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 337 | | 338 | | stop(self) 339 | | Stop internal color pattern playing 340 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 341 | | 342 | | write(self, buf) 343 | | Write command to blink(1), low-level internal use 344 | | Send USB Feature Report 0x01 to blink(1) with 8-byte payload 345 | | Note: arg 'buf' must be 8 bytes or bad things happen 346 | | :raises: Blink1ConnectionFailed if blink(1) is disconnected 347 | | 348 | | write_pattern_line(self, step_milliseconds, color, pos, ledn=0) 349 | | Write a color & step time color pattern line to RAM 350 | | :param step_milliseconds: how long for this pattern line to take 351 | | :param color: LED color 352 | | :param pos: color pattern line number (0-15) 353 | | :param ledn: LED number to adjust, 0=all, 1=LEDA, 2=LEDB 354 | | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 355 | | 356 | | ---------------------------------------------------------------------- 357 | | Static methods defined here: 358 | | 359 | | color_to_rgb(color) 360 | | Convert color name or hexcode to (r,g,b) tuple 361 | | :param color: a color string, e.g. "#FF00FF" or "red" 362 | | :raises: InvalidColor: if color string is bad 363 | | 364 | | find(serial_number=None) 365 | | Find a praticular blink(1) device, or the first one 366 | | :param serial_number: serial number of blink(1) device (from Blink1.list()) 367 | | :raises: Blink1ConnectionFailed: if blink(1) is not present 368 | | 369 | | list() 370 | | List blink(1) devices connected, by serial number 371 | | :return: List of blink(1) device serial numbers 372 | | 373 | | parse_pattern(pattern_str) 374 | | Parse a Blink1Control pattern string to a list of pattern lines 375 | | e.g. of the form '10,#ff00ff,0.1,0,#00ff00,0.1,0' 376 | | :param pattern_str: The Blink1Control-style pattern string to parse 377 | | :returns: an list of dicts of the parsed out pieces 378 | ``` 379 | 380 | 381 | ## Developer installation 382 | 383 | Having checked out the `blink1-python` library, cd to its directory and run the setup script: 384 | ``` 385 | git clone https://github.com/todbot/blink1-python 386 | cd blink1-python 387 | pip3 install --editable . 388 | python3 ./blink1_demo/demo1.py 389 | ``` 390 | You can now use the `blink1` package on your system and edit it. 391 | 392 | To get internal blink1 library debug, messages set the environment variable `DEBUGBLINK1`: 393 | ``` 394 | DEBUGBLINK1=1 python3 ./blink1_demo/demo_logging.py 395 | ``` 396 | 397 | To uninstall the development version: 398 | ``` 399 | pip3 uninstall blink1 400 | ``` 401 | -------------------------------------------------------------------------------- /blink1/blink1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | blink1.py -- blink(1) Python library using python hidapi 4 | 5 | All platforms: 6 | % pip3 install blink1 7 | 8 | """ 9 | import logging 10 | import time 11 | from contextlib import contextmanager 12 | import webcolors 13 | import hid 14 | import os 15 | # from builtins import str as text 16 | 17 | from .kelvin import kelvin_to_rgb, COLOR_TEMPERATURES 18 | 19 | 20 | class Blink1ConnectionFailed(RuntimeError): 21 | """Raised when we cannot connect to a Blink(1) 22 | """ 23 | 24 | 25 | class InvalidColor(ValueError): 26 | """Raised when the user requests an implausible colour 27 | """ 28 | 29 | 30 | log = logging.getLogger(__name__) 31 | if os.getenv('DEBUGBLINK1'): 32 | log.setLevel(logging.DEBUG) 33 | 34 | 35 | DEFAULT_GAMMA = (2, 2, 2) 36 | DEFAULT_WHITE_POINT = (255, 255, 255) 37 | 38 | REPORT_ID = 0x01 39 | VENDOR_ID = 0x27B8 40 | PRODUCT_ID = 0x01ED 41 | 42 | REPORT_SIZE = 9 # 8 bytes + 1 byte reportId 43 | 44 | 45 | class ColorCorrect(object): 46 | """Apply a gamma correction to any selected RGB color, see: 47 | http://en.wikipedia.org/wiki/Gamma_correction 48 | """ 49 | def __init__(self, gamma, white_point): 50 | """ 51 | :param gamma: Tuple of r,g,b gamma values 52 | :param white_point: White point expressed as (r,g,b), integer color 53 | temperature (in Kelvin) or a string value. 54 | 55 | All gamma values should be 0 > x >= 1 56 | """ 57 | 58 | self.gamma = gamma 59 | 60 | if isinstance(white_point, str): 61 | kelvin = COLOR_TEMPERATURES[white_point] 62 | self.white_point = kelvin_to_rgb(kelvin) 63 | elif isinstance(white_point, (int, float)): 64 | self.white_point = kelvin_to_rgb(white_point) 65 | else: 66 | self.white_point = white_point 67 | 68 | @staticmethod 69 | def gamma_correct(gamma, white, luminance): 70 | return round(white * (luminance / 255.0) ** gamma) 71 | 72 | def __call__(self, r, g, b): 73 | color = [r, g, b] 74 | return tuple( 75 | self.gamma_correct(g, w, l) 76 | for (g, w, l) in zip(self.gamma, self.white_point, color) 77 | ) 78 | 79 | 80 | class Blink1(object): 81 | """Light controller class, sends messages to the blink(1) via USB HID. 82 | """ 83 | def __init__(self, serial_number=None, gamma=None, white_point=None): 84 | """ 85 | :param serial_number: serial number of blink(1) to open, otherwise first found 86 | :param gamma: Triple of gammas for each channel e.g. (2, 2, 2) 87 | """ 88 | self.cc = ColorCorrect( 89 | gamma=gamma or DEFAULT_GAMMA, 90 | white_point=(white_point or DEFAULT_WHITE_POINT) 91 | ) 92 | self.dev = self.find(serial_number) 93 | if self.dev is None: 94 | print("wtf") 95 | 96 | def close(self): 97 | self.dev.close() 98 | self.dev = None 99 | 100 | @staticmethod 101 | def find(serial_number=None): 102 | """ Find a praticular blink(1) device, or the first one 103 | :param serial_number: serial number of blink(1) device (from Blink1.list()) 104 | :raises: Blink1ConnectionFailed: if blink(1) is not present 105 | """ 106 | try: 107 | hidraw = hid.device(VENDOR_ID, PRODUCT_ID, serial_number) 108 | hidraw.open(VENDOR_ID, PRODUCT_ID, serial_number) 109 | # hidraw = hid.device(VENDOR_ID,PRODUCT_ID,unicode(serial_number)) 110 | # hidraw.open(VENDOR_ID,PRODUCT_ID,unicode(serial_number)) 111 | except IOError as e: # python2 112 | raise Blink1ConnectionFailed(e) 113 | # hidraw = None 114 | except OSError as e: # python3 115 | raise Blink1ConnectionFailed(e) 116 | # hidraw = None 117 | 118 | return hidraw 119 | 120 | @staticmethod 121 | def list(): 122 | """ List blink(1) devices connected, by serial number 123 | :return: List of blink(1) device serial numbers 124 | """ 125 | try: 126 | devs = hid.enumerate(VENDOR_ID, PRODUCT_ID) 127 | serials = list(map(lambda d: d.get('serial_number'), devs)) 128 | return serials 129 | except IOError: 130 | return [] 131 | 132 | @staticmethod 133 | def notfound(): 134 | return None # fixme what to do here 135 | 136 | def write(self, buf): 137 | """ Write command to blink(1), low-level internal use 138 | Send USB Feature Report 0x01 to blink(1) with 8-byte payload 139 | Note: arg 'buf' must be 8 bytes or bad things happen 140 | :raises: Blink1ConnectionFailed if blink(1) is disconnected 141 | """ 142 | log.debug("blink1write:" + ",".join('0x%02x' % v for v in buf)) 143 | rc = self.dev.send_feature_report(buf) 144 | # return self.dev.send_feature_report(buf) 145 | if rc != REPORT_SIZE: 146 | raise Blink1ConnectionFailed( 147 | "write returned %d instead of %d" % (rc, REPORT_SIZE) 148 | ) 149 | 150 | def read(self): 151 | """ Read command result from blink(1), low-level internal use 152 | Receive USB Feature Report 0x01 from blink(1) with 8-byte payload 153 | Note: buf must be 8 bytes or bad things happen 154 | """ 155 | buf = self.dev.get_feature_report(REPORT_ID, 9) 156 | log.debug("blink1read: " + ",".join('0x%02x' % v for v in buf)) 157 | return buf 158 | 159 | def fade_to_rgb_uncorrected( 160 | self, 161 | fade_milliseconds, 162 | red, 163 | green, 164 | blue, 165 | ledn=0 166 | ): 167 | """ Command blink(1) to fade to RGB color, no color correction applied. 168 | :raises: Blink1ConnectionFailed if blink(1) is disconnected 169 | """ 170 | action = ord('c') 171 | fade_time = int(fade_milliseconds / 10) 172 | th = (fade_time & 0xff00) >> 8 173 | tl = fade_time & 0x00ff 174 | buf = [REPORT_ID, action, int(red), int(green), int(blue), th, tl, ledn, 0] 175 | self.write(buf) 176 | 177 | def fade_to_rgb(self, fade_milliseconds, red, green, blue, ledn=0): 178 | """ Command blink(1) to fade to RGB color 179 | :param fade_milliseconds: millisecs duration of fade 180 | :param red: 0-255 181 | :param green: 0-255 182 | :param blue: 0-255 183 | :param ledn: which LED to control (0=all, 1=LED A, 2=LED B) 184 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 185 | """ 186 | r, g, b = self.cc(red, green, blue) 187 | return self.fade_to_rgb_uncorrected(fade_milliseconds, r, g, b, ledn) 188 | 189 | @staticmethod 190 | def color_to_rgb(color): 191 | """ Convert color name or hexcode to (r,g,b) tuple 192 | :param color: a color string, e.g. "#FF00FF" or "red" 193 | :raises: InvalidColor: if color string is bad 194 | """ 195 | if isinstance(color, tuple): 196 | return color 197 | if color.startswith('#'): 198 | try: 199 | return webcolors.hex_to_rgb(color) 200 | except ValueError: 201 | raise InvalidColor(color) 202 | 203 | try: 204 | return webcolors.name_to_rgb(color) 205 | except ValueError: 206 | raise InvalidColor(color) 207 | 208 | def fade_to_color(self, fade_milliseconds, color, ledn=0): 209 | """ Fade the light to a known colour 210 | :param fade_milliseconds: Duration of the fade in milliseconds 211 | :param color: Named color to fade to (e.g. "#FF00FF", "red") 212 | :param ledn: which led to control 213 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 214 | """ 215 | red, green, blue = self.color_to_rgb(color) 216 | 217 | return self.fade_to_rgb(fade_milliseconds, red, green, blue, ledn) 218 | 219 | def off(self): 220 | """ Switch the blink(1) off instantly 221 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 222 | """ 223 | self.fade_to_color(0, 'black') 224 | 225 | def get_version(self): 226 | """ Get blink(1) firmware version 227 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 228 | """ 229 | buf = [REPORT_ID, ord('v'), 0, 0, 0, 0, 0, 0, 0] 230 | self.write(buf) 231 | time.sleep(.05) 232 | version_raw = self.read() 233 | version = (version_raw[3] - ord('0')) * 100 + (version_raw[4] - ord('0')) 234 | return str(version) 235 | 236 | def get_serial_number(self): 237 | """ Get blink(1) serial number 238 | :return blink(1) serial number as string 239 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 240 | """ 241 | return self.dev.get_serial_number_string() 242 | 243 | def play(self, start_pos=0, end_pos=0, count=0): 244 | """ Play internal color pattern 245 | :param start_pos: pattern line to start from 246 | :param end_pos: pattern line to end at 247 | :param count: number of times to play, 0=play forever 248 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 249 | """ 250 | if self.dev is None: 251 | raise Blink1ConnectionFailed("must open first") 252 | 253 | buf = [ 254 | REPORT_ID, 255 | ord('p'), 256 | 1, 257 | int(start_pos), 258 | int(end_pos), 259 | int(count), 260 | 0, 261 | 0, 262 | 0 263 | ] 264 | self.write(buf) 265 | 266 | def stop(self): 267 | """ Stop internal color pattern playing 268 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 269 | """ 270 | if self.dev is None: 271 | return False 272 | 273 | buf = [REPORT_ID, ord('p'), 0, 0, 0, 0, 0, 0, 0] 274 | self.write(buf) 275 | 276 | def save_pattern(self): 277 | """ Save internal RAM pattern to flash 278 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 279 | """ 280 | buf = [REPORT_ID, ord('W'), 0xBE, 0xEF, 0xCA, 0xFE, 0, 0, 0] 281 | self.write(buf) 282 | 283 | def set_ledn(self, ledn=0): 284 | """ Set the 'current LED' value for writePatternLine 285 | :param ledn: LED to adjust, 0=all, 1=LEDA, 2=LEDB 286 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 287 | """ 288 | buf = [REPORT_ID, ord('l'), ledn, 0, 0, 0, 0, 0, 0] 289 | self.write(buf) 290 | 291 | def write_pattern_line(self, step_milliseconds, color, pos, ledn=0): 292 | """ Write a color & step time color pattern line to RAM 293 | :param step_milliseconds: how long for this pattern line to take 294 | :param color: LED color 295 | :param pos: color pattern line number (0-15) 296 | :param ledn: LED number to adjust, 0=all, 1=LEDA, 2=LEDB 297 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 298 | """ 299 | self.set_ledn(ledn) 300 | red, green, blue = self.color_to_rgb(color) 301 | r, g, b = self.cc(red, green, blue) 302 | step_time = int(step_milliseconds / 10) 303 | th = (step_time & 0xff00) >> 8 304 | tl = step_time & 0x00ff 305 | buf = [REPORT_ID, ord('P'), int(r), int(g), int(b), th, tl, pos, 0] 306 | self.write(buf) 307 | 308 | def read_pattern_line(self, pos): 309 | """ Read a color pattern line at position 310 | :param pos: pattern line to read 311 | :return pattern line data as tuple (r,g,b, step_millis) or False on err 312 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 313 | """ 314 | buf = [REPORT_ID, ord('R'), 0, 0, 0, 0, 0, int(pos), 0] 315 | self.write(buf) 316 | buf = self.read() 317 | 318 | r, g, b = buf[2:5] 319 | 320 | step_millis = ((buf[5] << 8) | buf[6]) * 10 321 | return r, g, b, step_millis 322 | 323 | def read_pattern(self): 324 | """ Read the entire color pattern 325 | :return List of pattern line tuples 326 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 327 | """ 328 | pattern = [] 329 | for i in range(0, 32): # FIXME: adjustable for diff blink(1) models 330 | pattern.append(self.read_pattern_line(i)) 331 | return pattern 332 | 333 | def clear_pattern(self): 334 | """ Clear entire color pattern in blink(1) 335 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 336 | """ 337 | for i in range(0, 32): # FIXME: pattern length 338 | self.write_pattern_line(0, 'black', i) 339 | 340 | def play_pattern(self, pattern_str, onDevice=True): 341 | """ Play a Blink1Control-style pattern string 342 | :param pattern_str: The Blink1Control-style pattern string to play 343 | :param onDevice: True (default) to run pattern on blink(1), 344 | otherwise plays in Python process 345 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 346 | """ 347 | if not onDevice: 348 | return self.play_pattern_local(pattern_str) 349 | 350 | # else, play it in the blink(1) 351 | num_repeats, colorlist = self.parse_pattern(pattern_str) 352 | 353 | empty_color = { 354 | 'rgb': '#000000', 355 | 'time': 0.0, 356 | 'ledn': 0, 357 | 'millis': 0 358 | } 359 | 360 | colorlist += [empty_color] * (32 - len(colorlist)) 361 | 362 | for i, c in enumerate(colorlist): 363 | self.write_pattern_line(c['millis'], c['rgb'], i, c['ledn']) 364 | 365 | return self.play(count=num_repeats) 366 | 367 | def play_pattern_local(self, pattern_str): 368 | """ Play a Blink1Control pattern string in Python process 369 | (plays in blink1-python, so blocks) 370 | :param pattern_str: The Blink1Control-style pattern string to play 371 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 372 | """ 373 | num_repeats, colorlist = self.parse_pattern(pattern_str) 374 | if num_repeats == 0: 375 | num_repeats = -1 376 | 377 | while num_repeats: 378 | num_repeats -= 1 379 | 380 | for c in colorlist: 381 | self.fade_to_color(c['millis'], c['rgb'], c['ledn']) 382 | time.sleep(c['time']) 383 | 384 | @staticmethod 385 | def parse_pattern(pattern_str): 386 | """ Parse a Blink1Control pattern string to a list of pattern lines 387 | e.g. of the form '10,#ff00ff,0.1,0,#00ff00,0.1,0' 388 | :param pattern_str: The Blink1Control-style pattern string to parse 389 | :returns: an list of dicts of the parsed out pieces 390 | """ 391 | pattparts = pattern_str.replace(' ', '').split(',') 392 | num_repeats = int(pattparts[0]) # FIXME 393 | pattparts = pattparts[1:] 394 | 395 | colorlist = [] 396 | dpattparts = dict(enumerate(pattparts)) # lets us use .get(i,'default') 397 | for i in range(0, len(pattparts), 3): 398 | rgb = dpattparts.get(i + 0, '#000000') 399 | time_ = float(dpattparts.get(i + 1, 0.0)) 400 | ledn = int(dpattparts.get(i + 2, 0)) 401 | # set default if empty string 402 | rgb = rgb if rgb else '#000000' # sigh 403 | time_ = time_ if time_ else 0.0 # sigh 404 | ledn = ledn if ledn else 0 # sigh 405 | millis = int(time_ * 1000) 406 | color = { 407 | 'rgb': rgb, 408 | 'time': time_, 409 | 'ledn': ledn, 410 | 'millis': millis 411 | } 412 | 413 | colorlist.append(color) 414 | 415 | return num_repeats, colorlist 416 | 417 | def server_tickle( 418 | self, 419 | enable, 420 | timeout_millis=0, 421 | stay_lit=False, 422 | start_pos=0, 423 | end_pos=16 424 | ): 425 | """Enable/disable servertickle / serverdown watchdog 426 | :param: enable: Set True to enable serverTickle 427 | :param: timeout_millis: millisecs until servertickle is triggered 428 | :param: stay_lit: Set True to keep current color of blink(1), False to turn off 429 | :param: start_pos: Sub-pattern start position in whole color pattern 430 | :param: end_pos: Sub-pattern end position in whole color pattern 431 | :raises: Blink1ConnectionFailed: if blink(1) is disconnected 432 | """ 433 | if self.dev is None: 434 | return '' 435 | 436 | en = int(enable is True) 437 | timeout_time = int(timeout_millis / 10) 438 | th = (timeout_time & 0xff00) >> 8 439 | tl = timeout_time & 0x00ff 440 | st = int(stay_lit is True) 441 | buf = [REPORT_ID, ord('D'), en, th, tl, st, start_pos, end_pos, 0] 442 | self.write(buf) 443 | 444 | 445 | @contextmanager 446 | def blink1(switch_off=True, gamma=None, white_point=None): 447 | """Context manager which automatically shuts down the Blink(1) 448 | after use. 449 | :param switch_off: turn blink(1) off when existing context 450 | :param gamma: set gamma curve (as tuple) 451 | :param white_point: set white point (as tuple) 452 | """ 453 | b1 = Blink1(gamma=gamma, white_point=white_point) 454 | yield b1 455 | if switch_off: 456 | b1.off() 457 | b1.close() 458 | --------------------------------------------------------------------------------