blinkenhub 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blinkenhub-0.1.0/LICENSE +21 -0
- blinkenhub-0.1.0/PKG-INFO +84 -0
- blinkenhub-0.1.0/README.md +59 -0
- blinkenhub-0.1.0/blinkenhub.egg-info/PKG-INFO +84 -0
- blinkenhub-0.1.0/blinkenhub.egg-info/SOURCES.txt +10 -0
- blinkenhub-0.1.0/blinkenhub.egg-info/dependency_links.txt +1 -0
- blinkenhub-0.1.0/blinkenhub.egg-info/entry_points.txt +2 -0
- blinkenhub-0.1.0/blinkenhub.egg-info/requires.txt +3 -0
- blinkenhub-0.1.0/blinkenhub.egg-info/top_level.txt +1 -0
- blinkenhub-0.1.0/blinkenhub.py +660 -0
- blinkenhub-0.1.0/pyproject.toml +41 -0
- blinkenhub-0.1.0/setup.cfg +4 -0
blinkenhub-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Systemix Software, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blinkenhub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library and command-line tool to control the Blinkenhub Mac mini stand
|
|
5
|
+
Author-email: Brian Cuthie <brian@systemix.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://blinkenhub.com
|
|
8
|
+
Project-URL: Source, https://github.com/blinkenhub/python
|
|
9
|
+
Project-URL: Issues, https://github.com/blinkenhub/python/issues
|
|
10
|
+
Keywords: blinkenhub,led,mac mini,serial,rgb
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: System :: Hardware
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: click
|
|
22
|
+
Requires-Dist: crc
|
|
23
|
+
Requires-Dist: pyserial
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# blinkenhub
|
|
27
|
+
|
|
28
|
+
Python library and command-line tool to control the [Blinkenhub](https://blinkenhub.com)
|
|
29
|
+
Mac mini stand, a USB-connected ring of 17 RGB LEDs.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install blinkenhub
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This installs the `blinkenhub` module and the `blinkenhub` command.
|
|
38
|
+
|
|
39
|
+
## Command-line usage
|
|
40
|
+
|
|
41
|
+
The device is found automatically. To use a specific serial port, pass
|
|
42
|
+
`--port` or set the `BLINKENHUB_PORT` environment variable.
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
blinkenhub rainbow --value 0.3 --rate 2 # spinning rainbow
|
|
46
|
+
blinkenhub disco # random colors until Ctrl-C
|
|
47
|
+
blinkenhub set-pixels -p 0 1 .5 -d 240 1 .2
|
|
48
|
+
blinkenhub animate-value --oscillate --rate 0.5 --count -1
|
|
49
|
+
blinkenhub stop # stop all animations
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Run `blinkenhub --help`, or `blinkenhub <command> --help`, for every command
|
|
53
|
+
and option.
|
|
54
|
+
|
|
55
|
+
`blinkenhub run` shows random colors while the Mac's displays are awake and a
|
|
56
|
+
red sweep while they sleep. It works on macOS only; the other commands work on
|
|
57
|
+
any platform.
|
|
58
|
+
|
|
59
|
+
## Library usage
|
|
60
|
+
|
|
61
|
+
Colors are HSV: hue in degrees (0–360), saturation and value (brightness)
|
|
62
|
+
from 0 to 1.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from blinkenhub import Blinkenhub, HSV_Color, SetPixels, Rotate, ValueAnimation, Animation
|
|
66
|
+
|
|
67
|
+
bh = Blinkenhub() # or Blinkenhub("/dev/cu.usbmodem1101")
|
|
68
|
+
|
|
69
|
+
# Set all 17 pixels to dim blue, fading over one second
|
|
70
|
+
bh.send(SetPixels([HSV_Color(240, 1, 0.2)] * 17, 1.0))
|
|
71
|
+
|
|
72
|
+
# Pulse the brightness forever
|
|
73
|
+
bh.send(ValueAnimation(Animation.ANIMATE_UPDOWN, rate=0.5, top=0.5, bottom=0.05, count=-1))
|
|
74
|
+
|
|
75
|
+
# Spin the pattern clockwise at 3 pixels per second
|
|
76
|
+
bh.send(Rotate(Rotate.CW, 3.0, -1))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`send()` returns `True` when the device accepts the command, and raises
|
|
80
|
+
`TimeoutException` if it doesn't answer.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# blinkenhub
|
|
2
|
+
|
|
3
|
+
Python library and command-line tool to control the [Blinkenhub](https://blinkenhub.com)
|
|
4
|
+
Mac mini stand, a USB-connected ring of 17 RGB LEDs.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
pip install blinkenhub
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
This installs the `blinkenhub` module and the `blinkenhub` command.
|
|
13
|
+
|
|
14
|
+
## Command-line usage
|
|
15
|
+
|
|
16
|
+
The device is found automatically. To use a specific serial port, pass
|
|
17
|
+
`--port` or set the `BLINKENHUB_PORT` environment variable.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
blinkenhub rainbow --value 0.3 --rate 2 # spinning rainbow
|
|
21
|
+
blinkenhub disco # random colors until Ctrl-C
|
|
22
|
+
blinkenhub set-pixels -p 0 1 .5 -d 240 1 .2
|
|
23
|
+
blinkenhub animate-value --oscillate --rate 0.5 --count -1
|
|
24
|
+
blinkenhub stop # stop all animations
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run `blinkenhub --help`, or `blinkenhub <command> --help`, for every command
|
|
28
|
+
and option.
|
|
29
|
+
|
|
30
|
+
`blinkenhub run` shows random colors while the Mac's displays are awake and a
|
|
31
|
+
red sweep while they sleep. It works on macOS only; the other commands work on
|
|
32
|
+
any platform.
|
|
33
|
+
|
|
34
|
+
## Library usage
|
|
35
|
+
|
|
36
|
+
Colors are HSV: hue in degrees (0–360), saturation and value (brightness)
|
|
37
|
+
from 0 to 1.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from blinkenhub import Blinkenhub, HSV_Color, SetPixels, Rotate, ValueAnimation, Animation
|
|
41
|
+
|
|
42
|
+
bh = Blinkenhub() # or Blinkenhub("/dev/cu.usbmodem1101")
|
|
43
|
+
|
|
44
|
+
# Set all 17 pixels to dim blue, fading over one second
|
|
45
|
+
bh.send(SetPixels([HSV_Color(240, 1, 0.2)] * 17, 1.0))
|
|
46
|
+
|
|
47
|
+
# Pulse the brightness forever
|
|
48
|
+
bh.send(ValueAnimation(Animation.ANIMATE_UPDOWN, rate=0.5, top=0.5, bottom=0.05, count=-1))
|
|
49
|
+
|
|
50
|
+
# Spin the pattern clockwise at 3 pixels per second
|
|
51
|
+
bh.send(Rotate(Rotate.CW, 3.0, -1))
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`send()` returns `True` when the device accepts the command, and raises
|
|
55
|
+
`TimeoutException` if it doesn't answer.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blinkenhub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library and command-line tool to control the Blinkenhub Mac mini stand
|
|
5
|
+
Author-email: Brian Cuthie <brian@systemix.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://blinkenhub.com
|
|
8
|
+
Project-URL: Source, https://github.com/blinkenhub/python
|
|
9
|
+
Project-URL: Issues, https://github.com/blinkenhub/python/issues
|
|
10
|
+
Keywords: blinkenhub,led,mac mini,serial,rgb
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: System :: Hardware
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: click
|
|
22
|
+
Requires-Dist: crc
|
|
23
|
+
Requires-Dist: pyserial
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# blinkenhub
|
|
27
|
+
|
|
28
|
+
Python library and command-line tool to control the [Blinkenhub](https://blinkenhub.com)
|
|
29
|
+
Mac mini stand, a USB-connected ring of 17 RGB LEDs.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install blinkenhub
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This installs the `blinkenhub` module and the `blinkenhub` command.
|
|
38
|
+
|
|
39
|
+
## Command-line usage
|
|
40
|
+
|
|
41
|
+
The device is found automatically. To use a specific serial port, pass
|
|
42
|
+
`--port` or set the `BLINKENHUB_PORT` environment variable.
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
blinkenhub rainbow --value 0.3 --rate 2 # spinning rainbow
|
|
46
|
+
blinkenhub disco # random colors until Ctrl-C
|
|
47
|
+
blinkenhub set-pixels -p 0 1 .5 -d 240 1 .2
|
|
48
|
+
blinkenhub animate-value --oscillate --rate 0.5 --count -1
|
|
49
|
+
blinkenhub stop # stop all animations
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Run `blinkenhub --help`, or `blinkenhub <command> --help`, for every command
|
|
53
|
+
and option.
|
|
54
|
+
|
|
55
|
+
`blinkenhub run` shows random colors while the Mac's displays are awake and a
|
|
56
|
+
red sweep while they sleep. It works on macOS only; the other commands work on
|
|
57
|
+
any platform.
|
|
58
|
+
|
|
59
|
+
## Library usage
|
|
60
|
+
|
|
61
|
+
Colors are HSV: hue in degrees (0–360), saturation and value (brightness)
|
|
62
|
+
from 0 to 1.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from blinkenhub import Blinkenhub, HSV_Color, SetPixels, Rotate, ValueAnimation, Animation
|
|
66
|
+
|
|
67
|
+
bh = Blinkenhub() # or Blinkenhub("/dev/cu.usbmodem1101")
|
|
68
|
+
|
|
69
|
+
# Set all 17 pixels to dim blue, fading over one second
|
|
70
|
+
bh.send(SetPixels([HSV_Color(240, 1, 0.2)] * 17, 1.0))
|
|
71
|
+
|
|
72
|
+
# Pulse the brightness forever
|
|
73
|
+
bh.send(ValueAnimation(Animation.ANIMATE_UPDOWN, rate=0.5, top=0.5, bottom=0.05, count=-1))
|
|
74
|
+
|
|
75
|
+
# Spin the pattern clockwise at 3 pixels per second
|
|
76
|
+
bh.send(Rotate(Rotate.CW, 3.0, -1))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`send()` returns `True` when the device accepts the command, and raises
|
|
80
|
+
`TimeoutException` if it doesn't answer.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
blinkenhub.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
blinkenhub.egg-info/PKG-INFO
|
|
6
|
+
blinkenhub.egg-info/SOURCES.txt
|
|
7
|
+
blinkenhub.egg-info/dependency_links.txt
|
|
8
|
+
blinkenhub.egg-info/entry_points.txt
|
|
9
|
+
blinkenhub.egg-info/requires.txt
|
|
10
|
+
blinkenhub.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blinkenhub
|
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2026 Systemix Software, Inc.
|
|
4
|
+
#
|
|
5
|
+
# See the LICENSE file for the full license text.
|
|
6
|
+
|
|
7
|
+
"""Control library and command-line interface for the Blinkenhub Mac mini stand.
|
|
8
|
+
|
|
9
|
+
Blinkenhub is a USB-attached ring of 17 RGB LEDs. This module talks to it over
|
|
10
|
+
a serial port using a simple framed protocol: each command is serialized to
|
|
11
|
+
little-endian binary and followed by a CRC-8 (CCITT) checksum. ``ESC`` and
|
|
12
|
+
``ETX`` bytes are escaped, and the frame ends with ``ETX``. The device answers
|
|
13
|
+
each frame with a single ``A`` (ACK) or ``N`` (NACK) byte.
|
|
14
|
+
|
|
15
|
+
Colors are expressed in HSV: hue in degrees ``[0.0, 360.0]``, and saturation
|
|
16
|
+
and value (brightness) in ``[0.0, 1.0]``.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
Show a dim rainbow and spin it clockwise::
|
|
20
|
+
|
|
21
|
+
from blinkenhub import Blinkenhub, SetPixels, Rotate, mkrainbow
|
|
22
|
+
|
|
23
|
+
bh = Blinkenhub() # auto-detect the device
|
|
24
|
+
bh.send(SetPixels(mkrainbow(0.25), 0))
|
|
25
|
+
bh.send(Rotate(Rotate.CW, 2.0, -1))
|
|
26
|
+
|
|
27
|
+
The ``blinkenhub`` console script (see :func:`main`) exposes the same
|
|
28
|
+
functionality from the shell; run ``blinkenhub --help`` for details.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import struct
|
|
32
|
+
from crc import Calculator, Crc8
|
|
33
|
+
import serial
|
|
34
|
+
import serial.tools.list_ports
|
|
35
|
+
from random import random
|
|
36
|
+
from time import sleep
|
|
37
|
+
import ctypes
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
#: Serial port used by :func:`stop_animations` and the CLI commands. ``None``
|
|
41
|
+
#: means auto-detect. The CLI sets it from ``--port`` / ``BLINKENHUB_PORT``.
|
|
42
|
+
device = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TimeoutException(Exception):
|
|
46
|
+
"""Raised when the Blinkenhub does not reply to a command before the serial timeout."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HSV_Color:
|
|
51
|
+
"""A single pixel color in the HSV color space.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
h (float): Hue in degrees, ``[0.0, 360.0]``.
|
|
55
|
+
s (float): Saturation, ``[0.0, 1.0]``.
|
|
56
|
+
v (float): Value (brightness), ``[0.0, 1.0]``.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, hue, saturation, value):
|
|
60
|
+
"""Create a color.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
hue (float): Hue in degrees, ``[0.0, 360.0]``.
|
|
64
|
+
saturation (float): Saturation, ``[0.0, 1.0]``.
|
|
65
|
+
value (float): Value (brightness), ``[0.0, 1.0]``.
|
|
66
|
+
"""
|
|
67
|
+
self.h = hue
|
|
68
|
+
self.s = saturation
|
|
69
|
+
self.v = value
|
|
70
|
+
|
|
71
|
+
def __bytes__(self):
|
|
72
|
+
"""Encode the color for transmission to the device.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
bytes: Three little-endian 32-bit floats ``(h, s, v)``, 12 bytes total.
|
|
76
|
+
"""
|
|
77
|
+
# encode in format suitable to transmission over the wire
|
|
78
|
+
return struct.pack("<fff", self.h, self.s, self.v)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Blinkenhub:
|
|
82
|
+
"""Serial connection to a Blinkenhub device.
|
|
83
|
+
|
|
84
|
+
Handles device discovery, framing, and the ACK/NACK handshake. Commands are
|
|
85
|
+
built from :class:`BlinkenhubCmd` subclasses and sent with :meth:`send`.
|
|
86
|
+
|
|
87
|
+
Attributes:
|
|
88
|
+
ESC (int): Escape byte; precedes any ``ESC`` or ``ETX`` byte in the payload.
|
|
89
|
+
ACK (str): Reply sent by the device when it accepts a command.
|
|
90
|
+
NACK (str): Reply sent by the device when it rejects a command.
|
|
91
|
+
ETX (int): End-of-frame byte.
|
|
92
|
+
calculator (crc.Calculator): CRC-8 (CCITT) calculator for frame checksums.
|
|
93
|
+
ser (serial.Serial): The open serial port.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
ESC = 0x1b
|
|
97
|
+
ACK = 'A'
|
|
98
|
+
NACK = 'N'
|
|
99
|
+
ETX = 0x03
|
|
100
|
+
|
|
101
|
+
def __init__(self, port=None):
|
|
102
|
+
"""Open a connection to a Blinkenhub.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
port (str, optional): Serial device path, such as
|
|
106
|
+
``/dev/cu.usbmodem1101``. If omitted, the first USB serial port
|
|
107
|
+
whose manufacturer is ``Systemix`` and product is ``Blinkenhub``
|
|
108
|
+
is used.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
Exception: If ``port`` is omitted and no Blinkenhub is found.
|
|
112
|
+
serial.SerialException: If the serial port cannot be opened.
|
|
113
|
+
"""
|
|
114
|
+
self.calculator = Calculator(Crc8.CCITT)
|
|
115
|
+
if port is None:
|
|
116
|
+
# find blinkenhub
|
|
117
|
+
for p in serial.tools.list_ports.comports():
|
|
118
|
+
if p.manufacturer == 'Systemix' and p.product == 'Blinkenhub':
|
|
119
|
+
port = p.device
|
|
120
|
+
break
|
|
121
|
+
if port is None:
|
|
122
|
+
raise Exception("Blinkenhub not found")
|
|
123
|
+
|
|
124
|
+
self.ser = serial.Serial(port, timeout=.5)
|
|
125
|
+
|
|
126
|
+
def prepare_buf(self, buf):
|
|
127
|
+
"""Frame a raw payload for transmission.
|
|
128
|
+
|
|
129
|
+
Appends a CRC-8 checksum, escapes every ``ESC`` and ``ETX`` byte by
|
|
130
|
+
prefixing it with ``ESC``, and terminates the frame with ``ETX``.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
buf (bytes): The serialized command.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
bytearray: The framed bytes, ready to write to the serial port.
|
|
137
|
+
"""
|
|
138
|
+
# add crc
|
|
139
|
+
raw = bytearray()
|
|
140
|
+
raw += buf
|
|
141
|
+
raw.append(self.calculator.checksum(raw))
|
|
142
|
+
|
|
143
|
+
# escape special chars
|
|
144
|
+
escbuf = bytearray()
|
|
145
|
+
for b in raw:
|
|
146
|
+
if b == self.ESC or b == self.ETX:
|
|
147
|
+
escbuf.append(self.ESC)
|
|
148
|
+
escbuf.append(b)
|
|
149
|
+
|
|
150
|
+
# add the record terminator
|
|
151
|
+
escbuf.append(self.ETX)
|
|
152
|
+
|
|
153
|
+
return escbuf
|
|
154
|
+
|
|
155
|
+
def send(self, item, reply=True):
|
|
156
|
+
"""Send a command to the device.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
item (BlinkenhubCmd): The command to send. Any object that supports
|
|
160
|
+
``bytes()`` is accepted.
|
|
161
|
+
reply (bool): If ``True`` (the default), wait for the device's
|
|
162
|
+
ACK/NACK reply.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
bool: ``True`` if the device acknowledged the command, or if
|
|
166
|
+
``reply`` is ``False``. ``False`` if the device replied with
|
|
167
|
+
anything other than ACK.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
TimeoutException: If ``reply`` is ``True`` and no reply arrives
|
|
171
|
+
within the serial timeout (0.5 s).
|
|
172
|
+
"""
|
|
173
|
+
self.ser.write(self.prepare_buf(item.__bytes__()))
|
|
174
|
+
if reply:
|
|
175
|
+
resp = self.ser.read()
|
|
176
|
+
if len(resp) < 1:
|
|
177
|
+
raise TimeoutException()
|
|
178
|
+
elif resp == b'A':
|
|
179
|
+
return True
|
|
180
|
+
else:
|
|
181
|
+
return False
|
|
182
|
+
else:
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class BlinkenhubCmd:
|
|
187
|
+
"""Base class for all device commands.
|
|
188
|
+
|
|
189
|
+
Each command is sent as a 4-byte header (command ID, protocol version, and
|
|
190
|
+
two padding bytes) followed by a command-specific payload. Subclasses must
|
|
191
|
+
set ``self.buf`` to that payload.
|
|
192
|
+
|
|
193
|
+
Attributes:
|
|
194
|
+
VERSION (int): Protocol version sent in every command header.
|
|
195
|
+
CMD_SET_PIXELS (int): Command ID for :class:`SetPixels`.
|
|
196
|
+
CMD_ANIMATE_HUE (int): Command ID for :class:`HueAnimation`.
|
|
197
|
+
CMD_ANIMATE_SATURATION (int): Command ID for :class:`SaturationAnimation`.
|
|
198
|
+
CMD_ANIMATE_VALUE (int): Command ID for :class:`ValueAnimation`.
|
|
199
|
+
CMD_ROTATE (int): Command ID for :class:`Rotate`.
|
|
200
|
+
CMD_SET_GAMMA (int): Command ID for :class:`SetGamma`.
|
|
201
|
+
cmd (int): This command's ID.
|
|
202
|
+
buf (bytes): This command's payload, set by the subclass.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
VERSION = 1
|
|
206
|
+
|
|
207
|
+
CMD_SET_PIXELS = 0
|
|
208
|
+
CMD_ANIMATE_HUE = 1
|
|
209
|
+
CMD_ANIMATE_SATURATION = 2
|
|
210
|
+
CMD_ANIMATE_VALUE = 3
|
|
211
|
+
CMD_ROTATE = 4
|
|
212
|
+
CMD_SET_GAMMA = 5
|
|
213
|
+
|
|
214
|
+
def __init__(self, cmd):
|
|
215
|
+
"""Initialize the command header.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
cmd (int): One of the ``CMD_*`` command IDs.
|
|
219
|
+
"""
|
|
220
|
+
self.cmd = cmd
|
|
221
|
+
|
|
222
|
+
def __bytes__(self):
|
|
223
|
+
"""Serialize the command.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
bytearray: The 4-byte header followed by the payload in ``self.buf``.
|
|
227
|
+
"""
|
|
228
|
+
b = bytearray()
|
|
229
|
+
b += struct.pack("<BBxx", self.cmd, self.VERSION)
|
|
230
|
+
b += self.buf
|
|
231
|
+
return b
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Animation(BlinkenhubCmd):
|
|
235
|
+
"""Base class for commands that animate one HSV channel of every pixel.
|
|
236
|
+
|
|
237
|
+
Use one of the subclasses: :class:`HueAnimation`,
|
|
238
|
+
:class:`SaturationAnimation`, or :class:`ValueAnimation`.
|
|
239
|
+
|
|
240
|
+
Attributes:
|
|
241
|
+
ANIMATE_NONE (int): No animation.
|
|
242
|
+
ANIMATE_UP (int): Ramp from ``bottom`` to ``top``.
|
|
243
|
+
ANIMATE_DOWN (int): Ramp from ``top`` to ``bottom``.
|
|
244
|
+
ANIMATE_UPDOWN (int): Oscillate between ``bottom`` and ``top``.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
ANIMATE_NONE = 0
|
|
248
|
+
ANIMATE_UP = 1
|
|
249
|
+
ANIMATE_DOWN = 2
|
|
250
|
+
ANIMATE_UPDOWN = 3
|
|
251
|
+
|
|
252
|
+
def __init__(self, cmd, typ, rate, top, bottom, count):
|
|
253
|
+
"""Build an animation command.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
cmd (int): The ``CMD_ANIMATE_*`` command ID.
|
|
257
|
+
typ (int): One of the ``ANIMATE_*`` constants.
|
|
258
|
+
rate (float): Speed in cycles per second.
|
|
259
|
+
top (float): Upper limit of the animated channel.
|
|
260
|
+
bottom (float): Lower limit of the animated channel.
|
|
261
|
+
count (int): Number of cycles to run; ``-1`` runs forever.
|
|
262
|
+
"""
|
|
263
|
+
BlinkenhubCmd.__init__(self, cmd)
|
|
264
|
+
self.buf = struct.pack("<Bxxxfffi", typ, rate, top, bottom, count)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class SetGamma(BlinkenhubCmd):
|
|
268
|
+
"""Set the gamma correction the device applies to pixel brightness."""
|
|
269
|
+
|
|
270
|
+
def __init__(self, gamma):
|
|
271
|
+
"""Build a set-gamma command.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
gamma (float): Gamma exponent; ``1.0`` means no correction.
|
|
275
|
+
"""
|
|
276
|
+
BlinkenhubCmd.__init__(self, BlinkenhubCmd.CMD_SET_GAMMA)
|
|
277
|
+
self.buf = struct.pack("<f", gamma)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class SetPixels(BlinkenhubCmd):
|
|
281
|
+
"""Set the color of each pixel, with an optional fade.
|
|
282
|
+
|
|
283
|
+
Attributes:
|
|
284
|
+
LEN_LIMIT (int): Maximum number of pixels (the ring has 17).
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
LEN_LIMIT = 17
|
|
288
|
+
def __init__(self, colors, fade_time):
|
|
289
|
+
"""Build a set-pixels command.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
colors (list[HSV_Color]): One color per pixel, starting at pixel 0.
|
|
293
|
+
fade_time (float): Seconds to fade from the current colors to the new
|
|
294
|
+
ones; ``0`` changes them at once.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
Exception: If ``colors`` has more than :attr:`LEN_LIMIT` entries.
|
|
298
|
+
"""
|
|
299
|
+
BlinkenhubCmd.__init__(self, BlinkenhubCmd.CMD_SET_PIXELS)
|
|
300
|
+
if len(colors) > self.LEN_LIMIT:
|
|
301
|
+
raise Exception("color vector too long")
|
|
302
|
+
b = bytearray()
|
|
303
|
+
for c in colors:
|
|
304
|
+
b += c.__bytes__()
|
|
305
|
+
fmt = f'<Bxxxf{len(b)}s'
|
|
306
|
+
self.buf = struct.pack(fmt, len(colors), fade_time, b)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class HueAnimation(Animation):
|
|
310
|
+
"""Animate the hue of every pixel."""
|
|
311
|
+
|
|
312
|
+
def __init__(self, typ = Animation.ANIMATE_NONE, rate = .5, top = 1, bottom = .1, count = -1):
|
|
313
|
+
"""Build a hue animation command.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
typ (int): One of the ``Animation.ANIMATE_*`` constants.
|
|
317
|
+
rate (float): Speed in cycles per second.
|
|
318
|
+
top (float): Upper hue limit in degrees, ``[0.0, 360.0]``.
|
|
319
|
+
bottom (float): Lower hue limit in degrees, ``[0.0, 360.0]``.
|
|
320
|
+
count (int): Number of cycles to run; ``-1`` runs forever.
|
|
321
|
+
"""
|
|
322
|
+
Animation.__init__(self, BlinkenhubCmd.CMD_ANIMATE_HUE, typ, rate, top, bottom, count)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class SaturationAnimation(Animation):
|
|
326
|
+
"""Animate the saturation of every pixel."""
|
|
327
|
+
|
|
328
|
+
def __init__(self, typ = Animation.ANIMATE_NONE, rate = .5, top = 1, bottom = .1, count = -1):
|
|
329
|
+
"""Build a saturation animation command.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
typ (int): One of the ``Animation.ANIMATE_*`` constants.
|
|
333
|
+
rate (float): Speed in cycles per second.
|
|
334
|
+
top (float): Upper saturation limit, ``[0.0, 1.0]``.
|
|
335
|
+
bottom (float): Lower saturation limit, ``[0.0, 1.0]``.
|
|
336
|
+
count (int): Number of cycles to run; ``-1`` runs forever.
|
|
337
|
+
"""
|
|
338
|
+
Animation.__init__(self, BlinkenhubCmd.CMD_ANIMATE_SATURATION, typ, rate, top, bottom, count)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class ValueAnimation(Animation):
|
|
342
|
+
"""Animate the value (brightness) of every pixel."""
|
|
343
|
+
|
|
344
|
+
def __init__(self, typ = Animation.ANIMATE_NONE, rate = .5, top = 1, bottom = .1, count = -1):
|
|
345
|
+
"""Build a value (brightness) animation command.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
typ (int): One of the ``Animation.ANIMATE_*`` constants.
|
|
349
|
+
rate (float): Speed in cycles per second.
|
|
350
|
+
top (float): Upper brightness limit, ``[0.0, 1.0]``.
|
|
351
|
+
bottom (float): Lower brightness limit, ``[0.0, 1.0]``.
|
|
352
|
+
count (int): Number of cycles to run; ``-1`` runs forever.
|
|
353
|
+
"""
|
|
354
|
+
Animation.__init__(self, BlinkenhubCmd.CMD_ANIMATE_VALUE, typ, rate, top, bottom, count)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class Rotate(BlinkenhubCmd):
|
|
358
|
+
"""Rotate the current pixel pattern around the ring.
|
|
359
|
+
|
|
360
|
+
Attributes:
|
|
361
|
+
NONE (int): Stop rotating.
|
|
362
|
+
CW (int): Rotate clockwise.
|
|
363
|
+
CCW (int): Rotate counterclockwise.
|
|
364
|
+
CYLON (int): Sweep back and forth over ``limit`` pixels.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
NONE = 0
|
|
368
|
+
CW = 1
|
|
369
|
+
CCW = 2
|
|
370
|
+
CYLON = 3
|
|
371
|
+
|
|
372
|
+
def __init__(self, typ, rate, limit):
|
|
373
|
+
"""Build a rotate command.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
typ (int): One of :attr:`NONE`, :attr:`CW`, :attr:`CCW`, or
|
|
377
|
+
:attr:`CYLON`.
|
|
378
|
+
rate (float): Speed in pixels per second.
|
|
379
|
+
limit (int): In :attr:`CYLON` mode, the number of pixels to sweep
|
|
380
|
+
across. The other modes pass ``-1``.
|
|
381
|
+
"""
|
|
382
|
+
BlinkenhubCmd.__init__(self, BlinkenhubCmd.CMD_ROTATE)
|
|
383
|
+
self.buf = struct.pack("<Bxxxfi", typ, rate, limit)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def mkrainbow(value):
|
|
387
|
+
"""Build a rainbow that spans the full hue circle across the ring.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
value (float): Brightness of every pixel, ``[0.0, 1.0]``.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
list[HSV_Color]: 17 fully saturated colors with evenly spaced hues.
|
|
394
|
+
"""
|
|
395
|
+
colors = []
|
|
396
|
+
h = 0
|
|
397
|
+
for _ in range(17):
|
|
398
|
+
colors += [HSV_Color(h, 1, value)]
|
|
399
|
+
h += 21.17647
|
|
400
|
+
return colors
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def stop_animations():
|
|
404
|
+
"""Stop any rotation and channel animations running on the device.
|
|
405
|
+
|
|
406
|
+
Connects to the port in the module-level ``device`` variable, which the CLI
|
|
407
|
+
sets from ``--port``.
|
|
408
|
+
"""
|
|
409
|
+
bh = Blinkenhub(device)
|
|
410
|
+
bh.send(Rotate(0,0,0))
|
|
411
|
+
bh.send(HueAnimation(Animation.ANIMATE_UP,100,1,0,1))
|
|
412
|
+
bh.send(SaturationAnimation(Animation.ANIMATE_UP,100,1,0,1))
|
|
413
|
+
bh.send(ValueAnimation(Animation.ANIMATE_UP,100,1,0,1))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def mk_random_colors(value):
|
|
417
|
+
"""Build a random pattern in which about half the pixels are lit.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
value (float): Brightness of the lit pixels, ``[0.0, 1.0]``.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
list[HSV_Color]: 17 colors. Each is either off or a fully saturated
|
|
424
|
+
random hue.
|
|
425
|
+
"""
|
|
426
|
+
colors = []
|
|
427
|
+
for _ in range(17):
|
|
428
|
+
if random() > 0.5:
|
|
429
|
+
colors += [HSV_Color(random() * 360, 1, value)]
|
|
430
|
+
else:
|
|
431
|
+
colors +=[HSV_Color(0, 0, 0)]
|
|
432
|
+
return colors
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# Inspired by Claude
|
|
436
|
+
def are_we_awake(cg):
|
|
437
|
+
"""Report whether any active display on this Mac is awake (macOS only).
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
cg (ctypes.CDLL): The loaded CoreGraphics framework, with ``restype``
|
|
441
|
+
set to ``ctypes.c_int`` for ``CGDisplayIsAsleep`` and
|
|
442
|
+
``CGGetActiveDisplayList``.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
bool: ``True`` if at least one active display is awake, otherwise ``False``.
|
|
446
|
+
"""
|
|
447
|
+
# Get list of active display IDs
|
|
448
|
+
max_displays = 16
|
|
449
|
+
display_ids = (ctypes.c_uint32 * max_displays)()
|
|
450
|
+
display_count = ctypes.c_uint32(0)
|
|
451
|
+
cg.CGGetActiveDisplayList(max_displays, display_ids, ctypes.byref(display_count))
|
|
452
|
+
sleeping = True
|
|
453
|
+
for i in range(display_count.value):
|
|
454
|
+
did = display_ids[i]
|
|
455
|
+
if not bool(cg.CGDisplayIsAsleep(did)):
|
|
456
|
+
sleeping = False
|
|
457
|
+
return not sleeping
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
##
|
|
461
|
+
## Commands for exercising Blinkenhub
|
|
462
|
+
##
|
|
463
|
+
|
|
464
|
+
def main():
|
|
465
|
+
"""Entry point for the ``blinkenhub`` command-line tool.
|
|
466
|
+
|
|
467
|
+
Builds the Click command group and runs it. The serial port can be given
|
|
468
|
+
with ``--port`` or the ``BLINKENHUB_PORT`` environment variable; otherwise
|
|
469
|
+
the device is detected automatically.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
import click
|
|
473
|
+
|
|
474
|
+
@click.group()
|
|
475
|
+
@click.option('--port', envvar='BLINKENHUB_PORT', help='Serial port to which Blinkenhub is connected')
|
|
476
|
+
def cli(port):
|
|
477
|
+
"""Control the Blinkenhub Mac mini stand."""
|
|
478
|
+
global device
|
|
479
|
+
device = port
|
|
480
|
+
|
|
481
|
+
@cli.command()
|
|
482
|
+
def run():
|
|
483
|
+
"""Show random colors while the displays are awake, and a red Cylon sweep while they sleep (macOS only)."""
|
|
484
|
+
# Load CoreGraphics
|
|
485
|
+
cg = ctypes.cdll.LoadLibrary(
|
|
486
|
+
"/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Set return type for CGDisplayIsAsleep (returns boolean int)
|
|
490
|
+
cg.CGDisplayIsAsleep.restype = ctypes.c_int
|
|
491
|
+
cg.CGGetActiveDisplayList.restype = ctypes.c_int
|
|
492
|
+
|
|
493
|
+
bh = Blinkenhub(device)
|
|
494
|
+
|
|
495
|
+
while(True):
|
|
496
|
+
if are_we_awake(cg):
|
|
497
|
+
stop_animations()
|
|
498
|
+
while are_we_awake(cg):
|
|
499
|
+
colors = mk_random_colors(.25)
|
|
500
|
+
bh.send(SetPixels(colors, 0))
|
|
501
|
+
sleep(.25)
|
|
502
|
+
else:
|
|
503
|
+
stop_animations()
|
|
504
|
+
colors = [
|
|
505
|
+
HSV_Color(0, 1, .1),
|
|
506
|
+
HSV_Color(0, 1, .5),
|
|
507
|
+
HSV_Color(0, 1, .1),
|
|
508
|
+
HSV_Color(0, 1, 0),
|
|
509
|
+
HSV_Color(0, 1, 0),
|
|
510
|
+
HSV_Color(0, 1, 0),
|
|
511
|
+
HSV_Color(0, 1, 0),
|
|
512
|
+
HSV_Color(0, 1, 0),
|
|
513
|
+
HSV_Color(0, 1, 0),
|
|
514
|
+
HSV_Color(0, 1, 0),
|
|
515
|
+
HSV_Color(0, 1, 0),
|
|
516
|
+
HSV_Color(0, 1, 0),
|
|
517
|
+
HSV_Color(0, 1, 0),
|
|
518
|
+
HSV_Color(0, 1, 0),
|
|
519
|
+
HSV_Color(0, 1, 0),
|
|
520
|
+
HSV_Color(0, 1, 0),
|
|
521
|
+
HSV_Color(0, 1, 0)
|
|
522
|
+
]
|
|
523
|
+
bh.send(SetPixels(colors, 0))
|
|
524
|
+
bh.send(Rotate(Rotate.CYLON, 10, 14))
|
|
525
|
+
while not are_we_awake(cg):
|
|
526
|
+
sleep(.5)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@cli.command()
|
|
530
|
+
def stop():
|
|
531
|
+
"""Stop all rotation and animations."""
|
|
532
|
+
stop_animations()
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@cli.command()
|
|
536
|
+
@click.option('-v', '--value', default=.5, help='The value (brightness)')
|
|
537
|
+
@click.option('-r', '--rate', default=0.0, help='Spin rate')
|
|
538
|
+
def rainbow(value, rate):
|
|
539
|
+
"""Show a rainbow, optionally spinning clockwise."""
|
|
540
|
+
stop_animations()
|
|
541
|
+
bh = Blinkenhub(device)
|
|
542
|
+
bh.send(SetPixels(mkrainbow(value), 0))
|
|
543
|
+
if (rate > 0):
|
|
544
|
+
bh.send(Rotate(Rotate.CW, rate, -1))
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@cli.command()
|
|
548
|
+
@click.option('-p', '--period', default=0.3, help='Time in seconds of each step')
|
|
549
|
+
@click.option('-v', '--value', default=.5, help='The value (brightness)')
|
|
550
|
+
def disco(period, value):
|
|
551
|
+
"""Show a new random pattern every PERIOD seconds until interrupted."""
|
|
552
|
+
stop_animations()
|
|
553
|
+
bh = Blinkenhub(device)
|
|
554
|
+
while(True):
|
|
555
|
+
colors = mk_random_colors(value)
|
|
556
|
+
bh.send(SetPixels(colors, 0))
|
|
557
|
+
sleep(period)
|
|
558
|
+
|
|
559
|
+
@cli.command()
|
|
560
|
+
@click.argument('gamma', default=1.0)
|
|
561
|
+
def set_gamma(gamma):
|
|
562
|
+
"""Set the gamma correction (1.0 = none)."""
|
|
563
|
+
if Blinkenhub(device).send(SetGamma(gamma)):
|
|
564
|
+
click.echo('OK')
|
|
565
|
+
else:
|
|
566
|
+
click.echo('Error')
|
|
567
|
+
|
|
568
|
+
@cli.command()
|
|
569
|
+
@click.option('-p', '--pixel', type=(float, float, float), multiple=True)
|
|
570
|
+
@click.option('-f', '--fade', default=0.0, help='Fade time in seconds)')
|
|
571
|
+
@click.option('-d', '--default', type=(float, float, float), help='HSV values for the unspecified pixels')
|
|
572
|
+
def set_pixels(pixel, fade, default):
|
|
573
|
+
"""Set pixel colors as H S V triples.
|
|
574
|
+
|
|
575
|
+
Each --pixel sets the next pixel, starting at pixel 0. The remaining
|
|
576
|
+
pixels get the --default color, or the last --pixel color if no default
|
|
577
|
+
is given.
|
|
578
|
+
"""
|
|
579
|
+
colors = []
|
|
580
|
+
for p in pixel:
|
|
581
|
+
colors += [HSV_Color(p[0], p[1], p[2])]
|
|
582
|
+
if default is not None:
|
|
583
|
+
d = HSV_Color(default[0], default[1], default[2])
|
|
584
|
+
else:
|
|
585
|
+
pi = len(pixel) - 1
|
|
586
|
+
d = HSV_Color(pixel[pi][0], pixel[pi][1], pixel[pi][2])
|
|
587
|
+
for _ in range(17 - len(pixel)):
|
|
588
|
+
colors += [d]
|
|
589
|
+
if Blinkenhub(device).send(SetPixels(colors, fade)):
|
|
590
|
+
click.echo('OK')
|
|
591
|
+
else:
|
|
592
|
+
click.echo('Error')
|
|
593
|
+
|
|
594
|
+
@cli.command()
|
|
595
|
+
@click.option('-n', '--none', 'typ', flag_value=Animation.ANIMATE_NONE, default=True)
|
|
596
|
+
@click.option('-u', '--up', 'typ', flag_value=Animation.ANIMATE_UP)
|
|
597
|
+
@click.option('-d', '--down', 'typ', flag_value=Animation.ANIMATE_DOWN)
|
|
598
|
+
@click.option('-o', '--oscillate', 'typ', flag_value=Animation.ANIMATE_UPDOWN)
|
|
599
|
+
@click.option('-r', '--rate', default=1.0, help='Rate of change as cycles per second)')
|
|
600
|
+
@click.option('-t', '--top', default=360.0, help='Terminal high value [0.0, 360.0]')
|
|
601
|
+
@click.option('-b', '--bottom', default=0.0, help='Terminal low value [0.0, 360.0]')
|
|
602
|
+
@click.option('-c', '--count', default=1, help='Number of times to perform animation; -1 == forever')
|
|
603
|
+
def animate_hue(typ, rate, top, bottom, count):
|
|
604
|
+
"""Animate the hue of every pixel."""
|
|
605
|
+
if Blinkenhub(device).send(HueAnimation(typ, rate, top, bottom, count)):
|
|
606
|
+
click.echo('OK')
|
|
607
|
+
else:
|
|
608
|
+
click.echo('Error')
|
|
609
|
+
|
|
610
|
+
@cli.command()
|
|
611
|
+
@click.option('-n', '--none', 'typ', flag_value=Animation.ANIMATE_NONE, default=True)
|
|
612
|
+
@click.option('-u', '--up', 'typ', flag_value=Animation.ANIMATE_UP)
|
|
613
|
+
@click.option('-d', '--down', 'typ', flag_value=Animation.ANIMATE_DOWN)
|
|
614
|
+
@click.option('-o', '--oscillate', 'typ', flag_value=Animation.ANIMATE_UPDOWN)
|
|
615
|
+
@click.option('-r', '--rate', default=1.0, help='Rate of change as cycles per second)')
|
|
616
|
+
@click.option('-t', '--top', default=1.0, help='Terminal high value')
|
|
617
|
+
@click.option('-b', '--bottom', default=0.0, help='Terminal low value')
|
|
618
|
+
@click.option('-c', '--count', default=1, help='Number of times to perform animation; -1 == forever')
|
|
619
|
+
def animate_saturation(typ, rate, top, bottom, count):
|
|
620
|
+
"""Animate the saturation of every pixel."""
|
|
621
|
+
if Blinkenhub(device).send(SaturationAnimation(typ, rate, top, bottom, count)):
|
|
622
|
+
click.echo('OK')
|
|
623
|
+
else:
|
|
624
|
+
click.echo('Error')
|
|
625
|
+
|
|
626
|
+
@cli.command()
|
|
627
|
+
@click.option('-n', '--none', 'typ', flag_value=Animation.ANIMATE_NONE, default=True)
|
|
628
|
+
@click.option('-u', '--up', 'typ', flag_value=Animation.ANIMATE_UP)
|
|
629
|
+
@click.option('-d', '--down', 'typ', flag_value=Animation.ANIMATE_DOWN)
|
|
630
|
+
@click.option('-o', '--oscillate', 'typ', flag_value=Animation.ANIMATE_UPDOWN)
|
|
631
|
+
@click.option('-r', '--rate', default=1.0, help='Rate of change as cycles per second)')
|
|
632
|
+
@click.option('-t', '--top', default=1.0, help='Terminal high value')
|
|
633
|
+
@click.option('-b', '--bottom', default=0.0, help='Terminal low value')
|
|
634
|
+
@click.option('-c', '--count', default=1, help='Number of times to perform animation; -1 == forever')
|
|
635
|
+
def animate_value(typ, rate, top, bottom, count):
|
|
636
|
+
"""Animate the value (brightness) of every pixel."""
|
|
637
|
+
if Blinkenhub(device).send(ValueAnimation(typ, rate, top, bottom, count)):
|
|
638
|
+
click.echo('OK')
|
|
639
|
+
else:
|
|
640
|
+
click.echo('Error')
|
|
641
|
+
|
|
642
|
+
@cli.command()
|
|
643
|
+
@click.option('--none', 'typ', flag_value=Rotate.NONE, default=True)
|
|
644
|
+
@click.option('--cw', 'typ', flag_value=Rotate.CW)
|
|
645
|
+
@click.option('--ccw', 'typ', flag_value=Rotate.CCW)
|
|
646
|
+
@click.option('--cylon', 'typ', flag_value=Rotate.CYLON)
|
|
647
|
+
@click.option('-r', '--rate', default=1.0, help='Rate of change as pixels per second)')
|
|
648
|
+
@click.option('-l', '--limit', default=1, help='Number of pixels to rotate in cylon mode')
|
|
649
|
+
def rotate(typ, rate, limit):
|
|
650
|
+
"""Rotate the current pattern around the ring."""
|
|
651
|
+
if Blinkenhub(device).send(Rotate(typ, rate, limit)):
|
|
652
|
+
click.echo('OK')
|
|
653
|
+
else:
|
|
654
|
+
click.echo('Error')
|
|
655
|
+
|
|
656
|
+
cli()
|
|
657
|
+
|
|
658
|
+
if __name__ == '__main__':
|
|
659
|
+
|
|
660
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "blinkenhub"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python library and command-line tool to control the Blinkenhub Mac mini stand"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Brian Cuthie", email = "brian@systemix.com"}
|
|
15
|
+
]
|
|
16
|
+
keywords = ["blinkenhub", "led", "mac mini", "serial", "rgb"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Intended Audience :: End Users/Desktop",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Topic :: System :: Hardware",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"click",
|
|
28
|
+
"crc",
|
|
29
|
+
"pyserial",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://blinkenhub.com"
|
|
34
|
+
Source = "https://github.com/blinkenhub/python"
|
|
35
|
+
Issues = "https://github.com/blinkenhub/python/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
blinkenhub = "blinkenhub:main"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
py-modules = ["blinkenhub"]
|