Skip to content

Commit 64882c5

Browse files
committed
ovos-skill-mark1-ctrl
1 parent 310b326 commit 64882c5

File tree

298 files changed

+5236
-70
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

298 files changed

+5236
-70
lines changed

README.md

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
1-
## Enclosure Control
1+
## Mark1 Enclosure Control
22

33
Controls the enclosure api vocally
44

55
## Description
66

7-
This skill can be used to trigger changes in the enclosure
7+
The Mycroft Mark 1 has several unique capabilities which this Skill lets you control.
8+
9+
The Mark 1 has beautiful eyes -- and you get to pick their color! Set them to
10+
a named color ("blue", "magenta", "teal", etc) or any color using RGB values.
11+
Please see the [color](./locale/en-us/colors.value)
12+
list for more options
13+
814

915
## Examples
1016

11-
* "look up"
12-
* "look down"
13-
* "look left"
14-
* "look right"
15-
* "look left and right"
16-
* "look up and down"
17-
* "reset enclosure"
18-
* "narrow your eyes"
19-
* "spin your eyes"
20-
* "blink your eyes"
21-
* "perform system reboot"
22-
* "system mute"
23-
* "system unmute"
24-
* "smile animation"
25-
* "listen animation"
26-
* "think animation"
27-
28-
## TODO
29-
30-
* use dialog files
31-
* more animations
32-
* ssh enable/disable intents
33-
* Intents for viseme cycling / talking
34-
* fill eyes intents
17+
* "Set your eye color to green"
18+
* "Set a custom eye color" (you'll be prompted for values)
19+
* "Change to low brightness"
20+
* "Dim to 50%"
21+
* "look up"
22+
* "look down"
23+
* "look left"
24+
* "look right"
25+
* "look left and right"
26+
* "look up and down"
27+
* "reset enclosure"
28+
* "narrow your eyes"
29+
* "spin your eyes"
30+
* "blink your eyes"
31+
* "smile animation"
32+
* "listen animation"
33+
* "think animation"
34+
3535

3636
## Credits
3737

3838
JarbasAI
39+
40+
MycroftAI - https://github.com/MycroftAI/mycroft-mark-1

__init__.py

Lines changed: 251 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,67 @@
11
import random
22
import time
3-
from threading import Thread
4-
5-
from ovos_utils.intents import IntentBuilder
3+
from ast import literal_eval as parse_tuple
4+
from difflib import SequenceMatcher
5+
from ovos_utils import create_daemon
66
from ovos_workshop.decorators import intent_handler
7+
from ovos_workshop.intents import IntentBuilder
78
from ovos_workshop.skills import OVOSSkill
9+
from threading import Thread
10+
11+
12+
def _hex_to_rgb(_hex):
13+
""" Convert hex color code to RGB tuple
14+
Args:
15+
hex (str): Hex color string, e.g '#ff12ff' or 'ff12ff'
16+
Returns:
17+
(rgb): tuple i.e (123, 200, 155) or None
18+
"""
19+
try:
20+
if '#' in _hex:
21+
_hex = _hex.replace('#', "").strip()
22+
if len(_hex) != 6:
23+
return None
24+
(r, g, b) = int(_hex[0:2], 16), int(_hex[2:4], 16), int(_hex[4:6], 16)
25+
return (r, g, b)
26+
except Exception:
27+
return None
28+
29+
30+
def fuzzy_match_color(color_a, color_dict):
31+
""" fuzzy match for colors
32+
33+
Args:
34+
color_a (str): color as string
35+
color_dict (dict): dict with colors
36+
Returns:
37+
color: color from color_dict
38+
"""
39+
highest_ratio = float("-inf")
40+
_color = None
41+
for color, value in color_dict.items():
42+
s = SequenceMatcher(None, color_a, color)
43+
if s.ratio() > highest_ratio:
44+
highest_ratio = s.ratio()
45+
_color = color
46+
if highest_ratio > 0.8:
47+
return _color
48+
else:
49+
return None
850

951

1052
class EnclosureControlSkill(OVOSSkill):
1153
def initialize(self):
1254
self.thread = None
1355
self.playing = False
1456
self.animations = []
57+
self.brightness_dict = self.translate_namedvalues('brightness.levels')
58+
self.color_dict = self.translate_namedvalues('colors')
59+
self.add_event('mycroft.eyes.default', self.handle_default_eyes)
60+
self.add_event('mycroft.ready', self.handle_default_eyes)
61+
62+
# TODO: Add OVOSSkill.register_entity_list() and use the
63+
# self.color_dict.keys() instead of duplicating data
64+
self.register_entity_file('color.entity')
1565

1666
@property
1767
def crazy_eyes_animation(self):
@@ -113,27 +163,7 @@ def play_animation(self, animation=None):
113163

114164
# Build the list of animation actions to run
115165
self.animations = animation
116-
self.thread = Thread(None, self.run)
117-
self.thread.daemon = True
118-
self.thread.start()
119-
120-
@intent_handler(IntentBuilder("SystemReboot")
121-
.require("perform").require("system").require("reboot"))
122-
def handle_system_reboot(self, message):
123-
self.speak("rebooting")
124-
self.bus.emit(message.reply("system.reboot", {}))
125-
126-
@intent_handler(IntentBuilder("SystemUnmute")
127-
.require("system").require("unmute"))
128-
def handle_system_unmute(self, message):
129-
self.enclosure.system_unmute()
130-
self.speak("now that i have a voice, i shall not be silent")
131-
132-
@intent_handler(IntentBuilder("SystemMute")
133-
.require("system").require("mute"))
134-
def handle_system_mute(self, message):
135-
self.speak("am i that annoying?")
136-
self.enclosure.system_mute()
166+
self.thread = create_daemon(self.run)
137167

138168
@intent_handler(IntentBuilder("EnclosureLookRight")
139169
.require("look").require("right")
@@ -241,10 +271,206 @@ def handle_enclosure_crazy_eyes(self, message):
241271
"stupidity, you don't see this every day")
242272
self.play_animation(self.crazy_eyes_animation)
243273

274+
#####################################################################
275+
# Color interactions
276+
def set_eye_color(self, color=None, rgb=None, speak=True):
277+
""" Change the eye color on the faceplate, update saved setting
278+
"""
279+
if color is not None:
280+
color_rgb = self._parse_to_rgb(color)
281+
if color_rgb is not None:
282+
(r, g, b) = color_rgb
283+
elif rgb is not None:
284+
(r, g, b) = rgb
285+
else:
286+
return # no color provided!
287+
288+
try:
289+
self.enclosure.eyes_color(r, g, b)
290+
if speak:
291+
self.speak_dialog('set.color.success')
292+
# Update saved color
293+
self.settings['current_eye_color'] = [r, g, b]
294+
except Exception:
295+
self.log.debug('Bad color code: ' + str(color))
296+
if speak:
297+
self.speak_dialog('error.set.color')
298+
299+
@intent_handler('custom.eye.color.intent')
300+
def handle_custom_eye_color(self, message):
301+
# Conversational interaction to set a custom eye color
302+
303+
def is_byte(utt):
304+
try:
305+
return 0 <= int(utt) <= 255
306+
except Exception:
307+
return False
308+
309+
self.speak_dialog('set.custom.color')
310+
wait_while_speaking()
311+
r = self.get_response('get.r.value', validator=is_byte,
312+
on_fail="error.rgbvalue", num_retries=2)
313+
if not r:
314+
return # cancelled
315+
316+
g = self.get_response('get.g.value', validator=is_byte,
317+
on_fail="error.rgbvalue", num_retries=2)
318+
if not g:
319+
return # cancelled
320+
321+
b = self.get_response('get.b.value', validator=is_byte,
322+
on_fail="error.rgbvalue", num_retries=2)
323+
if not b:
324+
return # cancelled
325+
326+
custom_rgb = [r, g, b]
327+
self.set_eye_color(rgb=custom_rgb)
328+
329+
@intent_handler('eye.color.intent')
330+
def handle_eye_color(self, message):
331+
""" Callback to set eye color from list
332+
333+
Args:
334+
message (dict): messagebus message from intent parser
335+
"""
336+
color_str = (message.data.get('color', None) or
337+
self.get_response('color.need'))
338+
if color_str:
339+
match = fuzzy_match_color(normalize(color_str), self.color_dict)
340+
if match is not None:
341+
self.set_eye_color(color=match)
342+
else:
343+
self.speak_dialog('color.not.exist')
344+
345+
def _parse_to_rgb(self, color):
346+
""" Convert color descriptor to RGB
347+
348+
Parse a color name ('dark blue'), hex ('#000088') or rgb tuple
349+
'(0,0,128)' to an RGB tuple.
350+
351+
Args:
352+
color (str): RGB, Hex, or color from color_dict
353+
Returns:
354+
(r, g, b) (tuple): Tuple of rgb values (0-255) or None
355+
"""
356+
if not color:
357+
return None
358+
359+
# check if named color in dict
360+
try:
361+
if color.lower() in self.color_dict:
362+
return _hex_to_rgb(self.color_dict[color.lower()])
363+
except Exception:
364+
pass
365+
366+
# check if rgb tuple like '(0,0,128)'
367+
try:
368+
(r, g, b) = parse_tuple(color)
369+
if 0 <= r <= 255 and 0 <= g <= 255 and 0 <= b <= 255:
370+
return (r, g, b)
371+
else:
372+
return None
373+
except Exception:
374+
pass
375+
376+
# Finally check if color is hex, like '#0000cc' or '0000cc'
377+
return _hex_to_rgb(color)
378+
379+
def handle_default_eyes(self, message):
380+
if self.settings.get('current_eye_color'):
381+
self.set_eye_color(self.settings['current_eye_color'], speak=False)
382+
383+
#####################################################################
384+
# Brightness intent interaction
385+
386+
def percent_to_level(self, percent):
387+
""" converts the brigtness value from percentage to
388+
a value arduino can read
389+
390+
Args:
391+
percent (int): interger value from 0 to 100
392+
393+
return:
394+
(int): value form 0 to 30
395+
"""
396+
return int(float(percent) / float(100) * 30)
397+
398+
def parse_brightness(self, brightness):
399+
""" parse text for brightness percentage
400+
401+
Args:
402+
brightness (str): string containing brightness level
403+
404+
return:
405+
(int): brightness as percentage (0-100)
406+
"""
407+
408+
try:
409+
# Handle "full", etc.
410+
name = normalize(brightness)
411+
if name in self.brightness_dict:
412+
return self.brightness_dict[name]
413+
414+
if '%' in brightness:
415+
brightness = brightness.replace("%", "").strip()
416+
return int(brightness)
417+
if 'percent' in brightness:
418+
brightness = brightness.replace("percent", "").strip()
419+
return int(brightness)
420+
421+
i = int(brightness)
422+
if i < 0 or i > 100:
423+
return None
424+
425+
if i < 30:
426+
# Assume plain 0-30 is "level"
427+
return int((i * 100.0) / 30.0)
428+
429+
# Assume plain 31-100 is "percentage"
430+
return i
431+
except Exception:
432+
return None # failed in an int() conversion
433+
434+
def set_eye_brightness(self, level, speak=True):
435+
""" Actually change hardware eye brightness
436+
437+
Args:
438+
level (int): 0-30, brightness level
439+
speak (bool): when True, speak a confirmation
440+
"""
441+
self.enclosure.eyes_brightness(level)
442+
if speak is True:
443+
percent = int(float(level) * float(100) / float(30))
444+
self.speak_dialog(
445+
'brightness.set', data={'val': str(percent) + '%'})
446+
447+
def _set_brightness(self, brightness):
448+
# brightness can be a number or word like "full", "half"
449+
percent = self.parse_brightness(brightness)
450+
if percent is None:
451+
self.speak_dialog('brightness.not.found.final')
452+
elif int(percent) is -1:
453+
self.handle_auto_brightness(None)
454+
else:
455+
self.auto_brightness = False
456+
self.set_eye_brightness(self.percent_to_level(percent))
457+
458+
@intent_handler('brightness.intent')
459+
def handle_brightness(self, message):
460+
""" Intent Callback to set custom eye colors in rgb
461+
462+
Args:
463+
message (dict): messagebus message from intent parser
464+
"""
465+
brightness = (message.data.get('brightness', None) or
466+
self.get_response('brightness.not.found'))
467+
if brightness:
468+
self._set_brightness(brightness)
469+
244470
def stop(self):
245471
if self.playing and self.thread is not None:
246472
self.playing = False
247-
self.thread.join(1)
248473
self.enclosure.activate_mouth_events()
249474
self.enclosure.eyes_reset()
250475
return True
476+
return False

locale/ca-es/brightness.entity

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#
2+
##
3+
###
4+
#%
5+
##%
6+
###%
7+
complet|ple|plena
8+
brillant|lluminós
9+
meitat
10+
atenuar
11+
baix
12+
automàtic|automàtica|automàticament
13+
automàtic
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
No entenc aquest valor de brillantor, doneu-me un valor de brillantor entre el 0 i el 100 per cent.

0 commit comments

Comments
 (0)