ovos-PHAL-plugin-mk1 0.1.2__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-mk1-0.1.2/LICENSE +203 -0
- ovos-PHAL-plugin-mk1-0.1.2/PKG-INFO +16 -0
- ovos-PHAL-plugin-mk1-0.1.2/README.md +83 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1/__init__.py +591 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1/arduino.py +157 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1/version.py +6 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1.egg-info/PKG-INFO +16 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1.egg-info/SOURCES.txt +13 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1.egg-info/dependency_links.txt +1 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1.egg-info/entry_points.txt +3 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1.egg-info/requires.txt +4 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1.egg-info/top_level.txt +1 -0
- ovos-PHAL-plugin-mk1-0.1.2/ovos_PHAL_plugin_mk1.egg-info/zip-safe +1 -0
- ovos-PHAL-plugin-mk1-0.1.2/setup.cfg +4 -0
- ovos-PHAL-plugin-mk1-0.1.2/setup.py +66 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2024 Casimiro Ferreira
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
203
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 1.1
|
|
2
|
+
Name: ovos-PHAL-plugin-mk1
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A PHAL plugin for mycroft
|
|
5
|
+
Home-page: https://github.com/OpenVoiceOS/ovos-PHAL-plugin-mk1
|
|
6
|
+
Author: JarbasAi
|
|
7
|
+
Author-email: jarbasai@mailfence.com
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Description: UNKNOWN
|
|
10
|
+
Platform: UNKNOWN
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.5
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# ovos-PHAL-plugin - Mark1
|
|
2
|
+
|
|
3
|
+
handles integration with the Mycroft Mark1 hardware
|
|
4
|
+
|
|
5
|
+
the firmware for mark1 arduino that controls the eyes and faceplate can be found [here](https://github.com/OpenVoiceOS/mycroft-mark1-firmware)
|
|
6
|
+
|
|
7
|
+
utits to interact with the faceplate pixel by pixel can be found in [ovos-mark1-utils](https://github.com/OpenVoiceOS/ovos-mark1-utils)
|
|
8
|
+
|
|
9
|
+
# Serial Port Protocols
|
|
10
|
+
|
|
11
|
+
See the file [protocols.txt](./protocols.txt) for a description of commands that can be sent to the faceplate.
|
|
12
|
+
|
|
13
|
+
Commands can be sent from the command line on a Raspberry Pi, such as this:
|
|
14
|
+
```
|
|
15
|
+
$ echo "eyes.blink" > /dev/ttyAMA0
|
|
16
|
+
$ echo "eyes.color=16711680" > /dev/ttyAMA0
|
|
17
|
+
$ echo "mouth.text=I am angry!" > /dev/ttyAMA0
|
|
18
|
+
```
|
|
19
|
+
Which will blink the eyes, turn them red, then display the phrase on the faceplate.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Graphics
|
|
23
|
+
The `mouth.icon=` command takes a custom format for it's black and white images. You can use the [HTML editor](http://htmlpreview.github.io/?https://raw.githubusercontent.com/MycroftAI/enclosure-mark1/master/editor.html) to create the image strings that can be sent.
|
|
24
|
+
|
|
25
|
+
# IO pins
|
|
26
|
+
When looking at the device from the back, pins are laid out as:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
-----------------------------------------------------------------------------------------------
|
|
30
|
+
RCA Port | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 |
|
|
31
|
+
| 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 | 33 | 35 | 37 | 39 |
|
|
32
|
+
-----------------------------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
HDMI Ethernet USB USB
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Where:
|
|
38
|
+
|
|
39
|
+
| Pin| Description |
|
|
40
|
+
|:--:|:---------------------|
|
|
41
|
+
| 1 | GND |
|
|
42
|
+
| 2 | +12V |
|
|
43
|
+
| 3 | GND |
|
|
44
|
+
| 4 | +5V |
|
|
45
|
+
| 5 | GND |
|
|
46
|
+
| 6 | +3.3V |
|
|
47
|
+
| 7 | Arduino Reset |
|
|
48
|
+
| 8 | Arduino D5 |
|
|
49
|
+
| 9 | Arduino D6 |
|
|
50
|
+
| 10 | Arduino D10 |
|
|
51
|
+
| 11 | Arduino A2 |
|
|
52
|
+
| 12 | Arduino A3 |
|
|
53
|
+
| 13 | +3.3V |
|
|
54
|
+
| 14 | +3.3V |
|
|
55
|
+
| 15 | +3.3V |
|
|
56
|
+
| 16 | GND |
|
|
57
|
+
| 17 | GND |
|
|
58
|
+
| 18 | GND |
|
|
59
|
+
| 19 | +5V |
|
|
60
|
+
| 20 | +5V |
|
|
61
|
+
| 21 | +5V |
|
|
62
|
+
| 22 | +5V |
|
|
63
|
+
| 23 | GND |
|
|
64
|
+
| 24 | GND |
|
|
65
|
+
| 25 | Raspberry Pi ID_SC |
|
|
66
|
+
| 26 | Raspberry Pi ID_SD |
|
|
67
|
+
| 27 | Raspberry Pi GPIO 4 |
|
|
68
|
+
| 28 | Raspberry Pi GPIO 5 |
|
|
69
|
+
| 29 | Raspberry Pi GPIO 6 |
|
|
70
|
+
| 30 | Raspberry Pi GPIO 7 |
|
|
71
|
+
| 31 | Raspberry Pi GPIO 8 |
|
|
72
|
+
| 32 | Raspberry Pi GPIO 9 |
|
|
73
|
+
| 33 | Raspberry Pi GPIO 10 |
|
|
74
|
+
| 34 | Raspberry Pi GPIO 11 |
|
|
75
|
+
| 35 | Raspberry Pi GPIO 12 |
|
|
76
|
+
| 36 | Raspberry Pi GPIO 16 |
|
|
77
|
+
| 37 | Raspberry Pi GPIO 25 |
|
|
78
|
+
| 38 | Raspberry Pi GPIO 26 |
|
|
79
|
+
| 39 | GND |
|
|
80
|
+
| 40 | GND |
|
|
81
|
+
|
|
82
|
+
**WARNING: This is not the same as the standard Raspberry Pi GPIO headers!**
|
|
83
|
+
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from threading import Event
|
|
3
|
+
from time import sleep
|
|
4
|
+
|
|
5
|
+
import serial
|
|
6
|
+
from ovos_bus_client.message import Message
|
|
7
|
+
from ovos_utils import create_daemon
|
|
8
|
+
from ovos_utils.log import LOG
|
|
9
|
+
from ovos_utils.network_utils import is_connected
|
|
10
|
+
|
|
11
|
+
from ovos_mark1.faceplate.icons import MusicIcon, WarningIcon, SnowIcon, StormIcon, SunnyIcon, \
|
|
12
|
+
CloudyIcon, PartlyCloudyIcon, WindIcon, RainIcon, LightRainIcon
|
|
13
|
+
from ovos_PHAL_plugin_mk1.arduino import EnclosureReader, EnclosureWriter
|
|
14
|
+
from ovos_plugin_manager.phal import PHALPlugin
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# The Mark 1 hardware consists of a Raspberry Pi main CPU which is connected
|
|
18
|
+
# to an Arduino over the serial port. A custom serial protocol sends
|
|
19
|
+
# commands to control various visual elements which are controlled by the
|
|
20
|
+
# Arduino (e.g. two circular rings of RGB LEDs; and four 8x8 white LEDs).
|
|
21
|
+
#
|
|
22
|
+
# The Arduino can also send back notifications in response to either
|
|
23
|
+
# pressing or turning a rotary encoder.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MycroftMark1Validator:
|
|
27
|
+
@staticmethod
|
|
28
|
+
def validate(config=None):
|
|
29
|
+
""" this method is called before loading the plugin.
|
|
30
|
+
If it returns False the plugin is not loaded.
|
|
31
|
+
This allows a plugin to run platform checks"""
|
|
32
|
+
# TODO how to detect if running in a mark1 ?
|
|
33
|
+
# detect "/dev/ttyAMA0" ?
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MycroftMark1(PHALPlugin):
|
|
38
|
+
"""
|
|
39
|
+
Serves as a communication interface between Arduino and Mycroft Core.
|
|
40
|
+
|
|
41
|
+
``Enclosure`` initializes and aggregates all enclosures implementation.
|
|
42
|
+
|
|
43
|
+
E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino``
|
|
44
|
+
|
|
45
|
+
It also listens to the basic messages in order to perform those core actions
|
|
46
|
+
on the unit.
|
|
47
|
+
|
|
48
|
+
E.g. Start and Stop talk animation
|
|
49
|
+
"""
|
|
50
|
+
validator = MycroftMark1Validator
|
|
51
|
+
|
|
52
|
+
def __init__(self, bus=None, config=None):
|
|
53
|
+
super().__init__(bus=bus, name="ovos-PHAL-plugin-mk1", config=config)
|
|
54
|
+
self.stopped = Event()
|
|
55
|
+
self.config = config or {
|
|
56
|
+
"port": "/dev/ttyAMA0",
|
|
57
|
+
"rate": 9600,
|
|
58
|
+
"timeout": 5.0
|
|
59
|
+
}
|
|
60
|
+
self.__init_serial()
|
|
61
|
+
self.reader = EnclosureReader(self.serial, self.bus, self.handle_button_press)
|
|
62
|
+
self.writer = EnclosureWriter(self.serial, self.bus)
|
|
63
|
+
|
|
64
|
+
self._num_pixels = 12 * 2
|
|
65
|
+
self._current_rgb = [(255, 255, 255) for i in range(self._num_pixels)]
|
|
66
|
+
self.showing_visemes = False
|
|
67
|
+
self.speaking = False
|
|
68
|
+
self.listening = False
|
|
69
|
+
|
|
70
|
+
LOG.debug("clearing eyes and mouth")
|
|
71
|
+
self.__reset()
|
|
72
|
+
|
|
73
|
+
# TODO settings
|
|
74
|
+
# - default eye color
|
|
75
|
+
# - narrow / half / full eyes default position
|
|
76
|
+
self._init_animation()
|
|
77
|
+
|
|
78
|
+
self.bus.on("system.factory.reset.ping", self.handle_register_factory_reset_handler)
|
|
79
|
+
self.bus.on("system.factory.reset.phal", self.handle_factory_reset)
|
|
80
|
+
|
|
81
|
+
self.bus.on("mycroft.internet.connected", self.on_display_reset)
|
|
82
|
+
self.bus.on("mycroft.stop", self.on_display_reset)
|
|
83
|
+
self.bus.on("ovos.common_play.play", self.on_music)
|
|
84
|
+
self.bus.on("ovos.common_play.stop", self.on_display_reset)
|
|
85
|
+
self.bus.on("mycroft.audio.service.play", self.on_music)
|
|
86
|
+
self.bus.on("mycroft.audio.service.stop", self.on_display_reset)
|
|
87
|
+
|
|
88
|
+
self.bus.emit(Message("system.factory.reset.register",
|
|
89
|
+
{"skill_id": "ovos-phal-plugin-mk1"}))
|
|
90
|
+
|
|
91
|
+
def _init_animation(self):
|
|
92
|
+
# change eye color
|
|
93
|
+
r = 0
|
|
94
|
+
g = 0
|
|
95
|
+
b = 255
|
|
96
|
+
color = (r * 65536) + (g * 256) + b
|
|
97
|
+
self._current_rgb = [(r, g, b) for i in range(self._num_pixels)]
|
|
98
|
+
self.writer.write("eyes.color=" + str(color))
|
|
99
|
+
|
|
100
|
+
# narrow eyes while we do system checks
|
|
101
|
+
self.on_eyes_narrow()
|
|
102
|
+
|
|
103
|
+
# signal no internet
|
|
104
|
+
if not is_connected():
|
|
105
|
+
self.on_no_internet()
|
|
106
|
+
|
|
107
|
+
# if core is ready, reset eyes
|
|
108
|
+
self.bus.once("mycroft.ready", self.on_eyes_reset)
|
|
109
|
+
if self._check_services_ready():
|
|
110
|
+
self.on_eyes_reset()
|
|
111
|
+
|
|
112
|
+
def _check_services_ready(self):
|
|
113
|
+
"""Report if all specified services are ready.
|
|
114
|
+
|
|
115
|
+
services (iterable): service names to check.
|
|
116
|
+
"""
|
|
117
|
+
services = {k: False for k in ["skills", # ovos-core
|
|
118
|
+
"audio", # ovos-audio
|
|
119
|
+
"voice" # ovos-dinkum-listener
|
|
120
|
+
]}
|
|
121
|
+
|
|
122
|
+
for ser, rdy in services.items():
|
|
123
|
+
if rdy:
|
|
124
|
+
# already reported ready
|
|
125
|
+
continue
|
|
126
|
+
response = self.bus.wait_for_response(
|
|
127
|
+
Message(f'mycroft.{ser}.is_ready',
|
|
128
|
+
context={"source": "mk1", "destination": "skills"}))
|
|
129
|
+
if response and response.data['status']:
|
|
130
|
+
services[ser] = True
|
|
131
|
+
return all([services[ser] for ser in services])
|
|
132
|
+
|
|
133
|
+
def __init_serial(self):
|
|
134
|
+
LOG.info("Connecting to mark1 faceplate")
|
|
135
|
+
try:
|
|
136
|
+
self.port = self.config.get("port", "/dev/ttyAMA0")
|
|
137
|
+
self.rate = self.config.get("rate", 9600)
|
|
138
|
+
self.timeout = self.config.get("timeout", 5.0)
|
|
139
|
+
self.serial = serial.serial_for_url(
|
|
140
|
+
url=self.port, baudrate=self.rate, timeout=self.timeout)
|
|
141
|
+
LOG.info("Connected to: %s rate: %s timeout: %s" %
|
|
142
|
+
(self.port, self.rate, self.timeout))
|
|
143
|
+
except Exception as e:
|
|
144
|
+
LOG.exception(f"Impossible to connect to serial: {self.port}")
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
def __reset(self, message=None):
|
|
148
|
+
self.writer.write("eyes.reset")
|
|
149
|
+
self.writer.write("mouth.reset")
|
|
150
|
+
|
|
151
|
+
def handle_button_press(self):
|
|
152
|
+
if self.speaking or self.listening:
|
|
153
|
+
self.bus.emit(Message("mycroft.stop"))
|
|
154
|
+
else:
|
|
155
|
+
self.bus.emit(Message("mycroft.mic.listen"))
|
|
156
|
+
|
|
157
|
+
def on_music(self, message=None):
|
|
158
|
+
MusicIcon(bus=self.bus).display()
|
|
159
|
+
|
|
160
|
+
def handle_get_color(self, message):
|
|
161
|
+
"""Get the eye RGB color for all pixels
|
|
162
|
+
Returns:
|
|
163
|
+
(list) list of (r,g,b) tuples for each eye pixel
|
|
164
|
+
"""
|
|
165
|
+
self.bus.emit(message.reply("enclosure.eyes.rgb",
|
|
166
|
+
{"pixels": self._current_rgb}))
|
|
167
|
+
|
|
168
|
+
def handle_factory_reset(self, message):
|
|
169
|
+
self.writer.write("eyes.spin")
|
|
170
|
+
self.writer.write("mouth.reset")
|
|
171
|
+
# TODO re-flash firmware to faceplate
|
|
172
|
+
|
|
173
|
+
def handle_register_factory_reset_handler(self, message):
|
|
174
|
+
self.bus.emit(message.reply("system.factory.reset.register",
|
|
175
|
+
{"skill_id": "ovos-phal-plugin-mk1"}))
|
|
176
|
+
|
|
177
|
+
# Audio Events
|
|
178
|
+
def on_record_begin(self, message=None):
|
|
179
|
+
# NOTE: ignore self._mouth_events, listening should ALWAYS be obvious
|
|
180
|
+
self.listening = True
|
|
181
|
+
self.on_listen(message)
|
|
182
|
+
|
|
183
|
+
def on_record_end(self, message=None):
|
|
184
|
+
self.listening = False
|
|
185
|
+
self.on_display_reset(message)
|
|
186
|
+
|
|
187
|
+
def on_audio_output_start(self, message=None):
|
|
188
|
+
self.speaking = True
|
|
189
|
+
if self._mouth_events:
|
|
190
|
+
self.on_talk(message)
|
|
191
|
+
|
|
192
|
+
def on_audio_output_end(self, message=None):
|
|
193
|
+
self.speaking = False
|
|
194
|
+
if self._mouth_events:
|
|
195
|
+
self.on_display_reset(message)
|
|
196
|
+
|
|
197
|
+
def on_awake(self, message=None):
|
|
198
|
+
''' on wakeup animation
|
|
199
|
+
triggered by "mycroft.awoken"
|
|
200
|
+
'''
|
|
201
|
+
self.writer.write("eyes.reset")
|
|
202
|
+
sleep(1)
|
|
203
|
+
self.writer.write("eyes.blink=b")
|
|
204
|
+
sleep(1)
|
|
205
|
+
# brighten the rest of the way
|
|
206
|
+
self.writer.write("eyes.level=" + str(self.old_brightness))
|
|
207
|
+
|
|
208
|
+
def on_sleep(self, message=None):
|
|
209
|
+
''' on naptime animation
|
|
210
|
+
triggered by "recognizer_loop:sleep"
|
|
211
|
+
'''
|
|
212
|
+
# Dim and look downward to 'go to sleep'
|
|
213
|
+
# TODO: Get current brightness from somewhere
|
|
214
|
+
self.old_brightness = 30
|
|
215
|
+
for i in range(0, (self.old_brightness - 10) // 2):
|
|
216
|
+
level = self.old_brightness - i * 2
|
|
217
|
+
self.writer.write("eyes.level=" + str(level))
|
|
218
|
+
time.sleep(0.15)
|
|
219
|
+
self.writer.write("eyes.look=d")
|
|
220
|
+
|
|
221
|
+
def on_reset(self, message=None):
|
|
222
|
+
"""The enclosure should restore itself to a started state.
|
|
223
|
+
Typically this would be represented by the eyes being 'open'
|
|
224
|
+
and the mouth reset to its default (smile or blank).
|
|
225
|
+
triggered by "enclosure.reset"
|
|
226
|
+
"""
|
|
227
|
+
self.writer.write("eyes.reset")
|
|
228
|
+
self.writer.write("mouth.reset")
|
|
229
|
+
|
|
230
|
+
# System Events
|
|
231
|
+
def on_no_internet(self, message=None):
|
|
232
|
+
"""
|
|
233
|
+
triggered by "enclosure.notify.no_internet"
|
|
234
|
+
"""
|
|
235
|
+
WarningIcon(bus=self.bus).display()
|
|
236
|
+
|
|
237
|
+
def on_system_reset(self, message=None):
|
|
238
|
+
"""The enclosure hardware should reset any CPUs, etc.
|
|
239
|
+
triggered by "enclosure.system.reset"
|
|
240
|
+
"""
|
|
241
|
+
self.writer.write("system.reset")
|
|
242
|
+
|
|
243
|
+
def on_system_mute(self, message=None):
|
|
244
|
+
"""Mute (turn off) the system speaker.
|
|
245
|
+
triggered by "enclosure.system.mute"
|
|
246
|
+
"""
|
|
247
|
+
self.writer.write("system.mute")
|
|
248
|
+
|
|
249
|
+
def on_system_unmute(self, message=None):
|
|
250
|
+
"""Unmute (turn on) the system speaker.
|
|
251
|
+
triggered by "enclosure.system.unmute"
|
|
252
|
+
"""
|
|
253
|
+
self.writer.write("system.unmute")
|
|
254
|
+
|
|
255
|
+
def on_system_blink(self, message=None):
|
|
256
|
+
"""The 'eyes' should blink the given number of times.
|
|
257
|
+
triggered by "enclosure.system.blink"
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
times (int): number of times to blink
|
|
261
|
+
"""
|
|
262
|
+
times = 1
|
|
263
|
+
if message and message.data:
|
|
264
|
+
times = message.data.get("times", times)
|
|
265
|
+
self.writer.write("system.blink=" + str(times))
|
|
266
|
+
|
|
267
|
+
# Eyes messages
|
|
268
|
+
def on_eyes_on(self, message=None):
|
|
269
|
+
"""Illuminate or show the eyes.
|
|
270
|
+
triggered by "enclosure.eyes.on"
|
|
271
|
+
"""
|
|
272
|
+
self.writer.write("eyes.on")
|
|
273
|
+
|
|
274
|
+
def on_eyes_off(self, message=None):
|
|
275
|
+
"""Turn off or hide the eyes.
|
|
276
|
+
triggered by "enclosure.eyes.off"
|
|
277
|
+
"""
|
|
278
|
+
self.writer.write("eyes.off")
|
|
279
|
+
|
|
280
|
+
def on_eyes_fill(self, message=None):
|
|
281
|
+
"""triggered by "enclosure.eyes.fill" """
|
|
282
|
+
amount = 0
|
|
283
|
+
if message and message.data:
|
|
284
|
+
percent = int(message.data.get("percentage", 0))
|
|
285
|
+
amount = int(round(23.0 * percent / 100.0))
|
|
286
|
+
self.writer.write("eyes.fill=" + str(amount))
|
|
287
|
+
|
|
288
|
+
def on_eyes_blink(self, message=None):
|
|
289
|
+
"""Make the eyes blink
|
|
290
|
+
triggered by "enclosure.eyes.blink"
|
|
291
|
+
Args:
|
|
292
|
+
side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both'
|
|
293
|
+
"""
|
|
294
|
+
side = "b"
|
|
295
|
+
if message and message.data:
|
|
296
|
+
side = message.data.get("side", side)
|
|
297
|
+
self.writer.write("eyes.blink=" + side)
|
|
298
|
+
|
|
299
|
+
def on_eyes_narrow(self, message=None):
|
|
300
|
+
"""Make the eyes look narrow, like a squint
|
|
301
|
+
triggered by "enclosure.eyes.narrow"
|
|
302
|
+
"""
|
|
303
|
+
self.writer.write("eyes.narrow")
|
|
304
|
+
|
|
305
|
+
def on_eyes_look(self, message=None):
|
|
306
|
+
"""Make the eyes look to the given side
|
|
307
|
+
triggered by "enclosure.eyes.look"
|
|
308
|
+
Args:
|
|
309
|
+
side (str): 'r' for right
|
|
310
|
+
'l' for left
|
|
311
|
+
'u' for up
|
|
312
|
+
'd' for down
|
|
313
|
+
'c' for crossed
|
|
314
|
+
"""
|
|
315
|
+
if message and message.data:
|
|
316
|
+
side = message.data.get("side", "")
|
|
317
|
+
self.writer.write("eyes.look=" + side)
|
|
318
|
+
|
|
319
|
+
def on_eyes_color(self, message=None):
|
|
320
|
+
"""Change the eye color to the given RGB color
|
|
321
|
+
triggered by "enclosure.eyes.color"
|
|
322
|
+
Args:
|
|
323
|
+
r (int): 0-255, red value
|
|
324
|
+
g (int): 0-255, green value
|
|
325
|
+
b (int): 0-255, blue value
|
|
326
|
+
"""
|
|
327
|
+
r, g, b = 255, 255, 255
|
|
328
|
+
if message and message.data:
|
|
329
|
+
r = int(message.data.get("r", r))
|
|
330
|
+
g = int(message.data.get("g", g))
|
|
331
|
+
b = int(message.data.get("b", b))
|
|
332
|
+
color = (r * 65536) + (g * 256) + b
|
|
333
|
+
self._current_rgb = [(r, g, b) for i in range(self._num_pixels)]
|
|
334
|
+
self.writer.write("eyes.color=" + str(color))
|
|
335
|
+
|
|
336
|
+
def on_eyes_brightness(self, message=None):
|
|
337
|
+
"""Set the brightness of the eyes in the display.
|
|
338
|
+
triggered by "enclosure.eyes.brightness"
|
|
339
|
+
Args:
|
|
340
|
+
level (int): 1-30, bigger numbers being brighter
|
|
341
|
+
"""
|
|
342
|
+
level = 30
|
|
343
|
+
if message and message.data:
|
|
344
|
+
level = message.data.get("level", level)
|
|
345
|
+
self.writer.write("eyes.level=" + str(level))
|
|
346
|
+
|
|
347
|
+
def on_eyes_reset(self, message=None):
|
|
348
|
+
"""Restore the eyes to their default (ready) state
|
|
349
|
+
triggered by "enclosure.eyes.reset".
|
|
350
|
+
"""
|
|
351
|
+
self.writer.write("eyes.reset")
|
|
352
|
+
|
|
353
|
+
def on_eyes_timed_spin(self, message=None):
|
|
354
|
+
"""Make the eyes 'roll' for the given time.
|
|
355
|
+
triggered by "enclosure.eyes.timedspin"
|
|
356
|
+
Args:
|
|
357
|
+
length (int): duration in milliseconds of roll, None = forever
|
|
358
|
+
"""
|
|
359
|
+
length = 5000
|
|
360
|
+
if message and message.data:
|
|
361
|
+
length = message.data.get("length", length)
|
|
362
|
+
self.writer.write("eyes.spin=" + str(length))
|
|
363
|
+
|
|
364
|
+
def on_eyes_volume(self, message=None):
|
|
365
|
+
"""Indicate the volume using the eyes
|
|
366
|
+
triggered by "enclosure.eyes.volume"
|
|
367
|
+
Args:
|
|
368
|
+
volume (int): 0 to 11
|
|
369
|
+
"""
|
|
370
|
+
volume = 4
|
|
371
|
+
if message and message.data:
|
|
372
|
+
volume = message.data.get("volume", volume)
|
|
373
|
+
self.writer.write("eyes.volume=" + str(volume))
|
|
374
|
+
|
|
375
|
+
def on_eyes_spin(self, message=None):
|
|
376
|
+
"""
|
|
377
|
+
triggered by "enclosure.eyes.spin"
|
|
378
|
+
"""
|
|
379
|
+
self.writer.write("eyes.spin")
|
|
380
|
+
|
|
381
|
+
def on_eyes_set_pixel(self, message=None):
|
|
382
|
+
"""
|
|
383
|
+
triggered by "enclosure.eyes.set_pixel"
|
|
384
|
+
"""
|
|
385
|
+
idx = 0
|
|
386
|
+
r, g, b = 255, 255, 255
|
|
387
|
+
if message and message.data:
|
|
388
|
+
idx = int(message.data.get("idx", idx))
|
|
389
|
+
r = int(message.data.get("r", r))
|
|
390
|
+
g = int(message.data.get("g", g))
|
|
391
|
+
b = int(message.data.get("b", b))
|
|
392
|
+
self._current_rgb[idx] = (r, g, b)
|
|
393
|
+
color = (r * 65536) + (g * 256) + b
|
|
394
|
+
self.writer.write("eyes.set=" + str(idx) + "," + str(color))
|
|
395
|
+
|
|
396
|
+
# Display (faceplate) messages
|
|
397
|
+
def on_display_reset(self, message=None):
|
|
398
|
+
"""Restore the mouth display to normal (blank)
|
|
399
|
+
triggered by "enclosure.mouth.reset" / "recognizer_loop:record_end"
|
|
400
|
+
"""
|
|
401
|
+
self.writer.write("mouth.reset")
|
|
402
|
+
|
|
403
|
+
def on_talk(self, message=None):
|
|
404
|
+
"""Show a generic 'talking' animation for non-synched speech
|
|
405
|
+
triggered by "enclosure.mouth.talk"
|
|
406
|
+
"""
|
|
407
|
+
self.writer.write("mouth.talk")
|
|
408
|
+
|
|
409
|
+
def on_think(self, message=None):
|
|
410
|
+
"""Show a 'thinking' image or animation
|
|
411
|
+
triggered by "enclosure.mouth.think"
|
|
412
|
+
"""
|
|
413
|
+
self.writer.write("mouth.think")
|
|
414
|
+
|
|
415
|
+
def on_listen(self, message=None):
|
|
416
|
+
"""Show a 'thinking' image or animation
|
|
417
|
+
triggered by "enclosure.mouth.listen" / "recognizer_loop:record_begin"
|
|
418
|
+
"""
|
|
419
|
+
self.writer.write("mouth.listen")
|
|
420
|
+
|
|
421
|
+
def on_smile(self, message=None):
|
|
422
|
+
"""Show a 'smile' image or animation
|
|
423
|
+
triggered by "enclosure.mouth.smile"
|
|
424
|
+
"""
|
|
425
|
+
self.writer.write("mouth.smile")
|
|
426
|
+
|
|
427
|
+
def on_viseme(self, message=None):
|
|
428
|
+
"""Display a viseme mouth shape for synced speech
|
|
429
|
+
|
|
430
|
+
triggered by "enclosure.mouth.viseme"
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
code (int): 0 = shape for sounds like 'y' or 'aa'
|
|
434
|
+
1 = shape for sounds like 'aw'
|
|
435
|
+
2 = shape for sounds like 'uh' or 'r'
|
|
436
|
+
3 = shape for sounds like 'th' or 'sh'
|
|
437
|
+
4 = neutral shape for no sound
|
|
438
|
+
5 = shape for sounds like 'f' or 'v'
|
|
439
|
+
6 = shape for sounds like 'oy' or 'ao'
|
|
440
|
+
"""
|
|
441
|
+
if message and message.data:
|
|
442
|
+
code = message.data["code"]
|
|
443
|
+
self.writer.write('mouth.viseme=' + code)
|
|
444
|
+
|
|
445
|
+
def on_viseme_list(self, message=None):
|
|
446
|
+
""" Send mouth visemes as a list in a single message.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
start (int): Timestamp for start of speech
|
|
450
|
+
viseme_pairs: Pairs of viseme id and cumulative end times
|
|
451
|
+
(code, end time)
|
|
452
|
+
|
|
453
|
+
codes:
|
|
454
|
+
0 = shape for sounds like 'y' or 'aa'
|
|
455
|
+
1 = shape for sounds like 'aw'
|
|
456
|
+
2 = shape for sounds like 'uh' or 'r'
|
|
457
|
+
3 = shape for sounds like 'th' or 'sh'
|
|
458
|
+
4 = neutral shape for no sound
|
|
459
|
+
5 = shape for sounds like 'f' or 'v'
|
|
460
|
+
6 = shape for sounds like 'oy' or 'ao'
|
|
461
|
+
"""
|
|
462
|
+
if message and message.data:
|
|
463
|
+
start = message.data['start']
|
|
464
|
+
visemes = message.data['visemes']
|
|
465
|
+
|
|
466
|
+
def animate_mouth():
|
|
467
|
+
nonlocal start, visemes
|
|
468
|
+
self.showing_visemes = True
|
|
469
|
+
previous_end = -1
|
|
470
|
+
for code, end in visemes:
|
|
471
|
+
if not self.showing_visemes:
|
|
472
|
+
break
|
|
473
|
+
if end < previous_end:
|
|
474
|
+
start = time.time()
|
|
475
|
+
previous_end = end
|
|
476
|
+
if time.time() < start + end:
|
|
477
|
+
self.writer.write('mouth.viseme=' + code)
|
|
478
|
+
sleep(start + end - time.time())
|
|
479
|
+
self.writer.write("mouth.reset")
|
|
480
|
+
self.showing_visemes = False
|
|
481
|
+
|
|
482
|
+
# use a thread to not block FakeBus (eg, voice sat)
|
|
483
|
+
create_daemon(animate_mouth)
|
|
484
|
+
|
|
485
|
+
def on_text(self, message=None):
|
|
486
|
+
"""Display text (scrolling as needed)
|
|
487
|
+
|
|
488
|
+
triggered by "enclosure.mouth.text"
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
text (str): text string to display
|
|
492
|
+
"""
|
|
493
|
+
text = ""
|
|
494
|
+
if message and message.data:
|
|
495
|
+
text = message.data.get("text", text)
|
|
496
|
+
self.writer.write("mouth.text=" + text)
|
|
497
|
+
|
|
498
|
+
def on_display(self, message=None):
|
|
499
|
+
"""Display images on faceplate. Currently supports images up to 16x8,
|
|
500
|
+
or half the face. You can use the 'x' parameter to cover the other
|
|
501
|
+
half of the faceplate.
|
|
502
|
+
|
|
503
|
+
triggered by "enclosure.mouth.display"
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
img_code (str): text string that encodes a black and white image
|
|
507
|
+
x (int): x offset for image
|
|
508
|
+
y (int): y offset for image
|
|
509
|
+
refresh (bool): specify whether to clear the faceplate before
|
|
510
|
+
displaying the new image or not.
|
|
511
|
+
Useful if you'd like to display muliple images
|
|
512
|
+
on the faceplate at once.
|
|
513
|
+
"""
|
|
514
|
+
code = ""
|
|
515
|
+
x_offset = ""
|
|
516
|
+
y_offset = ""
|
|
517
|
+
clear_previous = ""
|
|
518
|
+
if message and message.data:
|
|
519
|
+
code = message.data.get("img_code", code)
|
|
520
|
+
x_offset = int(message.data.get("xOffset", x_offset))
|
|
521
|
+
y_offset = int(message.data.get("yOffset", y_offset))
|
|
522
|
+
clear_previous = message.data.get("clearPrev", clear_previous)
|
|
523
|
+
|
|
524
|
+
clear_previous = int(str(clear_previous) == "True")
|
|
525
|
+
clear_previous = "cP=" + str(clear_previous) + ","
|
|
526
|
+
x_offset = "x=" + str(x_offset) + ","
|
|
527
|
+
y_offset = "y=" + str(y_offset) + ","
|
|
528
|
+
|
|
529
|
+
message = "mouth.icon=" + x_offset + y_offset + clear_previous + code
|
|
530
|
+
# Check if message exceeds Arduino's serial buffer input limit 64 bytes
|
|
531
|
+
if len(message) > 60:
|
|
532
|
+
message1 = message[:31] + "$"
|
|
533
|
+
message2 = "mouth.icon=$" + message[31:]
|
|
534
|
+
self.writer.write(message1)
|
|
535
|
+
sleep(0.25) # writer bugs out if sending messages too rapidly
|
|
536
|
+
self.writer.write(message2)
|
|
537
|
+
else:
|
|
538
|
+
sleep(0.1)
|
|
539
|
+
self.writer.write(message)
|
|
540
|
+
|
|
541
|
+
def on_weather_display(self, message=None):
|
|
542
|
+
"""Show a the temperature and a weather icon
|
|
543
|
+
|
|
544
|
+
triggered by "enclosure.weather.display"
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
img_code (char): one of the following icon codes
|
|
548
|
+
0 = sunny
|
|
549
|
+
1 = partly cloudy
|
|
550
|
+
2 = cloudy
|
|
551
|
+
3 = light rain
|
|
552
|
+
4 = raining
|
|
553
|
+
5 = stormy
|
|
554
|
+
6 = snowing
|
|
555
|
+
7 = wind/mist
|
|
556
|
+
temp (int): the temperature (either C or F, not indicated)
|
|
557
|
+
"""
|
|
558
|
+
if message and message.data:
|
|
559
|
+
# Convert img_code to icon
|
|
560
|
+
img_code = message.data.get("img_code", None)
|
|
561
|
+
icon = None
|
|
562
|
+
if img_code == 0:
|
|
563
|
+
# sunny
|
|
564
|
+
icon = SunnyIcon(bus=self.bus).encode()
|
|
565
|
+
elif img_code == 1:
|
|
566
|
+
# partly cloudy
|
|
567
|
+
icon = PartlyCloudyIcon(bus=self.bus).encode()
|
|
568
|
+
elif img_code == 2:
|
|
569
|
+
# cloudy
|
|
570
|
+
icon = CloudyIcon(bus=self.bus).encode()
|
|
571
|
+
elif img_code == 3:
|
|
572
|
+
# light rain
|
|
573
|
+
icon = LightRainIcon(bus=self.bus).encode()
|
|
574
|
+
elif img_code == 4:
|
|
575
|
+
# raining
|
|
576
|
+
icon = RainIcon(bus=self.bus).encode()
|
|
577
|
+
elif img_code == 5:
|
|
578
|
+
# storming
|
|
579
|
+
icon = StormIcon(bus=self.bus).encode()
|
|
580
|
+
elif img_code == 6:
|
|
581
|
+
# snowing
|
|
582
|
+
icon = SnowIcon(bus=self.bus).encode()
|
|
583
|
+
elif img_code == 7:
|
|
584
|
+
# wind/mist
|
|
585
|
+
icon = WindIcon(bus=self.bus).encode()
|
|
586
|
+
|
|
587
|
+
temp = message.data.get("temp", None)
|
|
588
|
+
if icon is not None and temp is not None:
|
|
589
|
+
icon = "x=2," + icon
|
|
590
|
+
msg = "weather.display=" + str(temp) + "," + str(icon)
|
|
591
|
+
self.writer.write(msg)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright 2017 Mycroft AI Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import time
|
|
15
|
+
from queue import Queue
|
|
16
|
+
from threading import Thread
|
|
17
|
+
|
|
18
|
+
from ovos_bus_client import Message
|
|
19
|
+
from ovos_utils.log import LOG
|
|
20
|
+
from ovos_utils.signal import check_for_signal
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EnclosureReader(Thread):
|
|
24
|
+
"""
|
|
25
|
+
Reads data from Serial port.
|
|
26
|
+
|
|
27
|
+
Listens to all commands sent by Arduino that must be be performed on
|
|
28
|
+
Mycroft Core.
|
|
29
|
+
|
|
30
|
+
E.g. Mycroft Stop Feature
|
|
31
|
+
# . Arduino sends a Stop command after a button press on a Mycroft unit
|
|
32
|
+
# . ``EnclosureReader`` captures the Stop command
|
|
33
|
+
# . Notify all Mycroft Core processes (e.g. skills) to be stopped
|
|
34
|
+
|
|
35
|
+
Note: A command is identified by a line break
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, serial, bus, button_callback=None):
|
|
39
|
+
super(EnclosureReader, self).__init__(target=self.read)
|
|
40
|
+
self.alive = True
|
|
41
|
+
self.daemon = True
|
|
42
|
+
self.serial = serial
|
|
43
|
+
self.bus = bus
|
|
44
|
+
self.button_callback = button_callback
|
|
45
|
+
self.start()
|
|
46
|
+
|
|
47
|
+
def read(self):
|
|
48
|
+
while self.alive:
|
|
49
|
+
try:
|
|
50
|
+
data = self.serial.readline()[:-2]
|
|
51
|
+
if data:
|
|
52
|
+
try:
|
|
53
|
+
data_str = data.decode()
|
|
54
|
+
except UnicodeError as e:
|
|
55
|
+
data_str = data.decode('utf-8', errors='replace')
|
|
56
|
+
LOG.warning('Invalid characters in response from '
|
|
57
|
+
' enclosure: {}'.format(repr(e)))
|
|
58
|
+
self.process(data_str)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
LOG.error("Reading error: {0}".format(e))
|
|
61
|
+
|
|
62
|
+
def process(self, data):
|
|
63
|
+
LOG.info(f"faceplate event: {data}")
|
|
64
|
+
|
|
65
|
+
if "Command: system.version" in data:
|
|
66
|
+
# This happens in response to the "system.version" message
|
|
67
|
+
# sent during the construction of Enclosure()
|
|
68
|
+
self.bus.emit(Message("enclosure.started"))
|
|
69
|
+
|
|
70
|
+
if "mycroft.stop" in data:
|
|
71
|
+
if self.button_callback:
|
|
72
|
+
self.button_callback()
|
|
73
|
+
else:
|
|
74
|
+
self.bus.emit(Message("mycroft.stop"))
|
|
75
|
+
|
|
76
|
+
if "volume.up" in data:
|
|
77
|
+
self.bus.emit(Message("mycroft.volume.increase",
|
|
78
|
+
{'play_sound': True}))
|
|
79
|
+
|
|
80
|
+
if "volume.down" in data:
|
|
81
|
+
self.bus.emit(Message("mycroft.volume.decrease",
|
|
82
|
+
{'play_sound': True}))
|
|
83
|
+
|
|
84
|
+
if "unit.shutdown" in data:
|
|
85
|
+
# Eyes to soft gray on shutdown
|
|
86
|
+
self.bus.emit(Message("enclosure.eyes.color",
|
|
87
|
+
{'r': 70, 'g': 65, 'b': 69}))
|
|
88
|
+
self.bus.emit(
|
|
89
|
+
Message("enclosure.eyes.timedspin",
|
|
90
|
+
{'length': 12000}))
|
|
91
|
+
self.bus.emit(Message("enclosure.mouth.reset"))
|
|
92
|
+
time.sleep(0.5) # give the system time to pass the message
|
|
93
|
+
self.bus.emit(Message("system.shutdown"))
|
|
94
|
+
|
|
95
|
+
if "unit.reboot" in data:
|
|
96
|
+
# Eyes to soft gray on reboot
|
|
97
|
+
self.bus.emit(Message("enclosure.eyes.color",
|
|
98
|
+
{'r': 70, 'g': 65, 'b': 69}))
|
|
99
|
+
self.bus.emit(Message("enclosure.eyes.spin"))
|
|
100
|
+
self.bus.emit(Message("enclosure.mouth.reset"))
|
|
101
|
+
time.sleep(0.5) # give the system time to pass the message
|
|
102
|
+
self.bus.emit(Message("system.reboot"))
|
|
103
|
+
|
|
104
|
+
if "unit.setwifi" in data:
|
|
105
|
+
self.bus.emit(Message("system.wifi.setup"))
|
|
106
|
+
if "unit.factory-reset" in data:
|
|
107
|
+
self.bus.emit(Message("system.factory.reset")) # not in mycroft-core!
|
|
108
|
+
if "unit.enable-ssh" in data:
|
|
109
|
+
# This is handled by the wifi client
|
|
110
|
+
self.bus.emit(Message("system.ssh.enable"))
|
|
111
|
+
if "unit.disable-ssh" in data:
|
|
112
|
+
# This is handled by the wifi client
|
|
113
|
+
self.bus.emit(Message("system.ssh.disable"))
|
|
114
|
+
|
|
115
|
+
def stop(self):
|
|
116
|
+
self.alive = False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class EnclosureWriter(Thread):
|
|
120
|
+
"""
|
|
121
|
+
Writes data to Serial port.
|
|
122
|
+
# . Enqueues all commands received from Mycroft enclosures
|
|
123
|
+
implementation
|
|
124
|
+
# . Process them on the received order by writing on the Serial port
|
|
125
|
+
|
|
126
|
+
E.g. Displaying a text on Mycroft's Mouth
|
|
127
|
+
# . ``EnclosureMouth`` sends a text command
|
|
128
|
+
# . ``EnclosureWriter`` captures and enqueue the command
|
|
129
|
+
# . ``EnclosureWriter`` removes the next command from the queue
|
|
130
|
+
# . ``EnclosureWriter`` writes the command to Serial port
|
|
131
|
+
|
|
132
|
+
Note: A command has to end with a line break
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, serial, bus, size=16):
|
|
136
|
+
super(EnclosureWriter, self).__init__(target=self.flush)
|
|
137
|
+
self.alive = True
|
|
138
|
+
self.daemon = True
|
|
139
|
+
self.serial = serial
|
|
140
|
+
self.bus = bus
|
|
141
|
+
self.commands = Queue(size)
|
|
142
|
+
self.start()
|
|
143
|
+
|
|
144
|
+
def flush(self):
|
|
145
|
+
while self.alive:
|
|
146
|
+
try:
|
|
147
|
+
cmd = self.commands.get() + '\n'
|
|
148
|
+
self.serial.write(cmd.encode())
|
|
149
|
+
self.commands.task_done()
|
|
150
|
+
except Exception as e:
|
|
151
|
+
LOG.error("Writing error: {0}".format(e))
|
|
152
|
+
|
|
153
|
+
def write(self, command):
|
|
154
|
+
self.commands.put(str(command))
|
|
155
|
+
|
|
156
|
+
def stop(self):
|
|
157
|
+
self.alive = False
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 1.1
|
|
2
|
+
Name: ovos-PHAL-plugin-mk1
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A PHAL plugin for mycroft
|
|
5
|
+
Home-page: https://github.com/OpenVoiceOS/ovos-PHAL-plugin-mk1
|
|
6
|
+
Author: JarbasAi
|
|
7
|
+
Author-email: jarbasai@mailfence.com
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Description: UNKNOWN
|
|
10
|
+
Platform: UNKNOWN
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.5
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
ovos_PHAL_plugin_mk1/__init__.py
|
|
5
|
+
ovos_PHAL_plugin_mk1/arduino.py
|
|
6
|
+
ovos_PHAL_plugin_mk1/version.py
|
|
7
|
+
ovos_PHAL_plugin_mk1.egg-info/PKG-INFO
|
|
8
|
+
ovos_PHAL_plugin_mk1.egg-info/SOURCES.txt
|
|
9
|
+
ovos_PHAL_plugin_mk1.egg-info/dependency_links.txt
|
|
10
|
+
ovos_PHAL_plugin_mk1.egg-info/entry_points.txt
|
|
11
|
+
ovos_PHAL_plugin_mk1.egg-info/requires.txt
|
|
12
|
+
ovos_PHAL_plugin_mk1.egg-info/top_level.txt
|
|
13
|
+
ovos_PHAL_plugin_mk1.egg-info/zip-safe
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ovos_PHAL_plugin_mk1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
from os.path import join, dirname
|
|
4
|
+
|
|
5
|
+
from setuptools import setup
|
|
6
|
+
|
|
7
|
+
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def required(requirements_file):
|
|
11
|
+
""" Read requirements file and remove comments and empty lines. """
|
|
12
|
+
with open(os.path.join(BASEDIR, requirements_file), 'r') as f:
|
|
13
|
+
requirements = f.read().splitlines()
|
|
14
|
+
if 'MYCROFT_LOOSE_REQUIREMENTS' in os.environ:
|
|
15
|
+
print('USING LOOSE REQUIREMENTS!')
|
|
16
|
+
requirements = [r.replace('==', '>=').replace('~=', '>=') for r in requirements]
|
|
17
|
+
return [pkg for pkg in requirements
|
|
18
|
+
if pkg.strip() and not pkg.startswith("#")]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_version():
|
|
22
|
+
""" Find the version of this skill"""
|
|
23
|
+
version_file = join(BASEDIR, 'ovos_PHAL_plugin_mk1/version.py')
|
|
24
|
+
major, minor, build, alpha = (None, None, None, None)
|
|
25
|
+
with open(version_file) as f:
|
|
26
|
+
for line in f:
|
|
27
|
+
if 'VERSION_MAJOR' in line:
|
|
28
|
+
major = line.split('=')[1].strip()
|
|
29
|
+
elif 'VERSION_MINOR' in line:
|
|
30
|
+
minor = line.split('=')[1].strip()
|
|
31
|
+
elif 'VERSION_BUILD' in line:
|
|
32
|
+
build = line.split('=')[1].strip()
|
|
33
|
+
elif 'VERSION_ALPHA' in line:
|
|
34
|
+
alpha = line.split('=')[1].strip()
|
|
35
|
+
|
|
36
|
+
if ((major and minor and build and alpha) or
|
|
37
|
+
'# END_VERSION_BLOCK' in line):
|
|
38
|
+
break
|
|
39
|
+
version = f"{major}.{minor}.{build}"
|
|
40
|
+
if int(alpha):
|
|
41
|
+
version += f"a{alpha}"
|
|
42
|
+
return version
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
PLUGIN_ENTRY_POINT = 'ovos-phal-mk1=ovos_PHAL_plugin_mk1:MycroftMark1'
|
|
46
|
+
setup(
|
|
47
|
+
name='ovos-PHAL-plugin-mk1',
|
|
48
|
+
version=get_version(),
|
|
49
|
+
description='A PHAL plugin for mycroft',
|
|
50
|
+
url='https://github.com/OpenVoiceOS/ovos-PHAL-plugin-mk1',
|
|
51
|
+
author='JarbasAi',
|
|
52
|
+
author_email='jarbasai@mailfence.com',
|
|
53
|
+
license='Apache-2.0',
|
|
54
|
+
packages=['ovos_PHAL_plugin_mk1'],
|
|
55
|
+
install_requires=required("requirements.txt"),
|
|
56
|
+
zip_safe=True,
|
|
57
|
+
classifiers=[
|
|
58
|
+
'Development Status :: 3 - Alpha',
|
|
59
|
+
'Intended Audience :: Developers',
|
|
60
|
+
'Topic :: Text Processing :: Linguistic',
|
|
61
|
+
'License :: OSI Approved :: Apache Software License',
|
|
62
|
+
'Programming Language :: Python :: 3.5',
|
|
63
|
+
'Programming Language :: Python :: 3.6',
|
|
64
|
+
],
|
|
65
|
+
entry_points={'ovos.plugin.phal': PLUGIN_ENTRY_POINT}
|
|
66
|
+
)
|