ovos-PHAL-plugin-dotstar 0.0.3a1__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.
- ovos_phal_plugin_dotstar-0.0.3a1/LICENSE +21 -0
- ovos_phal_plugin_dotstar-0.0.3a1/PKG-INFO +153 -0
- ovos_phal_plugin_dotstar-0.0.3a1/README.md +125 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar/__init__.py +251 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar/animations.py +308 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar/leds.py +40 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar.egg-info/PKG-INFO +153 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar.egg-info/SOURCES.txt +14 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar.egg-info/dependency_links.txt +1 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar.egg-info/entry_points.txt +2 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar.egg-info/requires.txt +12 -0
- ovos_phal_plugin_dotstar-0.0.3a1/ovos_PHAL_plugin_dotstar.egg-info/top_level.txt +2 -0
- ovos_phal_plugin_dotstar-0.0.3a1/pyproject.toml +56 -0
- ovos_phal_plugin_dotstar-0.0.3a1/setup.cfg +4 -0
- ovos_phal_plugin_dotstar-0.0.3a1/test/test_unit.py +167 -0
- ovos_phal_plugin_dotstar-0.0.3a1/version.py +9 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 builderjer
|
|
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,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ovos-PHAL-plugin-dotstar
|
|
3
|
+
Version: 0.0.3a1
|
|
4
|
+
Summary: An OVOS PHAL plugin to control DotStar type LEDs
|
|
5
|
+
Author-email: builderjer <builderjer@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/OpenVoiceOS/ovos-PHAL-plugin-dotstar
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: ovos-plugin-manager<3.0.0,>=2.1.0
|
|
19
|
+
Requires-Dist: ovos-lingua-franca>=0.4.8a8
|
|
20
|
+
Requires-Dist: ovos-i2c-detection>=0.0.5
|
|
21
|
+
Requires-Dist: adafruit-circuitpython-dotstar; platform_machine == "aarch64" or platform_machine == "armv7l" or platform_machine == "armv6l"
|
|
22
|
+
Requires-Dist: gpiozero; platform_machine == "aarch64" or platform_machine == "armv7l" or platform_machine == "armv6l"
|
|
23
|
+
Requires-Dist: rpi-lgpio; platform_machine == "aarch64" or platform_machine == "armv7l" or platform_machine == "armv6l"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# ovos-PHAL-plugin-dotstar
|
|
30
|
+
|
|
31
|
+
**Compatible with**
|
|
32
|
+
* Respeaker 2/4/6/8 i2c microphone HAT's
|
|
33
|
+
* Adafruit 2 mic VoiceBonnet
|
|
34
|
+
|
|
35
|
+
# Usage
|
|
36
|
+
|
|
37
|
+
There are a few ways for this plugin to get enabled.
|
|
38
|
+
|
|
39
|
+
* Enable manually
|
|
40
|
+
|
|
41
|
+
Add this to your `~/.config/mycroft/mycroft.conf`
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"PHAL": {
|
|
46
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
47
|
+
"enabled": true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
With this configuration, no other validation checks are made. It is assuming you have a compatible HAT installed.
|
|
53
|
+
|
|
54
|
+
* Automatically with [ovos-i2csound](https://github.com/OpenVoiceOS/ovos-i2csound)
|
|
55
|
+
|
|
56
|
+
When `ovos-i2csound` is installed and running, it creates a file at `/etc/OpenVoiceOS/i2c_platform` with the HAT name it detected. This plugin then checks that file and if a compatible HAT is detected, the plugin is activated.
|
|
57
|
+
|
|
58
|
+
* Automatically with hardware detection
|
|
59
|
+
|
|
60
|
+
If the above two options don't work, the plugin tries to detect a compatible HAT using `i2c-detect`. If a compatible device address is found, the plugin will activate.
|
|
61
|
+
|
|
62
|
+
From this point, if you are using a ReSpeaker i2c microphone, your LED's should give you prompts of what is going on with your OVOS assistant.
|
|
63
|
+
|
|
64
|
+
### Adafruit 2mic voicebonnet
|
|
65
|
+
|
|
66
|
+
The [Adafruit voicebonnet](https://learn.adafruit.com/adafruit-voice-bonnet/overview) can be used with this plugin, but it requires one more step of manual configuration. While the above detection options will properly enable the plugin, this HAT uses different GPIO pins for it's LED's, therefore cannot be easily distinguished from a ReSpeaker 2mic HAT.
|
|
67
|
+
|
|
68
|
+
Add the following to your `~/.config/mycroft/mycroft.conf` file
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"PHAL": {
|
|
73
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
74
|
+
"dotstar_hat": "ADAFRUIT2MIC"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
And restart OVOS
|
|
81
|
+
|
|
82
|
+
Your LED's on your Adafruit voicebonnet will now show the same prompts as the ReSpeaker devices.
|
|
83
|
+
|
|
84
|
+
## Custom Configuration
|
|
85
|
+
|
|
86
|
+
Colors and animations can be configured in `~/.config/mycroft/mycroft.conf`
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"PHAL": {
|
|
91
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
92
|
+
"main_color": "Mycroft blue",
|
|
93
|
+
"background_color": "OVOS red",
|
|
94
|
+
"listen_animation": "breath",
|
|
95
|
+
"talking_animation": "blink"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`main_color` -> str:
|
|
102
|
+
```
|
|
103
|
+
rgb value -> "32, 195, 63"
|
|
104
|
+
name -> "blue"
|
|
105
|
+
hex value -> "#22A7F0"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`background_color` -> str
|
|
109
|
+
```
|
|
110
|
+
rgb value -> "32, 195, 63"
|
|
111
|
+
name -> "red"
|
|
112
|
+
hex value -> "#FF1A1A"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`listen_animation` -> str:
|
|
116
|
+
```
|
|
117
|
+
one of:
|
|
118
|
+
'breathe'
|
|
119
|
+
'chase'
|
|
120
|
+
'fill'
|
|
121
|
+
'refill'
|
|
122
|
+
'bounce'
|
|
123
|
+
'blink'
|
|
124
|
+
'alternating'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`talking_animation` -> str:
|
|
128
|
+
```
|
|
129
|
+
one of:
|
|
130
|
+
'breathe'
|
|
131
|
+
'chase'
|
|
132
|
+
'fill'
|
|
133
|
+
'refill'
|
|
134
|
+
'bounce'
|
|
135
|
+
'blink'
|
|
136
|
+
'alternating'
|
|
137
|
+
```
|
|
138
|
+
## Conflicts With
|
|
139
|
+
|
|
140
|
+
There are two pre-existing plugins that either need uninstalled with pip, or blacklisted in your `mycroft.conf` file.
|
|
141
|
+
|
|
142
|
+
[ovos-PHAL-plugin-respeaker2mic](https://github.com/OpenVoiceOS/ovos-PHAL-plugin-respeaker-2mic)
|
|
143
|
+
[ovos-PHAL-plugin-respeaker4mic](https://github.com/OpenVoiceOS/ovos-PHAL-plugin-respeaker-4mic)
|
|
144
|
+
|
|
145
|
+
### TODO
|
|
146
|
+
|
|
147
|
+
- [x] Add more animations
|
|
148
|
+
- [x] User configurable colors
|
|
149
|
+
- [ ] Theme support
|
|
150
|
+
- [x] User configurable animations
|
|
151
|
+
- [ ] Add github tests and automation
|
|
152
|
+
|
|
153
|
+
Please enjoy this plugin and don't be afraid to create an [issue](#) if you run into any problems.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# ovos-PHAL-plugin-dotstar
|
|
2
|
+
|
|
3
|
+
**Compatible with**
|
|
4
|
+
* Respeaker 2/4/6/8 i2c microphone HAT's
|
|
5
|
+
* Adafruit 2 mic VoiceBonnet
|
|
6
|
+
|
|
7
|
+
# Usage
|
|
8
|
+
|
|
9
|
+
There are a few ways for this plugin to get enabled.
|
|
10
|
+
|
|
11
|
+
* Enable manually
|
|
12
|
+
|
|
13
|
+
Add this to your `~/.config/mycroft/mycroft.conf`
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"PHAL": {
|
|
18
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
19
|
+
"enabled": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
With this configuration, no other validation checks are made. It is assuming you have a compatible HAT installed.
|
|
25
|
+
|
|
26
|
+
* Automatically with [ovos-i2csound](https://github.com/OpenVoiceOS/ovos-i2csound)
|
|
27
|
+
|
|
28
|
+
When `ovos-i2csound` is installed and running, it creates a file at `/etc/OpenVoiceOS/i2c_platform` with the HAT name it detected. This plugin then checks that file and if a compatible HAT is detected, the plugin is activated.
|
|
29
|
+
|
|
30
|
+
* Automatically with hardware detection
|
|
31
|
+
|
|
32
|
+
If the above two options don't work, the plugin tries to detect a compatible HAT using `i2c-detect`. If a compatible device address is found, the plugin will activate.
|
|
33
|
+
|
|
34
|
+
From this point, if you are using a ReSpeaker i2c microphone, your LED's should give you prompts of what is going on with your OVOS assistant.
|
|
35
|
+
|
|
36
|
+
### Adafruit 2mic voicebonnet
|
|
37
|
+
|
|
38
|
+
The [Adafruit voicebonnet](https://learn.adafruit.com/adafruit-voice-bonnet/overview) can be used with this plugin, but it requires one more step of manual configuration. While the above detection options will properly enable the plugin, this HAT uses different GPIO pins for it's LED's, therefore cannot be easily distinguished from a ReSpeaker 2mic HAT.
|
|
39
|
+
|
|
40
|
+
Add the following to your `~/.config/mycroft/mycroft.conf` file
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"PHAL": {
|
|
45
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
46
|
+
"dotstar_hat": "ADAFRUIT2MIC"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
And restart OVOS
|
|
53
|
+
|
|
54
|
+
Your LED's on your Adafruit voicebonnet will now show the same prompts as the ReSpeaker devices.
|
|
55
|
+
|
|
56
|
+
## Custom Configuration
|
|
57
|
+
|
|
58
|
+
Colors and animations can be configured in `~/.config/mycroft/mycroft.conf`
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"PHAL": {
|
|
63
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
64
|
+
"main_color": "Mycroft blue",
|
|
65
|
+
"background_color": "OVOS red",
|
|
66
|
+
"listen_animation": "breath",
|
|
67
|
+
"talking_animation": "blink"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`main_color` -> str:
|
|
74
|
+
```
|
|
75
|
+
rgb value -> "32, 195, 63"
|
|
76
|
+
name -> "blue"
|
|
77
|
+
hex value -> "#22A7F0"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`background_color` -> str
|
|
81
|
+
```
|
|
82
|
+
rgb value -> "32, 195, 63"
|
|
83
|
+
name -> "red"
|
|
84
|
+
hex value -> "#FF1A1A"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`listen_animation` -> str:
|
|
88
|
+
```
|
|
89
|
+
one of:
|
|
90
|
+
'breathe'
|
|
91
|
+
'chase'
|
|
92
|
+
'fill'
|
|
93
|
+
'refill'
|
|
94
|
+
'bounce'
|
|
95
|
+
'blink'
|
|
96
|
+
'alternating'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`talking_animation` -> str:
|
|
100
|
+
```
|
|
101
|
+
one of:
|
|
102
|
+
'breathe'
|
|
103
|
+
'chase'
|
|
104
|
+
'fill'
|
|
105
|
+
'refill'
|
|
106
|
+
'bounce'
|
|
107
|
+
'blink'
|
|
108
|
+
'alternating'
|
|
109
|
+
```
|
|
110
|
+
## Conflicts With
|
|
111
|
+
|
|
112
|
+
There are two pre-existing plugins that either need uninstalled with pip, or blacklisted in your `mycroft.conf` file.
|
|
113
|
+
|
|
114
|
+
[ovos-PHAL-plugin-respeaker2mic](https://github.com/OpenVoiceOS/ovos-PHAL-plugin-respeaker-2mic)
|
|
115
|
+
[ovos-PHAL-plugin-respeaker4mic](https://github.com/OpenVoiceOS/ovos-PHAL-plugin-respeaker-4mic)
|
|
116
|
+
|
|
117
|
+
### TODO
|
|
118
|
+
|
|
119
|
+
- [x] Add more animations
|
|
120
|
+
- [x] User configurable colors
|
|
121
|
+
- [ ] Theme support
|
|
122
|
+
- [x] User configurable animations
|
|
123
|
+
- [ ] Add github tests and automation
|
|
124
|
+
|
|
125
|
+
Please enjoy this plugin and don't be afraid to create an [issue](#) if you run into any problems.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from threading import Event
|
|
2
|
+
|
|
3
|
+
from os.path import exists, expanduser, join
|
|
4
|
+
from time import sleep
|
|
5
|
+
|
|
6
|
+
from ovos_bus_client.message import Message
|
|
7
|
+
from ovos_plugin_manager.phal import PHALPlugin
|
|
8
|
+
from ovos_plugin_manager.templates.phal import PHALValidator
|
|
9
|
+
from ovos_utils.log import LOG
|
|
10
|
+
from ovos_config.config import Configuration
|
|
11
|
+
|
|
12
|
+
from ovos_i2c_detection import is_wm8960, is_respeaker_4mic, is_respeaker_6mic, is_mark_1
|
|
13
|
+
|
|
14
|
+
from lingua_franca.util.colors import Color
|
|
15
|
+
from lingua_franca.internal import load_language
|
|
16
|
+
|
|
17
|
+
from ovos_PHAL_plugin_dotstar.leds import DotStarLed
|
|
18
|
+
from ovos_PHAL_plugin_dotstar.animations import animations
|
|
19
|
+
|
|
20
|
+
# File defined in ovos-i2csound
|
|
21
|
+
# https://github.com/OpenVoiceOS/ovos-i2csound/blob/dev/ovos-i2csound#L76
|
|
22
|
+
I2C_PLATFORM_FILE = "/etc/OpenVoiceOS/i2c_platform"
|
|
23
|
+
|
|
24
|
+
# Wiring of the supported HATs (clock pin, data pin, number of LEDs). The actual
|
|
25
|
+
# adafruit_dotstar.DotStar objects are built lazily by get_predefined_hat() so
|
|
26
|
+
# the module stays importable on hosts without SPI/LED hardware (e.g. CI).
|
|
27
|
+
PREDEFINED_HAT_PINS = {
|
|
28
|
+
"WM8960": ("D11", "D10", 3),
|
|
29
|
+
"RESPEAKER4": ("D11", "D10", 12),
|
|
30
|
+
"RESPEAKER6": ("D11", "D10", 12),
|
|
31
|
+
"ADAFRUIT2MIC": ("D6", "D5", 3),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_predefined_hat(name):
|
|
36
|
+
"""
|
|
37
|
+
Instantiate the adafruit_dotstar.DotStar strip for a known HAT.
|
|
38
|
+
|
|
39
|
+
Hardware libraries (adafruit_dotstar, board) are imported here rather than at
|
|
40
|
+
module load so the plugin can be imported and unit-tested without LED hardware.
|
|
41
|
+
|
|
42
|
+
Parameters:
|
|
43
|
+
name (str): One of the keys in PREDEFINED_HAT_PINS.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
adafruit_dotstar.DotStar: configured LED strip.
|
|
47
|
+
"""
|
|
48
|
+
clock, data, num_led = PREDEFINED_HAT_PINS[name]
|
|
49
|
+
from adafruit_dotstar import DotStar
|
|
50
|
+
import board
|
|
51
|
+
return DotStar(getattr(board, clock), getattr(board, data), num_led,
|
|
52
|
+
brightness=0.2)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_i2c_platform():
|
|
56
|
+
if exists(I2C_PLATFORM_FILE):
|
|
57
|
+
with open(I2C_PLATFORM_FILE, "r") as f:
|
|
58
|
+
platform = f.readline().strip()
|
|
59
|
+
LOG.debug(f"platform in check_i2c_platform: {platform}")
|
|
60
|
+
if platform in PREDEFINED_HAT_PINS:
|
|
61
|
+
LOG.debug(f"detected {platform} in i2c_platform")
|
|
62
|
+
return platform
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DotStarLedControlPluginValidator(PHALValidator):
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def validate(config=None):
|
|
70
|
+
# If the user enabled the plugin no need to go further
|
|
71
|
+
"""
|
|
72
|
+
Validate the configuration for the DotStar LED control plugin.
|
|
73
|
+
|
|
74
|
+
Determines whether the plugin should be activated based on configuration and hardware detection.
|
|
75
|
+
|
|
76
|
+
Parameters:
|
|
77
|
+
config (dict, optional): Plugin configuration dictionary. Defaults to None.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
bool: True if the plugin should be enabled, False otherwise.
|
|
81
|
+
|
|
82
|
+
Conditions for validation:
|
|
83
|
+
1. If plugin is explicitly enabled in configuration
|
|
84
|
+
2. If specific audio hardware is detected (WM8960, ReSpeaker 4-mic, ReSpeaker 6-mic)
|
|
85
|
+
3. Excludes Mark 1 hardware configuration
|
|
86
|
+
|
|
87
|
+
Logs debug information about validation process.
|
|
88
|
+
"""
|
|
89
|
+
if config.get("enabled"):
|
|
90
|
+
LOG.debug("user enabled")
|
|
91
|
+
return True
|
|
92
|
+
# Try a direct hardware check
|
|
93
|
+
if is_wm8960() or is_respeaker_4mic() or is_respeaker_6mic():
|
|
94
|
+
if is_mark_1():
|
|
95
|
+
LOG.debug("Mark 1 detected. Dotstar is not needed")
|
|
96
|
+
return False
|
|
97
|
+
LOG.debug("direct hardware check")
|
|
98
|
+
return True
|
|
99
|
+
LOG.debug("no validation")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DotStarLedControlPlugin(PHALPlugin):
|
|
104
|
+
validator = DotStarLedControlPluginValidator
|
|
105
|
+
|
|
106
|
+
lang = Configuration().get("lang", "en")
|
|
107
|
+
try:
|
|
108
|
+
load_language(lang)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
LOG.error(f"Could not load language model {e}")
|
|
111
|
+
|
|
112
|
+
def __init__(self, bus=None, config=None):
|
|
113
|
+
super().__init__(bus=bus, name="ovos-PHAL-plugin-dotstar", config=config)
|
|
114
|
+
self._enable_pin = None
|
|
115
|
+
self.active_animation = None
|
|
116
|
+
self.ds = None
|
|
117
|
+
# Check and see if there is a configuration for a specific board
|
|
118
|
+
if self.config.get("dotstar_hat"):
|
|
119
|
+
ds = self.config.get("dotstar_hat")
|
|
120
|
+
if ds in PREDEFINED_HAT_PINS:
|
|
121
|
+
LOG.debug(f"loading {ds} from config")
|
|
122
|
+
try:
|
|
123
|
+
self.ds = DotStarLed(get_predefined_hat(ds))
|
|
124
|
+
except Exception as e:
|
|
125
|
+
LOG.error(f"Could not load {ds} from config: {e}")
|
|
126
|
+
elif isinstance(self.config.get("dotstar_hat"), dict):
|
|
127
|
+
try:
|
|
128
|
+
from adafruit_dotstar import DotStar
|
|
129
|
+
self.ds = DotStar(
|
|
130
|
+
ds["clock_pin"], ds["led_pin"], ds["num_led"], brightness=ds.get("brightness", 0.2))
|
|
131
|
+
self._enable_pin = ds.get("enable_pin", None)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
LOG.error(f"Could not create led array: {e}")
|
|
134
|
+
else:
|
|
135
|
+
try:
|
|
136
|
+
self.ds = DotStarLed(get_predefined_hat(check_i2c_platform()))
|
|
137
|
+
except KeyError as e:
|
|
138
|
+
LOG.debug(f"check_i2c_platform failed {e}")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
LOG.error(e)
|
|
141
|
+
# No manual configuration and i2csound is not installed or failed
|
|
142
|
+
if not self.ds:
|
|
143
|
+
# Direct hardware checks
|
|
144
|
+
if is_wm8960():
|
|
145
|
+
self.ds = DotStarLed(get_predefined_hat("WM8960"))
|
|
146
|
+
elif is_respeaker_4mic():
|
|
147
|
+
self.ds = DotStarLed(get_predefined_hat("RESPEAKER4"))
|
|
148
|
+
elif is_respeaker_6mic():
|
|
149
|
+
self.ds = DotStarLed(get_predefined_hat("RESPEAKER6"))
|
|
150
|
+
# All else fails, fall back to respeker 4mic
|
|
151
|
+
else:
|
|
152
|
+
self.ds = DotStarLed(get_predefined_hat("RESPEAKER4"))
|
|
153
|
+
|
|
154
|
+
# Required for ReSpeaker 4/6/8 mic
|
|
155
|
+
if not is_wm8960():
|
|
156
|
+
LOG.debug("enable LED's")
|
|
157
|
+
from gpiozero import LED
|
|
158
|
+
from RPi.GPIO import cleanup
|
|
159
|
+
cleanup(5)
|
|
160
|
+
self._enable_pin = LED(5)
|
|
161
|
+
self._enable_pin.on()
|
|
162
|
+
|
|
163
|
+
self.ds.fill(self.main_color.rgb255)
|
|
164
|
+
sleep(1.0)
|
|
165
|
+
self.on_reset()
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def main_color(self):
|
|
169
|
+
color = self.config.get(
|
|
170
|
+
"main_color", Color.from_description("Mycroft blue"))
|
|
171
|
+
if isinstance(color, str):
|
|
172
|
+
try:
|
|
173
|
+
color = eval(color)
|
|
174
|
+
color = Color.from_rgb(color[0], color[1], color[2])
|
|
175
|
+
except Exception as e:
|
|
176
|
+
LOG.debug(f"Exception caught in eval {e}")
|
|
177
|
+
try:
|
|
178
|
+
LOG.debug(color)
|
|
179
|
+
color = Color.from_hex(color)
|
|
180
|
+
LOG.debug(color)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
LOG.debug(f"Exception caught in description {e}")
|
|
183
|
+
try:
|
|
184
|
+
color = Color.from_description(color)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
LOG.warning(f"could not set color to {color}: {e}")
|
|
187
|
+
color = Color.from_description("Mycroft blue")
|
|
188
|
+
return color
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def background_color(self):
|
|
192
|
+
color = self.config.get(
|
|
193
|
+
"background_color", Color.from_description("OVOS red"))
|
|
194
|
+
if isinstance(color, str):
|
|
195
|
+
try:
|
|
196
|
+
color = eval(color)
|
|
197
|
+
color = Color.from_rgb(color[0], color[1], color[2])
|
|
198
|
+
except Exception as e:
|
|
199
|
+
LOG.debug(f"Exception caught in eval {e}")
|
|
200
|
+
try:
|
|
201
|
+
color = Color.from_description(color)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
LOG.debug(f"Exception caught in description {e}")
|
|
204
|
+
try:
|
|
205
|
+
color = Color.from_hex(color)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
LOG.warning(f"could not set color to {color}: {e}")
|
|
208
|
+
color = Color.from_description("OVOS red")
|
|
209
|
+
return color
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def listen_animation(self):
|
|
213
|
+
return self.config.get("listen_animation", "breathe")
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def talking_animation(self):
|
|
217
|
+
return self.config.get("talking_animation", "blink")
|
|
218
|
+
|
|
219
|
+
def on_record_begin(self, message=None):
|
|
220
|
+
self.active_animation = animations[self.listen_animation](
|
|
221
|
+
self.ds, self.main_color)
|
|
222
|
+
self.active_animation.start()
|
|
223
|
+
|
|
224
|
+
def on_record_end(self, message=None):
|
|
225
|
+
self.on_reset()
|
|
226
|
+
|
|
227
|
+
def on_audio_output_start(self, message=None):
|
|
228
|
+
LOG.debug(animations[self.talking_animation])
|
|
229
|
+
self.active_animation = animations[self.talking_animation](
|
|
230
|
+
self.ds, self.main_color, repeat=True)
|
|
231
|
+
LOG.debug(self.active_animation)
|
|
232
|
+
self.active_animation.start()
|
|
233
|
+
|
|
234
|
+
def on_audio_output_end(self, message=None):
|
|
235
|
+
self.on_reset()
|
|
236
|
+
|
|
237
|
+
def on_think(self, message=None):
|
|
238
|
+
self.on_reset()
|
|
239
|
+
|
|
240
|
+
def on_reset(self, message=None):
|
|
241
|
+
if self.active_animation:
|
|
242
|
+
self.active_animation.stop()
|
|
243
|
+
self.active_animation = None
|
|
244
|
+
self.ds.fill(Color.from_description("black").rgb255)
|
|
245
|
+
|
|
246
|
+
def on_system_reset(self, message=None):
|
|
247
|
+
self.on_reset()
|
|
248
|
+
|
|
249
|
+
def shutdown(self):
|
|
250
|
+
self.reset()
|
|
251
|
+
super().shutdown()
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from threading import Event
|
|
3
|
+
|
|
4
|
+
from ovos_utils.log import LOG
|
|
5
|
+
from time import time
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from lingua_franca.util.colors import Color
|
|
9
|
+
from lingua_franca.internal import load_language
|
|
10
|
+
|
|
11
|
+
from ovos_config.config import Configuration
|
|
12
|
+
|
|
13
|
+
from ovos_PHAL_plugin_dotstar.leds import DotStarLed
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LedAnimation:
|
|
17
|
+
def __init__(self, leds: DotStarLed, **kwargs):
|
|
18
|
+
self.leds = leds
|
|
19
|
+
self._delay = Event()
|
|
20
|
+
# Needed for lingua_franca
|
|
21
|
+
self._lang = Configuration().get("lang", "en")
|
|
22
|
+
load_language(self._lang)
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def start(self, timeout: Optional[int] = None, one_shot: bool = False):
|
|
26
|
+
"""
|
|
27
|
+
Start the animation.
|
|
28
|
+
:param timeout: Optional timeout in seconds after which animation stops
|
|
29
|
+
:param one_shot: if True, run animation once and return
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def stop(self):
|
|
34
|
+
"""
|
|
35
|
+
Stop the animation and reset LEDs to black.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BreatheLedAnimation(LedAnimation):
|
|
40
|
+
def __init__(self, leds: DotStarLed, color: Color, **kwargs):
|
|
41
|
+
"""
|
|
42
|
+
Breathing effect where all LEDs dim up and down until timing out
|
|
43
|
+
or being stopped. LEDs are turned off after animation.
|
|
44
|
+
@param leds: LED object to interact with
|
|
45
|
+
@param color: Base color of LEDs
|
|
46
|
+
"""
|
|
47
|
+
LedAnimation.__init__(self, leds)
|
|
48
|
+
self.color = color
|
|
49
|
+
self.step = 0.1
|
|
50
|
+
self.step_delay = 0.1
|
|
51
|
+
self.stopping = Event()
|
|
52
|
+
|
|
53
|
+
def start(self, timeout=None, one_shot=False):
|
|
54
|
+
self.stopping.clear()
|
|
55
|
+
end_time = time() + timeout if timeout else None
|
|
56
|
+
brightness = 0
|
|
57
|
+
step = self.step
|
|
58
|
+
ending = False
|
|
59
|
+
while not self.stopping.is_set():
|
|
60
|
+
if brightness >= 1: # Going Down
|
|
61
|
+
step = -1 * self.step
|
|
62
|
+
elif brightness <= 0:
|
|
63
|
+
step = self.step
|
|
64
|
+
|
|
65
|
+
brightness += step
|
|
66
|
+
brightness = round(brightness, 1)
|
|
67
|
+
self.leds.fill(self.color.rgb255 + (brightness,))
|
|
68
|
+
self._delay.wait(self.step_delay)
|
|
69
|
+
if one_shot and brightness >= 1:
|
|
70
|
+
ending = True
|
|
71
|
+
elif ending and brightness <= 0:
|
|
72
|
+
self.stopping.set()
|
|
73
|
+
elif end_time and time() > end_time:
|
|
74
|
+
self.stopping.set()
|
|
75
|
+
self.leds.fill(Color.from_description("black".rgb255))
|
|
76
|
+
|
|
77
|
+
def stop(self):
|
|
78
|
+
self.stopping.set()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ChaseLedAnimation(LedAnimation):
|
|
82
|
+
def __init__(self, leds: DotStarLed, foreground_color: Color,
|
|
83
|
+
background_color: Optional[Color] = None, **kwargs):
|
|
84
|
+
"""
|
|
85
|
+
Chase effect where all LEDs are lit individually in order until timing
|
|
86
|
+
out or being stopped. LEDs are turned off after animation.
|
|
87
|
+
@param leds: LED object to interact with
|
|
88
|
+
@param foreground_color: Color of active LED
|
|
89
|
+
@param background_color: Color of inactive LEDs
|
|
90
|
+
"""
|
|
91
|
+
LedAnimation.__init__(self, leds)
|
|
92
|
+
self.foreground_color = foreground_color
|
|
93
|
+
self.background_color = background_color or Color.from_description(
|
|
94
|
+
"black")
|
|
95
|
+
self.step = 0.05
|
|
96
|
+
self.step_delay = 0.1
|
|
97
|
+
self.stopping = Event()
|
|
98
|
+
|
|
99
|
+
def start(self, timeout=None, one_shot=False):
|
|
100
|
+
self.stopping.clear()
|
|
101
|
+
end_time = time() + timeout if timeout else None
|
|
102
|
+
|
|
103
|
+
self.leds.fill(self.background_color.rgb255)
|
|
104
|
+
while not self.stopping.is_set():
|
|
105
|
+
for led in range(0, self.leds.num_leds):
|
|
106
|
+
self.leds.set_led(led, self.foreground_color.rgb255)
|
|
107
|
+
self._delay.wait(self.step_delay)
|
|
108
|
+
self.leds.set_led(led, self.background_color.rgb255)
|
|
109
|
+
if one_shot:
|
|
110
|
+
self.stopping.set()
|
|
111
|
+
elif end_time and time() > end_time:
|
|
112
|
+
self.stopping.set()
|
|
113
|
+
self.leds.fill(Color.from_description("black").rgb255)
|
|
114
|
+
|
|
115
|
+
def stop(self):
|
|
116
|
+
self.stopping.set()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FillLedAnimation(LedAnimation):
|
|
120
|
+
def __init__(self, leds: DotStarLed, fill_color: Color,
|
|
121
|
+
reverse: bool = False, **kwargs):
|
|
122
|
+
"""
|
|
123
|
+
Fill effect where LEDs are set to the same color in order. LEDs will
|
|
124
|
+
remain lit after the animation.
|
|
125
|
+
@param leds: LED object to interact with
|
|
126
|
+
@param fill_color: Color to fill LEDs
|
|
127
|
+
@param reverse: If true, fill in reverse order
|
|
128
|
+
"""
|
|
129
|
+
LedAnimation.__init__(self, leds)
|
|
130
|
+
self.fill_color = fill_color
|
|
131
|
+
self.reverse = reverse
|
|
132
|
+
self.step_delay = 0.05
|
|
133
|
+
|
|
134
|
+
def start(self, timeout=None, one_shot=True):
|
|
135
|
+
if not one_shot or timeout is not None:
|
|
136
|
+
LOG.warning("Fill animation does not support persistent animation")
|
|
137
|
+
leds = list(range(0, self.leds.num_leds))
|
|
138
|
+
if self.reverse:
|
|
139
|
+
leds.reverse()
|
|
140
|
+
for led in leds:
|
|
141
|
+
self.leds.set_led(led, self.fill_color.rgb255)
|
|
142
|
+
self._delay.wait(self.step_delay)
|
|
143
|
+
|
|
144
|
+
def stop(self):
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class RefillLedAnimation(LedAnimation):
|
|
149
|
+
def __init__(self, leds: DotStarLed, fill_color: Color,
|
|
150
|
+
reverse: bool = False, **kwargs):
|
|
151
|
+
"""
|
|
152
|
+
Fill effect in the requested color, followed by fill effect in black.
|
|
153
|
+
Animation repeats until timing out or being stopped. LEDs are turned
|
|
154
|
+
off after animation.
|
|
155
|
+
@param leds: LED object to interact with
|
|
156
|
+
@param fill_color: Color to fill LEDs
|
|
157
|
+
@param reverse: If true, fill in reverse order
|
|
158
|
+
"""
|
|
159
|
+
LedAnimation.__init__(self, leds)
|
|
160
|
+
self.stopping = Event()
|
|
161
|
+
self.fill_color = fill_color
|
|
162
|
+
self.fill_animation = FillLedAnimation(leds, fill_color, reverse)
|
|
163
|
+
|
|
164
|
+
def start(self, timeout=None, one_shot=False):
|
|
165
|
+
self.stopping.clear()
|
|
166
|
+
end_time = time() + timeout if timeout else None
|
|
167
|
+
|
|
168
|
+
while not self.stopping.is_set():
|
|
169
|
+
self.fill_animation.start()
|
|
170
|
+
self.fill_animation.fill_color = Color.from_description("black")
|
|
171
|
+
self.fill_animation.start()
|
|
172
|
+
self.fill_animation.fill_color = self.fill_color
|
|
173
|
+
if one_shot:
|
|
174
|
+
self.stopping.set()
|
|
175
|
+
elif end_time and time() > end_time:
|
|
176
|
+
self.stopping.set()
|
|
177
|
+
|
|
178
|
+
def stop(self):
|
|
179
|
+
self.stopping.set()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class BounceLedAnimation(LedAnimation):
|
|
183
|
+
def __init__(self, leds: DotStarLed, fill_color: Color,
|
|
184
|
+
reverse: bool = False, **kwargs):
|
|
185
|
+
"""
|
|
186
|
+
Fill effect in the requested color, followed by reversed fill effect
|
|
187
|
+
in black. Animation repeats until timing out or being stopped.
|
|
188
|
+
LEDs are turned off after animation.
|
|
189
|
+
@param leds: LED object to interact with
|
|
190
|
+
@param fill_color: Color to fill LEDs
|
|
191
|
+
@param reverse: If true, fill in reverse order
|
|
192
|
+
"""
|
|
193
|
+
LOG.debug("bouncing")
|
|
194
|
+
LedAnimation.__init__(self, leds)
|
|
195
|
+
self.stopping = Event()
|
|
196
|
+
self.fill_color = fill_color
|
|
197
|
+
self.fill_animation = FillLedAnimation(leds, fill_color, reverse)
|
|
198
|
+
|
|
199
|
+
def start(self, timeout=None, one_shot=False):
|
|
200
|
+
self.stopping.clear()
|
|
201
|
+
end_time = time() + timeout if timeout else None
|
|
202
|
+
|
|
203
|
+
while not self.stopping.is_set():
|
|
204
|
+
self.fill_animation.start()
|
|
205
|
+
self.fill_animation.reverse = not self.fill_animation.reverse
|
|
206
|
+
self.fill_animation.fill_color = Color.from_description("black")
|
|
207
|
+
self.fill_animation.start()
|
|
208
|
+
self.fill_animation.reverse = not self.fill_animation.reverse
|
|
209
|
+
self.fill_animation.fill_color = self.fill_color
|
|
210
|
+
if one_shot:
|
|
211
|
+
self.stopping.set()
|
|
212
|
+
elif end_time and time() > end_time:
|
|
213
|
+
self.stopping.set()
|
|
214
|
+
|
|
215
|
+
def stop(self):
|
|
216
|
+
self.stopping.set()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class BlinkLedAnimation(LedAnimation):
|
|
220
|
+
def __init__(self, leds: DotStarLed, color: Color,
|
|
221
|
+
num_blinks: int = 2, repeat: bool = False, **kwargs):
|
|
222
|
+
"""
|
|
223
|
+
Blink LEDs in the requested color, for the requested number of blinks.
|
|
224
|
+
If repeating, pause and repeat the effect until timeout or stop event.
|
|
225
|
+
@param leds: LED object to interact with
|
|
226
|
+
@param color: Color to blink LEDs
|
|
227
|
+
@param num_blinks: Number of times to blink LEDs
|
|
228
|
+
@param repeat: If true, repeat animation until timeout or stopped
|
|
229
|
+
"""
|
|
230
|
+
LedAnimation.__init__(self, leds)
|
|
231
|
+
self.stopping = Event()
|
|
232
|
+
self.color = color
|
|
233
|
+
self.num_blinks = num_blinks
|
|
234
|
+
self.repeat = repeat
|
|
235
|
+
|
|
236
|
+
def start(self, timeout=None, one_shot=False):
|
|
237
|
+
self.stopping.clear()
|
|
238
|
+
end_time = time() + timeout if timeout else None
|
|
239
|
+
|
|
240
|
+
self.leds.fill(Color.from_description("black").rgb255)
|
|
241
|
+
self._delay.wait(0.25)
|
|
242
|
+
while not self.stopping.is_set():
|
|
243
|
+
for i in range(self.num_blinks):
|
|
244
|
+
self.leds.fill(self.color.rgb255)
|
|
245
|
+
self._delay.wait(0.1)
|
|
246
|
+
self.leds.fill(Color.from_description("black").rgb255)
|
|
247
|
+
self._delay.wait(0.1)
|
|
248
|
+
if one_shot:
|
|
249
|
+
self.stopping.set()
|
|
250
|
+
elif self.repeat:
|
|
251
|
+
self._delay.wait(.2)
|
|
252
|
+
else:
|
|
253
|
+
self.stopping.set()
|
|
254
|
+
if end_time and time() > end_time:
|
|
255
|
+
self.stopping.set()
|
|
256
|
+
|
|
257
|
+
def stop(self):
|
|
258
|
+
self.stopping.set()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class AlternatingLedAnimation(LedAnimation):
|
|
262
|
+
def __init__(self, leds: DotStarLed, color: Color, **kwargs):
|
|
263
|
+
"""
|
|
264
|
+
Show alternating even/odd LEDs
|
|
265
|
+
@param leds: LED object to interact with
|
|
266
|
+
@param color: Color to blink LEDs
|
|
267
|
+
"""
|
|
268
|
+
LedAnimation.__init__(self, leds)
|
|
269
|
+
self.stopping = Event()
|
|
270
|
+
self.color = color
|
|
271
|
+
self.delay = 0.5
|
|
272
|
+
|
|
273
|
+
def start(self, timeout: Optional[int] = None, one_shot: bool = False):
|
|
274
|
+
evens = True
|
|
275
|
+
self.leds.fill(Color.from_description("black").rgb255)
|
|
276
|
+
self.stopping.clear()
|
|
277
|
+
end_time = time() + timeout if timeout else None
|
|
278
|
+
while not self.stopping.is_set():
|
|
279
|
+
for led in range(self.leds.num_leds):
|
|
280
|
+
if evens and led % 2 == 0:
|
|
281
|
+
self.leds.set_led(led, self.color.rgb255, False)
|
|
282
|
+
elif not evens and led % 2 == 1:
|
|
283
|
+
self.leds.set_led(led, self.color.rgb255, False)
|
|
284
|
+
else:
|
|
285
|
+
self.leds.set_led(
|
|
286
|
+
led, Color.from_description("black").rgb255, False)
|
|
287
|
+
# self.leds.show()
|
|
288
|
+
self._delay.wait(self.delay)
|
|
289
|
+
evens = not evens
|
|
290
|
+
if one_shot and evens: # We did one animation
|
|
291
|
+
self.stopping.set()
|
|
292
|
+
elif end_time and time() > end_time:
|
|
293
|
+
self.stopping.set()
|
|
294
|
+
self.leds.fill(Color.from_description("black").rgb255)
|
|
295
|
+
|
|
296
|
+
def stop(self):
|
|
297
|
+
self.stopping.set()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
animations = {
|
|
301
|
+
'breathe': BreatheLedAnimation,
|
|
302
|
+
'chase': ChaseLedAnimation,
|
|
303
|
+
'fill': FillLedAnimation,
|
|
304
|
+
'refill': RefillLedAnimation,
|
|
305
|
+
'bounce': BounceLedAnimation,
|
|
306
|
+
'blink': BlinkLedAnimation,
|
|
307
|
+
'alternating': AlternatingLedAnimation
|
|
308
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
|
|
3
|
+
from ovos_plugin_manager.hardware.led import AbstractLed
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DotStarLed(AbstractLed):
|
|
7
|
+
def __init__(self, dotstar_led_strip):
|
|
8
|
+
# dotstar_led_strip is an adafruit_dotstar.DotStar instance; the type is
|
|
9
|
+
# not imported here so the module is importable without LED hardware.
|
|
10
|
+
self.dotstar = dotstar_led_strip
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def num_leds(self):
|
|
14
|
+
return self.dotstar.n
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def capabilities(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def set_led(self, led_idx: int, color: tuple, immediate: bool = True):
|
|
21
|
+
self.dotstar[led_idx] = color
|
|
22
|
+
|
|
23
|
+
def fill(self, color: tuple):
|
|
24
|
+
self.dotstar.fill(color)
|
|
25
|
+
|
|
26
|
+
def show(self):
|
|
27
|
+
self.dotstar.show()
|
|
28
|
+
|
|
29
|
+
def shutdown(self):
|
|
30
|
+
# TODO: something
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def scale_brightness(color_val: int, bright_val: float) -> float:
|
|
34
|
+
"""
|
|
35
|
+
Scale an individual color value by a specified brightness.
|
|
36
|
+
:param color_val: 0-255 R, G, or B value
|
|
37
|
+
:param bright_val: 0.0-1.0 brightness scalar value
|
|
38
|
+
:returns: Float modified color value to account for brightness
|
|
39
|
+
"""
|
|
40
|
+
return min(255.0, round(color_val * bright_val))
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ovos-PHAL-plugin-dotstar
|
|
3
|
+
Version: 0.0.3a1
|
|
4
|
+
Summary: An OVOS PHAL plugin to control DotStar type LEDs
|
|
5
|
+
Author-email: builderjer <builderjer@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/OpenVoiceOS/ovos-PHAL-plugin-dotstar
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: ovos-plugin-manager<3.0.0,>=2.1.0
|
|
19
|
+
Requires-Dist: ovos-lingua-franca>=0.4.8a8
|
|
20
|
+
Requires-Dist: ovos-i2c-detection>=0.0.5
|
|
21
|
+
Requires-Dist: adafruit-circuitpython-dotstar; platform_machine == "aarch64" or platform_machine == "armv7l" or platform_machine == "armv6l"
|
|
22
|
+
Requires-Dist: gpiozero; platform_machine == "aarch64" or platform_machine == "armv7l" or platform_machine == "armv6l"
|
|
23
|
+
Requires-Dist: rpi-lgpio; platform_machine == "aarch64" or platform_machine == "armv7l" or platform_machine == "armv6l"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# ovos-PHAL-plugin-dotstar
|
|
30
|
+
|
|
31
|
+
**Compatible with**
|
|
32
|
+
* Respeaker 2/4/6/8 i2c microphone HAT's
|
|
33
|
+
* Adafruit 2 mic VoiceBonnet
|
|
34
|
+
|
|
35
|
+
# Usage
|
|
36
|
+
|
|
37
|
+
There are a few ways for this plugin to get enabled.
|
|
38
|
+
|
|
39
|
+
* Enable manually
|
|
40
|
+
|
|
41
|
+
Add this to your `~/.config/mycroft/mycroft.conf`
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"PHAL": {
|
|
46
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
47
|
+
"enabled": true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
With this configuration, no other validation checks are made. It is assuming you have a compatible HAT installed.
|
|
53
|
+
|
|
54
|
+
* Automatically with [ovos-i2csound](https://github.com/OpenVoiceOS/ovos-i2csound)
|
|
55
|
+
|
|
56
|
+
When `ovos-i2csound` is installed and running, it creates a file at `/etc/OpenVoiceOS/i2c_platform` with the HAT name it detected. This plugin then checks that file and if a compatible HAT is detected, the plugin is activated.
|
|
57
|
+
|
|
58
|
+
* Automatically with hardware detection
|
|
59
|
+
|
|
60
|
+
If the above two options don't work, the plugin tries to detect a compatible HAT using `i2c-detect`. If a compatible device address is found, the plugin will activate.
|
|
61
|
+
|
|
62
|
+
From this point, if you are using a ReSpeaker i2c microphone, your LED's should give you prompts of what is going on with your OVOS assistant.
|
|
63
|
+
|
|
64
|
+
### Adafruit 2mic voicebonnet
|
|
65
|
+
|
|
66
|
+
The [Adafruit voicebonnet](https://learn.adafruit.com/adafruit-voice-bonnet/overview) can be used with this plugin, but it requires one more step of manual configuration. While the above detection options will properly enable the plugin, this HAT uses different GPIO pins for it's LED's, therefore cannot be easily distinguished from a ReSpeaker 2mic HAT.
|
|
67
|
+
|
|
68
|
+
Add the following to your `~/.config/mycroft/mycroft.conf` file
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"PHAL": {
|
|
73
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
74
|
+
"dotstar_hat": "ADAFRUIT2MIC"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
And restart OVOS
|
|
81
|
+
|
|
82
|
+
Your LED's on your Adafruit voicebonnet will now show the same prompts as the ReSpeaker devices.
|
|
83
|
+
|
|
84
|
+
## Custom Configuration
|
|
85
|
+
|
|
86
|
+
Colors and animations can be configured in `~/.config/mycroft/mycroft.conf`
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"PHAL": {
|
|
91
|
+
"ovos-PHAL-plugin-dotstar": {
|
|
92
|
+
"main_color": "Mycroft blue",
|
|
93
|
+
"background_color": "OVOS red",
|
|
94
|
+
"listen_animation": "breath",
|
|
95
|
+
"talking_animation": "blink"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`main_color` -> str:
|
|
102
|
+
```
|
|
103
|
+
rgb value -> "32, 195, 63"
|
|
104
|
+
name -> "blue"
|
|
105
|
+
hex value -> "#22A7F0"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`background_color` -> str
|
|
109
|
+
```
|
|
110
|
+
rgb value -> "32, 195, 63"
|
|
111
|
+
name -> "red"
|
|
112
|
+
hex value -> "#FF1A1A"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`listen_animation` -> str:
|
|
116
|
+
```
|
|
117
|
+
one of:
|
|
118
|
+
'breathe'
|
|
119
|
+
'chase'
|
|
120
|
+
'fill'
|
|
121
|
+
'refill'
|
|
122
|
+
'bounce'
|
|
123
|
+
'blink'
|
|
124
|
+
'alternating'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`talking_animation` -> str:
|
|
128
|
+
```
|
|
129
|
+
one of:
|
|
130
|
+
'breathe'
|
|
131
|
+
'chase'
|
|
132
|
+
'fill'
|
|
133
|
+
'refill'
|
|
134
|
+
'bounce'
|
|
135
|
+
'blink'
|
|
136
|
+
'alternating'
|
|
137
|
+
```
|
|
138
|
+
## Conflicts With
|
|
139
|
+
|
|
140
|
+
There are two pre-existing plugins that either need uninstalled with pip, or blacklisted in your `mycroft.conf` file.
|
|
141
|
+
|
|
142
|
+
[ovos-PHAL-plugin-respeaker2mic](https://github.com/OpenVoiceOS/ovos-PHAL-plugin-respeaker-2mic)
|
|
143
|
+
[ovos-PHAL-plugin-respeaker4mic](https://github.com/OpenVoiceOS/ovos-PHAL-plugin-respeaker-4mic)
|
|
144
|
+
|
|
145
|
+
### TODO
|
|
146
|
+
|
|
147
|
+
- [x] Add more animations
|
|
148
|
+
- [x] User configurable colors
|
|
149
|
+
- [ ] Theme support
|
|
150
|
+
- [x] User configurable animations
|
|
151
|
+
- [ ] Add github tests and automation
|
|
152
|
+
|
|
153
|
+
Please enjoy this plugin and don't be afraid to create an [issue](#) if you run into any problems.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
version.py
|
|
5
|
+
ovos_PHAL_plugin_dotstar/__init__.py
|
|
6
|
+
ovos_PHAL_plugin_dotstar/animations.py
|
|
7
|
+
ovos_PHAL_plugin_dotstar/leds.py
|
|
8
|
+
ovos_PHAL_plugin_dotstar.egg-info/PKG-INFO
|
|
9
|
+
ovos_PHAL_plugin_dotstar.egg-info/SOURCES.txt
|
|
10
|
+
ovos_PHAL_plugin_dotstar.egg-info/dependency_links.txt
|
|
11
|
+
ovos_PHAL_plugin_dotstar.egg-info/entry_points.txt
|
|
12
|
+
ovos_PHAL_plugin_dotstar.egg-info/requires.txt
|
|
13
|
+
ovos_PHAL_plugin_dotstar.egg-info/top_level.txt
|
|
14
|
+
test/test_unit.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
ovos-plugin-manager<3.0.0,>=2.1.0
|
|
2
|
+
ovos-lingua-franca>=0.4.8a8
|
|
3
|
+
ovos-i2c-detection>=0.0.5
|
|
4
|
+
|
|
5
|
+
[:platform_machine == "aarch64" or platform_machine == "armv7l" or platform_machine == "armv6l"]
|
|
6
|
+
adafruit-circuitpython-dotstar
|
|
7
|
+
gpiozero
|
|
8
|
+
rpi-lgpio
|
|
9
|
+
|
|
10
|
+
[test]
|
|
11
|
+
pytest
|
|
12
|
+
pytest-cov
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ovos-PHAL-plugin-dotstar"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "An OVOS PHAL plugin to control DotStar type LEDs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "builderjer", email = "builderjer@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Topic :: Text Processing :: Linguistic",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11"
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"ovos-plugin-manager>=2.1.0,<3.0.0",
|
|
26
|
+
"ovos-lingua-franca>=0.4.8a8",
|
|
27
|
+
"ovos-i2c-detection>=0.0.5",
|
|
28
|
+
# Raspberry Pi LED/GPIO stack. These only build/run on the Pi and pull C
|
|
29
|
+
# extensions (lgpio) that cannot compile on generic CI runners, so they are
|
|
30
|
+
# gated to ARM Linux. The plugin imports them lazily at LED init time.
|
|
31
|
+
"adafruit-circuitpython-dotstar ; platform_machine == 'aarch64' or platform_machine == 'armv7l' or platform_machine == 'armv6l'",
|
|
32
|
+
"gpiozero ; platform_machine == 'aarch64' or platform_machine == 'armv7l' or platform_machine == 'armv6l'",
|
|
33
|
+
"rpi-lgpio ; platform_machine == 'aarch64' or platform_machine == 'armv7l' or platform_machine == 'armv6l'",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
test = ["pytest", "pytest-cov"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/OpenVoiceOS/ovos-PHAL-plugin-dotstar"
|
|
41
|
+
|
|
42
|
+
[project.entry-points."opm.phal"]
|
|
43
|
+
"ovos-PHAL-plugin-dotstar" = "ovos_PHAL_plugin_dotstar:DotStarLedControlPlugin"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.dynamic]
|
|
46
|
+
version = {attr = "version.__version__"}
|
|
47
|
+
|
|
48
|
+
[tool.setuptools]
|
|
49
|
+
py-modules = ["version"]
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["."]
|
|
53
|
+
include = ["ovos_PHAL_plugin_dotstar*"]
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.package-data]
|
|
56
|
+
ovos_PHAL_plugin_dotstar = ["*.json"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Unit tests for ovos-PHAL-plugin-dotstar.
|
|
2
|
+
|
|
3
|
+
These exercise the pure-python logic of the plugin (HAT pin tables, i2c platform
|
|
4
|
+
detection, the DotStarLed wrapper and the LED animations) without requiring any
|
|
5
|
+
real LED/GPIO hardware. The Raspberry Pi hardware libraries (adafruit_dotstar,
|
|
6
|
+
board, gpiozero, RPi.GPIO) are imported lazily inside the plugin, so the package
|
|
7
|
+
imports cleanly here and the LED strip is replaced by a simple in-memory fake.
|
|
8
|
+
"""
|
|
9
|
+
import unittest
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
import ovos_PHAL_plugin_dotstar as dotstar
|
|
13
|
+
from ovos_PHAL_plugin_dotstar import (
|
|
14
|
+
PREDEFINED_HAT_PINS,
|
|
15
|
+
check_i2c_platform,
|
|
16
|
+
get_predefined_hat,
|
|
17
|
+
)
|
|
18
|
+
from ovos_PHAL_plugin_dotstar.leds import DotStarLed
|
|
19
|
+
from ovos_PHAL_plugin_dotstar.animations import (
|
|
20
|
+
animations,
|
|
21
|
+
BlinkLedAnimation,
|
|
22
|
+
FillLedAnimation,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FakeStrip:
|
|
27
|
+
"""Minimal stand-in for an adafruit_dotstar.DotStar strip."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, n=4):
|
|
30
|
+
self.n = n
|
|
31
|
+
self.pixels = [(0, 0, 0)] * n
|
|
32
|
+
self.filled = None
|
|
33
|
+
self.shown = 0
|
|
34
|
+
|
|
35
|
+
def __setitem__(self, idx, color):
|
|
36
|
+
self.pixels[idx] = color
|
|
37
|
+
|
|
38
|
+
def fill(self, color):
|
|
39
|
+
self.filled = color
|
|
40
|
+
|
|
41
|
+
def show(self):
|
|
42
|
+
self.shown += 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestModuleImport(unittest.TestCase):
|
|
46
|
+
def test_imports_without_hardware(self):
|
|
47
|
+
# Importing the plugin must not require LED/GPIO hardware libraries.
|
|
48
|
+
self.assertTrue(hasattr(dotstar, "DotStarLedControlPlugin"))
|
|
49
|
+
self.assertTrue(hasattr(dotstar, "DotStarLedControlPluginValidator"))
|
|
50
|
+
|
|
51
|
+
def test_predefined_hat_pins_known(self):
|
|
52
|
+
self.assertIn("WM8960", PREDEFINED_HAT_PINS)
|
|
53
|
+
self.assertIn("RESPEAKER4", PREDEFINED_HAT_PINS)
|
|
54
|
+
self.assertIn("RESPEAKER6", PREDEFINED_HAT_PINS)
|
|
55
|
+
self.assertIn("ADAFRUIT2MIC", PREDEFINED_HAT_PINS)
|
|
56
|
+
for clock, data, num in PREDEFINED_HAT_PINS.values():
|
|
57
|
+
self.assertIsInstance(clock, str)
|
|
58
|
+
self.assertIsInstance(data, str)
|
|
59
|
+
self.assertIsInstance(num, int)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestGetPredefinedHat(unittest.TestCase):
|
|
63
|
+
def test_builds_strip_with_board_pins(self):
|
|
64
|
+
captured = {}
|
|
65
|
+
|
|
66
|
+
class FakeBoard:
|
|
67
|
+
D11 = "PIN_D11"
|
|
68
|
+
D10 = "PIN_D10"
|
|
69
|
+
|
|
70
|
+
def fake_dotstar(clock, data, num_led, brightness=0.2):
|
|
71
|
+
captured.update(clock=clock, data=data, num_led=num_led,
|
|
72
|
+
brightness=brightness)
|
|
73
|
+
return FakeStrip(num_led)
|
|
74
|
+
|
|
75
|
+
import sys
|
|
76
|
+
import types
|
|
77
|
+
fake_mod = types.ModuleType("adafruit_dotstar")
|
|
78
|
+
fake_mod.DotStar = fake_dotstar
|
|
79
|
+
with patch.dict(sys.modules, {"adafruit_dotstar": fake_mod,
|
|
80
|
+
"board": FakeBoard()}):
|
|
81
|
+
strip = get_predefined_hat("WM8960")
|
|
82
|
+
self.assertEqual(captured["clock"], "PIN_D11")
|
|
83
|
+
self.assertEqual(captured["data"], "PIN_D10")
|
|
84
|
+
self.assertEqual(captured["num_led"], 3)
|
|
85
|
+
self.assertIsInstance(strip, FakeStrip)
|
|
86
|
+
|
|
87
|
+
def test_unknown_hat_raises(self):
|
|
88
|
+
with self.assertRaises(KeyError):
|
|
89
|
+
get_predefined_hat("NOPE")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestCheckI2cPlatform(unittest.TestCase):
|
|
93
|
+
def test_missing_file_returns_none(self):
|
|
94
|
+
with patch("ovos_PHAL_plugin_dotstar.exists", return_value=False):
|
|
95
|
+
self.assertIsNone(check_i2c_platform())
|
|
96
|
+
|
|
97
|
+
def test_known_platform_returned(self):
|
|
98
|
+
from unittest.mock import mock_open
|
|
99
|
+
m = mock_open(read_data="WM8960\n")
|
|
100
|
+
with patch("ovos_PHAL_plugin_dotstar.exists", return_value=True), \
|
|
101
|
+
patch("builtins.open", m):
|
|
102
|
+
self.assertEqual(check_i2c_platform(), "WM8960")
|
|
103
|
+
|
|
104
|
+
def test_unknown_platform_returns_none(self):
|
|
105
|
+
from unittest.mock import mock_open
|
|
106
|
+
m = mock_open(read_data="SOMETHING_ELSE\n")
|
|
107
|
+
with patch("ovos_PHAL_plugin_dotstar.exists", return_value=True), \
|
|
108
|
+
patch("builtins.open", m):
|
|
109
|
+
self.assertIsNone(check_i2c_platform())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestDotStarLed(unittest.TestCase):
|
|
113
|
+
def setUp(self):
|
|
114
|
+
self.strip = FakeStrip(n=4)
|
|
115
|
+
self.led = DotStarLed(self.strip)
|
|
116
|
+
|
|
117
|
+
def test_num_leds(self):
|
|
118
|
+
self.assertEqual(self.led.num_leds, 4)
|
|
119
|
+
|
|
120
|
+
def test_set_led(self):
|
|
121
|
+
self.led.set_led(2, (1, 2, 3))
|
|
122
|
+
self.assertEqual(self.strip.pixels[2], (1, 2, 3))
|
|
123
|
+
|
|
124
|
+
def test_fill(self):
|
|
125
|
+
self.led.fill((9, 9, 9))
|
|
126
|
+
self.assertEqual(self.strip.filled, (9, 9, 9))
|
|
127
|
+
|
|
128
|
+
def test_show(self):
|
|
129
|
+
self.led.show()
|
|
130
|
+
self.assertEqual(self.strip.shown, 1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestAnimationsRegistry(unittest.TestCase):
|
|
134
|
+
def test_registry_keys(self):
|
|
135
|
+
for key in ("breathe", "chase", "fill", "refill", "bounce", "blink",
|
|
136
|
+
"alternating"):
|
|
137
|
+
self.assertIn(key, animations)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestFillAnimation(unittest.TestCase):
|
|
141
|
+
def test_fill_sets_each_led(self):
|
|
142
|
+
from lingua_franca.util.colors import Color
|
|
143
|
+
strip = FakeStrip(n=3)
|
|
144
|
+
led = DotStarLed(strip)
|
|
145
|
+
anim = FillLedAnimation(led, Color.from_rgb(10, 20, 30))
|
|
146
|
+
# patch the internal delay so the test does not sleep
|
|
147
|
+
anim._delay.set()
|
|
148
|
+
anim.start()
|
|
149
|
+
# all three pixels should have been written
|
|
150
|
+
for px in strip.pixels:
|
|
151
|
+
self.assertEqual(px, (10, 20, 30))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestBlinkAnimationOneShot(unittest.TestCase):
|
|
155
|
+
def test_blink_one_shot_terminates(self):
|
|
156
|
+
from lingua_franca.util.colors import Color
|
|
157
|
+
strip = FakeStrip(n=2)
|
|
158
|
+
led = DotStarLed(strip)
|
|
159
|
+
anim = BlinkLedAnimation(led, Color.from_rgb(255, 0, 0), num_blinks=1)
|
|
160
|
+
anim._delay.set()
|
|
161
|
+
# one_shot must terminate the loop without an explicit stop()
|
|
162
|
+
anim.start(one_shot=True)
|
|
163
|
+
self.assertTrue(anim.stopping.is_set())
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
unittest.main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The following lines are replaced during the release process.
|
|
2
|
+
# START_VERSION_BLOCK
|
|
3
|
+
VERSION_MAJOR = 0
|
|
4
|
+
VERSION_MINOR = 0
|
|
5
|
+
VERSION_BUILD = 3
|
|
6
|
+
VERSION_ALPHA = 1
|
|
7
|
+
# END_VERSION_BLOCK
|
|
8
|
+
|
|
9
|
+
__version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "")
|